Java for vs.switch性能

时间:2020-01-09 10:36:17  来源:igfitidea点击:

对于某些类型的操作,我们可以将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;
    }
}