我正在研究一些需要高度优化的Java代码,因为它将在我的主程序逻辑中的许多点调用的热函数中运行。这段代码的一部分包括用double变量乘以任意非负的int exponents。获得相乘值的一个快速方法(编辑:但不是最快的,请参阅下面的更新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到int0的switch(将int1限制为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的执行速度并没有变慢(或者最好保持不变的case2时间)。

这些是我通过运行随机生成的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回购也。