【问题标题】:Strange performance drop after innocent changes to a trivial program对琐碎程序进行无辜更改后,奇怪的性能下降
【发布时间】:2013-11-03 09:41:20
【问题描述】:

假设您想计算给定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,因此通常的陷阱不适用。结果相当可重复,末尾的小黑条表示最小和最大时间。

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

【问题讨论】:

  • 仅供参考,Java 微基准测试很难正确:ibm.com/developerworks/java/library/j-jtp12214
  • 我不明白智能算法比简单算法更智能。它只是使用两个循环而不是一个循环,每个循环基本上都做同样的事情:将每个字符与 256 进行比较。因此算法更复杂,因此可优化性较差。
  • 您的做法是正确的:您使用好的工具来衡量每个算法的性能。因此,只需选择最快的一个。试图猜测 JIT 如何优化代码(至少对我而言,但我猜对几乎所有人而言)是一项不可能完成的任务。
  • 顺便说一句,ASCII 以 127 结尾,而不是 255。在 unicode 中,128-255 范围是 Latin-1 补充块。
  • @maaartinus,您可能是对的,但是当您分析程序的性能时,尤其是当结果与您的预期不符时,它从来没有好假设的想法。检查一切,不要假设。即使您这次是对的,这种心态在将来排除故障时也会为您提供良好的服务。

标签: java performance


【解决方案1】:

知道了。

不同之处在于优化器/CPU 可以预测for 中的循环数。如果它能够预先预测重复次数,它可以跳过i &lt; string.length 的实际检查。因此优化器需要预先知道for循环中的条件多久成功一次,因此它必须知道string.lengthi的值。

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

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

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

【讨论】:

  • “局部变量,在设置方法中设置一次”你的意思是一个字段,对吧?我扩展了the benchmark 并得到some results 表明你可能是对的。同意吗?
  • Field:是的,我就是这个意思。你也有timeSmarter2的源代码吗?
  • 当然,请参阅我上面评论中的first link。我将它(以及string)缓存在一个局部变量中;这是 JVM 应该自己做的事情!
  • 好的,找到了。看起来像是执行我的更改,同意。真的很有趣,如何优化循环(尤其是条件)。
【解决方案2】:

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

这个循环:

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) 处理器肯定会很快了解到通常不会采用 break 分支,(b) 负载仍然是可预测的,因此就像可预取一样, (c) 在该循环退出后,代码进入一个与快速循环相同的循环。所以我不希望这会产生很大的不同。

【讨论】:

  • 这很好......除了第二个循环在几个字符后终止,因此应该不起作用。
  • 嗯,是的。您是否通过实验验证了跳过循环真的只运行几个字符?
  • 是的,我有。您是否认为string[i] &gt;= 128 ? 1 : 0 可以(但不必)使用分支进行翻译?
  • 即使它被编译为一个分支,我也猜想(这都是猜测,我害怕!)这将是一个不会对处理器造成太大问题的分支:它将是一个前向分支,距离两个臂的重新连接有很短的距离,因此处理器应该能够向下推测两侧。
  • 我们无需猜测,我很好奇investigated it here。 JVM 经常无法发挥其最佳效果。
猜你喜欢
  • 1970-01-01
  • 2014-03-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-01-08
  • 2014-09-12
  • 2015-01-04
  • 2015-12-08
相关资源
最近更新 更多