想象一下,您想要计算给定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并获得此信息 参数确定ASCII与非ASCII比率以及非ASCII序列的平均长度,但正如您所看到的那样无关紧要。尝试不同的种子和任何无关紧要的东西。 benchmark使用caliper,因此通常的问题不适用。结果是相当可重复的,最后的小黑条表示最小和最大时间。
有人知道这里发生了什么吗?任何人都可以重现它吗?
答案 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.length
和i
的值。
我做了一个简单的测试,将string.length
替换为在setup
方法中设置一次的局部变量。结果:smarterCount
的运行时间约为simpleCount
。在更改之前smarterCount
比simpleCount
长约50%。 smartCount
没有改变。
看起来优化器会丢失在调用另一个方法时必须执行多少循环的信息。这就是为什么finish()
立即使用常量集更快地运行的原因,而不是smartCount()
,因为smartCount()
不知道i
之后skip()
将会是什么步。所以我做了第二次测试,在那里我将循环从skip()
复制到smartCount()
。
瞧,这三种方法都在同一时间内返回(800-900毫秒)。