我正在研究一些需要高度优化的Java代码,因为它将在我的主程序逻辑中的许多点调用的热函数中运行。这段代码的一部分包括用double
变量乘以任意非负的int
exponent
s。获得相乘值的一个快速方法(编辑:但不是最快的,请参阅下面的更新2)是exponent
上的switch
:
double multiplyByPowerOfTen(final double d, final int exponent) {
switch (exponent) {
case 0:
return d;
case 1:
return d*10;
case 2:
return d*100;
// ... same pattern
case 9:
return d*1000000000;
case 10:
return d*10000000000L;
// ... same pattern with long literals
case 18:
return d*1000000000000000000L;
default:
throw new ParseException("Unhandled power of ten " + power, 0);
}
}
上面注释的省略号表明case
int
常量继续加1,因此在上面的代码段中实际上有19个case
。由于我不确定我是否真的需要在case
语句10
到18
中完成所有10的幂,我运行了一些微基准测试,比较用这个switch
语句完成1000万次操作的时间,以及用只有case
的0
到int
0的switch
(将int
1限制为9或更少,以避免破坏削减后的switch
)。我得到了一个相当令人惊讶的结果(至少对我来说!),更长的switch
和更多的case
语句实际上运行得更快。
我尝试添加更多返回虚拟值的__abc0,并发现在声明了大约22-27个__abc0时,我可以让开关运行得更快(尽管在代码运行时,这些虚拟情况从未真正发生)。(同样,case
是通过将前面的case
常量加1
来连续添加的。)这些执行时间差异不是很显著:对于0
和10
之间的随机exponent
,虚拟填充switch
语句在1.49秒内完成1000万次执行,而非填充版本的执行时间为1.54秒,每次执行总共节省了5ns。因此,从优化的角度来看,这种事情并不值得为填充switch
语句而付出努力。但我仍然觉得奇怪和违反直觉的是,当更多的__abc0被添加到switch
中时,switch
的执行速度并没有变慢(或者最好保持不变的case
2时间)。
这些是我通过运行随机生成的exponent
值的各种限制获得的结果。我没有包括一直到1
的exponent
限制的结果,但曲线的一般形状保持不变,在12-17格标记附近有一个山脊,在18-28格标记之间有一个山谷。所有测试都在junitbenchmark中运行,使用随机值的共享容器以确保相同的测试输入。我还按照从最长的switch
语句到最短的顺序运行测试,反之亦然,以尝试并消除与排序相关的测试问题的可能性。我已经把我的测试代码放在一个github回购,如果有人想尝试重现这些结果。
那么,这里发生了什么?我的架构或微基准构建的一些古怪之处?或者Java switch
在18
到28
case
范围内执行的速度真的比从11
到17
快一点吗?
github test repo "switch-experiment"
更新:我清理了相当多的基准测试库,并在/results中添加了一个文本文件,在更广泛的可能exponent
值范围内输出一些结果。我还在测试代码中添加了一个选项,不从default
抛出Exception
,但这似乎不会影响结果。
在2009年的xkcd论坛上找到了一些关于这个问题的很好的讨论:http://forums.xkcd.com/viewtopic.php?f=11&t=33524。OP中关于使用Array.binarySearch()
的讨论让我想到了上面的求幂模式的一个简单的基于数组的实现。没有必要进行二进制搜索,因为我知道array
中的条目是什么。它似乎比使用switch
快3倍,显然是以牺牲switch
提供的一些控制流为代价的。该代码已添加到github回购也。