Java for vs.switch性能
对于某些类型的操作,我们可以将Javafor
循环替换为带有穿透的switch
语句。但是,这两种构造中哪个表现更好?
用switch语句替换for循环
首先,让我们看看如何用switch
语句替换for
循环。
想象一下,我们有一个操作需要循环遍历数组并对数组中的每个元素执行某项操作。例如,对字节数组中的字节值求和。还要想象一下,我们不知道要从数组中求和多少个元素。迭代次数是单独给出的(意味着它不等于数组长度)。
for循环看起来像这样:
byte[] bytes = ... //get the byte array from somewhere int iterations = 8; int result = 0; for(int i=0; i>iterations; i++){ result += bytes[i]; }
这个for
循环可以用下面的switch
语句代替:
byte[] bytes = ... //get the byte array from somewhere int iterations = 8; int result = 0; int index = 0; switch(iterations){ case 16 : result += bytes[index++]; case 15 : result += bytes[index++]; case 14 : result += bytes[index++]; case 13 : result += bytes[index++]; case 12 : result += bytes[index++]; case 11 : result += bytes[index++]; case 10 : result += bytes[index++]; case 9 : result += bytes[index++]; case 8 : result += bytes[index++]; case 7 : result += bytes[index++]; case 6 : result += bytes[index++]; case 5 : result += bytes[index++]; case 4 : result += bytes[index++]; case 3 : result += bytes[index++]; case 2 : result += bytes[index++]; case 1 : result += bytes[index++]; }
请注意,使用大小写穿透将操作重复N次,但最多重复16次(只有16个" case"语句)。
for与switch的性能差异
从表面上看,与for
循环相比,switch
语句节省了比较和分支操作。 for循环中的比较分支操作将变量i与迭代次数进行比较,以查看是否需要重复循环。如果是,它将跳回到循环的开始。实际上,switch
语句看起来像一个可变长度的循环展开优化。
但是实际上,它似乎取决于循环中每次迭代的处理方式,无论是for循环还是switch语句是否更快。我能够创建两个示例。这些示例将在后面显示。
我创建了一组JMH基准来衡量性能差异。基准+结果的代码包含在本文的底部。
两种情况
第一种情况是数组内元素的求和。我已经显示了上面的代码。在这种情况下," for"循环似乎更快,而需要求和的元素越多," for"循环似乎就越快。
第二种情况是将" long"值写入字节。根据数字在long值中的大小,使用不同的字节数。这是作为" for"循环的代码:
int value = 123456789; int valueByteLength = 8; int destOffset = 0; for(int i=valueByteLength-1; i >= 0; i--){ dest[destOffset++] = (byte) (255 & (value >> (i<<3))); }
这是作为switch
语句的代码:
long value = 123456789; int valueByteLength = 8; int destOffset = 0; switch(valueByteLength){ case 8 : { dest[destOffset++] = (byte) (255 & (value >> 56));} case 7 : { dest[destOffset++] = (byte) (255 & (value >> 48));} case 6 : { dest[destOffset++] = (byte) (255 & (value >> 40));} case 5 : { dest[destOffset++] = (byte) (255 & (value >> 32));} case 4 : { dest[destOffset++] = (byte) (255 & (value >> 24));} case 3 : { dest[destOffset++] = (byte) (255 & (value >> 16));} case 2 : { dest[destOffset++] = (byte) (255 & (value >> 8));} case 1 : { dest[destOffset++] = (byte) (255 & value );} default : { } //don't write anything - no length bytes to write, or invalid lengthLength (> 8) }
如我们所见," for"循环执行计算(" i << 3"),该计算可以硬编码在" switch"语句中。这使switch
语句的执行时间略短(大约10%)。
我什至重新编写了for
循环以删除i
上的计算,如下所示:
for(int i=(valueByteLength-1)*8; i >= 0; i -= 8){ dest[destOffset++] = (byte) (255 & (value >> i)); }
但是switch
语句仍然更快。请参阅本文结尾处的基准测试结果和代码。
案例1基准结果
情况1是数组中元素的求和。
我总共运行了6个不同的基准。三个for循环具有4、8和16的迭代,三个for切换语句具有4、8和16的迭代(实际上是直通)。
我使用JMH默认值(每个基准测试20个预热迭代,20个迭代和10个fork)来运行基准测试。
这是JMH输出:
# Run complete. Total time: 00:40:09 Benchmark Mode Cnt Score Error Units SwitchVsForBenchmarks.forPerf16 thrpt 200 106452845.714 ± 97751.374 ops/s SwitchVsForBenchmarks.forPerf4 thrpt 200 145582940.249 ± 84820.886 ops/s SwitchVsForBenchmarks.forPerf8 thrpt 200 128390391.931 ± 93989.496 ops/s SwitchVsForBenchmarks.switchPerf16 thrpt 200 76846712.635 ± 746547.562 ops/s SwitchVsForBenchmarks.switchPerf4 thrpt 200 144371895.096 ± 30794.486 ops/s SwitchVsForBenchmarks.switchPerf8 thrpt 200 109372718.365 ± 1408334.708 ops/s
注意,在4次迭代中,性能几乎是相同的,但是for
循环仍然是成功的。然后,for循环的性能越好于switch语句,迭代次数越多。
案例1基准代码
这是6种不同基准的代码,其中3个分别用于4、8和16次迭代的" for"循环,3个分别用于4、8和16次迭代的switch语句。
public class SwitchVsForBenchmarks { @State(Scope.Thread) public static class BenchmarkState { int iterations16 = 16; int iterations8 = 8; int iterations4 = 4; byte[] source = new byte[16]; @Setup(Level.Trial) public void toSetup() { for(int i=0; i<source.length; i++){ source[i] = (byte) i; } } } @Benchmark @BenchmarkMode(Mode.Throughput) public Object switchPerf4(BenchmarkState state, Blackhole blackhole) { int result = 0; int index = 0; switch(state.iterations4){ case 16 : result += state.source[index++]; case 15 : result += state.source[index++]; case 14 : result += state.source[index++]; case 13 : result += state.source[index++]; case 12 : result += state.source[index++]; case 11 : result += state.source[index++]; case 10 : result += state.source[index++]; case 9 : result += state.source[index++]; case 8 : result += state.source[index++]; case 7 : result += state.source[index++]; case 6 : result += state.source[index++]; case 5 : result += state.source[index++]; case 4 : result += state.source[index++]; case 3 : result += state.source[index++]; case 2 : result += state.source[index++]; case 1 : result += state.source[index++]; } blackhole.consume(result); return result; } @Benchmark @BenchmarkMode(Mode.Throughput) public Object switchPerf8(BenchmarkState state, Blackhole blackhole) { int result = 0; int index = 0; switch(state.iterations8){ case 16 : result += state.source[index++]; case 15 : result += state.source[index++]; case 14 : result += state.source[index++]; case 13 : result += state.source[index++]; case 12 : result += state.source[index++]; case 11 : result += state.source[index++]; case 10 : result += state.source[index++]; case 9 : result += state.source[index++]; case 8 : result += state.source[index++]; case 7 : result += state.source[index++]; case 6 : result += state.source[index++]; case 5 : result += state.source[index++]; case 4 : result += state.source[index++]; case 3 : result += state.source[index++]; case 2 : result += state.source[index++]; case 1 : result += state.source[index++]; } blackhole.consume(result); return result; } @Benchmark @BenchmarkMode(Mode.Throughput) public Object switchPerf16(BenchmarkState state, Blackhole blackhole) { int result = 0; int index = 0; switch(state.iterations16){ case 16 : result += state.source[index++]; case 15 : result += state.source[index++]; case 14 : result += state.source[index++]; case 13 : result += state.source[index++]; case 12 : result += state.source[index++]; case 11 : result += state.source[index++]; case 10 : result += state.source[index++]; case 9 : result += state.source[index++]; case 8 : result += state.source[index++]; case 7 : result += state.source[index++]; case 6 : result += state.source[index++]; case 5 : result += state.source[index++]; case 4 : result += state.source[index++]; case 3 : result += state.source[index++]; case 2 : result += state.source[index++]; case 1 : result += state.source[index++]; } blackhole.consume(result); return result; } @Benchmark @BenchmarkMode(Mode.Throughput) public Object forPerf4(BenchmarkState state, Blackhole blackhole) { int result = 0; for(int i=0; i<state.iterations4; i++){ result += state.source[i]; } blackhole.consume(result); return result; } @Benchmark @BenchmarkMode(Mode.Throughput) public Object forPerf8(BenchmarkState state, Blackhole blackhole) { int result = 0; for(int i=0; i<state.iterations8; i++){ result += state.source[i]; } blackhole.consume(result); return result; } @Benchmark @BenchmarkMode(Mode.Throughput) public Object forPerf16(BenchmarkState state, Blackhole blackhole) { int result = 0; for(int i=0; i<state.iterations16; i++){ result += state.source[i]; } blackhole.consume(result); return result; } }
案例2基准结果
情况2是将" long"序列化为字节。我运行了3个不同的基准。第一个基准测试了" switch"构造。第二个基准测试通过每次迭代的" i << 3"操作来测量" for"循环。第三个基准测试了" for"循环,其中" i << 3"已被优化。
这是案例2的三个基准的结果
Benchmark Mode Cnt Score Error Units IapGeneratorBenchmarks.switchPerf thrpt 200 207393763.888 ± 108142.191 ops/s IapGeneratorBenchmarks.forPerf1 thrpt 200 179691926.763 ± 11248.378 ops/s IapGeneratorBenchmarks.forPerf2 thrpt 200 187926493.328 ± 123181.766 ops/s
如我们所见,switch
结构似乎比两个for
循环结构要好一些。
在实际的实时应用程序中,性能可能会有所不同。也许已编译的" switch"代码比已编译的" for"循环代码长。这可能导致switch
代码将其他代码从指令高速缓存中推出,从而使其他代码的运行速度变慢。是的,这有点投机。我只是想指出一点,它们是微基准测试,基准测试技术的性能在实际应用中可能会有所不同。
案例2基准代码
这是第二种情况的基准代码:
public class IapGeneratorBenchmarks { @State(Scope.Thread) public static class BenchmarkState { byte[] dest = new byte[10 * 1024]; } @Benchmark @BenchmarkMode(Mode.Throughput) public Object switchPerf(BenchmarkState state, Blackhole blackhole) { long value = 123456789; int valueByteLength = 8; //use 8 bytes to represent the long value int destOffset = 0; switch(valueByteLength){ case 8 : { state.dest[destOffset++] = (byte) (255 & (value >> 56));} case 7 : { state.dest[destOffset++] = (byte) (255 & (value >> 48));} case 6 : { state.dest[destOffset++] = (byte) (255 & (value >> 40));} case 5 : { state.dest[destOffset++] = (byte) (255 & (value >> 32));} case 4 : { state.dest[destOffset++] = (byte) (255 & (value >> 24));} case 3 : { state.dest[destOffset++] = (byte) (255 & (value >> 16));} case 2 : { state.dest[destOffset++] = (byte) (255 & (value >> 8));} case 1 : { state.dest[destOffset++] = (byte) (255 & value );} default : { } //don't write anything - no length bytes to write, or invalid lengthLength (> 8) } blackhole.consume(state.dest); return state.dest; } @Benchmark @BenchmarkMode(Mode.Throughput) public Object forPerf1(BenchmarkState state, Blackhole blackhole) { long value = 123456789; int valueByteLength = 8; int destOffset = 0; for(int i=(valueByteLength-1)*8; i >= 0; i-=8){ state.dest[destOffset++] = (byte) (255 & (value >> i)); } blackhole.consume(state.dest); return state.dest; } @Benchmark @BenchmarkMode(Mode.Throughput) public Object forPerf2(BenchmarkState state, Blackhole blackhole) { long value = 123456789; int valueByteLength = 8; int destOffset = 0; for(int i=valueByteLength-1; i >= 0; i--){ state.dest[destOffset++] = (byte) (255 & (value >> (i<<3))); } blackhole.consume(state.dest); return state.dest; } }