在无辜改变一个简单的程序后奇怪的性能下降

时间:2013-11-03 09:41:20

标签: java performance

想象一下,您想要计算给定char[]包含的非ASCII字符数。想象一下,表演真的很重要,所以我们可以跳过我们最喜欢的slogan

最简单的方法显然是

int simpleCount() {
    int result = 0;
    for (int i = 0; i < string.length; i++) {
        result += string[i] >= 128 ? 1 : 0;
    }
    return result;
}

然后您认为许多输入都是纯ASCII,并且单独处理它们可能是个好主意。为简单起见,假设你只是写这个

private int skip(int i) {
    for (; i < string.length; i++) {
        if (string[i] >= 128) break;
    }
    return i;
}

这种简单的方法可以用于更复杂的处理,在这里它不会造成任何伤害,对吧?所以让我们继续

int smartCount() {
    int result = 0;
    for (int i = skip(0); i < string.length; i++) {
        result += string[i] >= 128 ? 1 : 0;
    }
    return result;
}

simpleCount相同。我称之为“智能”,因为要完成的实际工作更复杂,因此快速跳过ASCII是有道理的。如果没有或非常短的ASCII前缀,它可能会花费几个周期,但这就是全部,对吗?

也许你想像这样重写它,它是一样的,可能更可重用,对吗?

int smarterCount() {
    return finish(skip(0));
}

int finish(int i) {
    int result = 0;
    for (; i < string.length; i++) {
        result += string[i] >= 128 ? 1 : 0;
    }
    return result;
}

然后你在一些非常长的随机字符串上运行benchmark并获得此信息 img 参数确定ASCII与非ASCII比率以及非ASCII序列的平均长度,但正如您所看到的那样无关紧要。尝试不同的种子和任何无关紧要的东西。 benchmark使用caliper,因此通常的问题不适用。结果是相当可重复的,最后的小黑条表示最小和最大时间。

有人知道这里发生了什么吗?任何人都可以重现它吗?

2 个答案:

答案 0 :(得分:3)

我的初步猜测是这是关于分支预测。

这个循环:

for (int i = 0; i < string.length; i++) {
    result += string[i] >= 128 ? 1 : 0;
}

只包含一个分支,即循环的后沿,并且具有高度可预测性。现代处理器将能够准确地预测这一点,因此用指令填充整个管道。负载序列也是高度可预测的,因此它将能够预取流水线指令所需的所有内容。高性能结果。

这个循环:

for (; i < string.length - 1; i++) {
    if (string[i] >= 128) break;
}

有一个脏的数据相关的条件分支位于其中间。这对处理器来说要难以准确预测。

现在,这并不完全有意义,因为(a)处理器肯定会很快知道通常不会采用中断分支,(b)负载仍然是可预测的,因此就像预先设定的那样, (c)在该循环退出之后,代码进入一个循环,该循环与快速循环相同。所以我不希望这会产生那么大的不同。

答案 1 :(得分:3)

知道了。

区别在于优化器/ CPU可以预测for中的循环次数。如果它能够预先预测重复次数,则可以跳过i < string.length的实际检查。因此,优化器需要预先知道for循环中的条件成功的频率,因此它必须知道string.lengthi的值。

我做了一个简单的测试,将string.length替换为在setup方法中设置一次的局部变量。结果:smarterCount的运行时间约为simpleCount。在更改之前smarterCountsimpleCount长约50%。 smartCount没有改变。

看起来优化器会丢失在调用另一个方法时必须执行多少循环的信息。这就是为什么finish()立即使用常量集更快地运行的原因,而不是smartCount(),因为smartCount()不知道i之后skip()将会是什么步。所以我做了第二次测试,在那里我将循环从skip()复制到smartCount()

瞧,这三种方法都在同一时间内返回(800-900毫秒)。

Different runtimes