【问题标题】:Performance impact of short-circuit evaluation短路评估的性能影响
【发布时间】:2016-10-19 20:37:06
【问题描述】:

免责声明:我在逆向工程字节码方面没有太多经验,所以如果可以“轻松”回答我的问题,请不要对我太苛刻。

在现代处理器上,如果预测失败,分支可能会非常昂贵(请参阅Why is it faster to process a sorted array than an unsorted array?)。

假设我在 Java 中有一些类似这样的短路评估:

if (condition && (list!=null) && (list.size()>0)) /* Do something */ ;

那基本上就相当于一堆这样的分支吗:

if (condition) {
    if (list!=null) {
        if (list.size()>0) {
            // Do something
        }
    }
}

还是 Java 有其他方法可以更巧妙地进行短路?

换句话说,是否最好通过重写以下行来避免至少一个分支:

if ((condition & (list!=null)) && (list.size()>0)) /* Do something */ ;

因为简单的list!=null-check 比可能预测错误的分支要便宜得多?

(很明显,如果不冒NullPointerException 的风险,我无法摆脱第二个&&。)

现在,在我被诸如“过早的优化是万恶之源!”之类的陈述撕成碎片之前,请记住这是在一般编码习惯之间的选择(始终使用短路与从不使用短路) -电路,除非需要),这将影响我的代码几乎所有,所以确保我在这里使用正确的习惯绝对值得花一些时间思考。

【问题讨论】:

  • 1) 过早优化万恶之源!" 2) 使用&& 是一个非常好的习惯。
  • 1) 是的,我确实知道 Java 是如何实现短路的,并且 2) 我真诚地相信,在一种尽一切可能抽象的语言中,担心极低级别、高度特定于 CPU 的行为远离底层平台是......成为慈善......“被误导”。无意义。浪费时间。几乎可以肯定会因情况而异。如果有疑问,我鼓励您尝试一些基准测试并发布结果:)
  • 您现在已经花了多少时间研究和讨论这个问题,而这很可能对您的代码没有明显的影响?也许可以衡量,但很明显? 过早优化的概念是把你非常宝贵的时间花在重要的事情上,所以把时间浪费在很可能不会重要的事情上,你就会有那么多在截止日期之前花更少的时间在真正重要的代码上。当然,如果您有空闲时间,您可以尝试优化,但您如何知道您的尝试有助于而不是损害性能??
  • 这真的取决于分支是否可能被正确预测,这与合成基准完全不同。您可以尝试衡量一个至少稍微现实的版本,在该版本中,您将两个成本相同的操作放在 && 上,其中一个为假,另一个为变量。交换一下,看看你是否得到了可衡量的效果。但同样,在 Java 中摆弄这个很可能是徒劳的。
  • 字节码不会将 1-1 映射到 CPU 指令。相同的字节码可以解释运行,并以不同程度的优化进行 JIT,即在其生命周期内运行不同的 CPU 指令。因此,所有“你不能轻易地从 Java 代码中推断出这一点”的谈话。

标签: java performance short-circuiting


【解决方案1】:

here 没有提到任何类型的分支。这只是一个表达式求值,if 语句中的&& 与表达式中的&& 没有区别。你会用下面的代码问同样的问题吗?

boolean isValid = condition && (list!=null) && (list.size()>0);
if (isValid) {
    ...
}

基本上就是这样,表达式被求值,然后分支发生。

【讨论】:

  • 没错,这里没有提到。但是跳过短路条件必须以某种方式实现,除了分支(至少在一般情况下)之外,我真的想不出一种方法来做到这一点。所以,是的,我会用你添加的代码问同样的问题。在那里,分支将发生在boolean isValid = ...-line。
  • 公平点。但是,我有点同意其他人的观点,你不能真的假设分支预测会很糟糕,而它应该是一种优化。仅仅因为它在特定示例上使事情变得更糟并不意味着您应该担心它。 过早优化的定义是在你知道你需要优化之前尝试优化。这不是一个好的过程。一般来说,你会发现你的代码运行得足够快,如果不是这样,你会寻找瓶颈。我敢打赌,在 99% 的情况下,您的瓶颈与分支无关
  • 真的“应该是优化”吗?或者它只是另一种语义工具,比如三元运算,基本上只是为你节省了一些打字时间?如果它只是一个语义工具,我认为尝试理解它代表什么以及何时以及如何有效地使用它是非常有意义的。
  • 我进行了大量的移动开发,其中每个时钟周期不仅对于您的应用程序在慢速设备上快速运行而言是宝贵的,而且在电池寿命消耗方面也是如此,即使在可用的最快设备上也是如此。所以,“总的来说,你会发现你的代码运行得足够快”不是我愿意接受的说法。没有代码足够快。例如,我敢肯定是有这种过早优化恐惧症的人写了 InkScape。如果 Corel Draw 可以在 486 上实时执行完全相同的操作,我会很尴尬地发布在 Core-i7 上运行如此缓慢的软件!
  • 那么你可能应该用二进制编码,我什至不是在开玩笑。在诸如 Java 之类的高级语言中,您无法控制很多事情,以至于我无法接受您关于“每个时钟周期都很重要”的论点。如果这是真的,您会担心 JIT 编译、垃圾收集以及所有这些超出您控制范围的事情,并且您最终会使用另一种语言。在没有看到您的任何代码的情况下,我真的认为与其他优化可能性相比,减少分支数量(无论是否虚构)根本没有影响。
【解决方案2】:

我做了一个简单的测试

int sum = 0;
Random rnd = new Random(1);

int[] a = new int[1000];
int[] b = new int[1000];
for (int i = 0; i < 1000; ++i)
{
    a[i] = rnd.nextInt(100);
    b[i] = rnd.nextInt(100);
}

long started = System.nanoTime();

for (int i = 0; i < 1000000; ++i)
{
    for (int j = 0; j < 1000; ++j)
    {
        if (a[j] < 50 && b[j] < 5)// change "&& b[j] < 5"
        {
            sum++;
        }
    }
}

long ended =  System.nanoTime();
System.out.println((ended - started)/1000000 + "  " + sum);

结果非常随机:

            &&      &
b[j] < 5    1450    1360
b[j] < 50   1330    1610
b[j] < 500  1310    1450
j < 50      2200    920
j < 500     1410    1730
j < 5000    1180    1040
j < i       1180    2050
i < j       2290    1450

这些是多次运行的最低值,我确保它们是可重复的。实际时间因运行而异。根据经验,我会避免过于花哨,坚持 && 并希望“那里”的优化做得最好。有关于optimizations的精彩视频

编辑

正如 Dici 指出的,应该进行 JVM 预热。函数的第一次和第二次调用似乎与其他调用不同。即使我增加循环次数,该规则也始终适用。所以我重新测试了,然后……又弄得一团糟。平均快 2 倍左右,但又一团糟。而且优化更加不稳定,通常不是一个,而是两个典型的时间值,甚至有 50% 的差异。我看了 JMH,不错的框架。如果我真的尝试过(在不同的系统,不同的硬件等等,很多工作),我可能会测量 & 是否比 && 更快。但这不是问题。问题是,如果我在我的程序中替换 && 为 & 我可以期望它更快或更慢吗?答案是 - 你不能期待任何东西,你必须衡量它。

EDIT2

在这种情况下,我认为这是浪费时间,但为了维护我的可信度,我测量了标准偏差(又名 1 sigma)。值在几次运行中表现良好,并且在数千次运行中表现良好,这并不奇怪(在 JVM 预热后我没有显示结果,因为它们表现不佳并且统计数据将是不可避免的)。有趣的是,所有结果都比我之前测量的结果快 7%,这对于大多数值来说都超过了 5 sigma。从几次尝试看来,网络浏览器选项卡会影响整个系统的速度,不,我不会统计数据来证实我的观察。

            &&          &
b[j] < 5    1333(14)    1265(25)
b[j] < 50   1231(11)    1514(13)
b[j] < 500  1223(9)     1360(13)
j < 50      2069(74)    842(11)
j < 500     1294(12)    1631(17)
j < 5000    1089(9)     957(8)
j < i       1086(8)     1907(23)
i < j       2164(16)    1357(14)

【讨论】:

  • 哇。真是一团糟......结果当然不是你的代码:)......所以,在大多数情况下,它似乎是无关紧要的,但有时它很重要,它实际上可以以任何一种方式去影响它的重要性...但是肯定有其他事情发生,因为每列中的某些数字相对于彼此没有任何意义。例如,j&lt;50 怎么会在 5% 的时间里非常可预测且快速地返回 falsej 应该立即可用)比 b[j]&lt;5 慢得多,false 5% 的时间,但不可预测且需要数组索引?!?
  • @MarkusA。我同意,这没有意义。我尝试用更多的随机性修改它,并在 C# 和 C++ 中进行了测试。结果 - 它仍然没有意义。 C++ 似乎最接近人们的期望。我必须在这里重复视频中的想法 - 判断更快的唯一方法是测量它。
  • 难怪结果是随机的:这个基准确实依赖于随机性。真正的基准测试应该 1) 预热 JVM,2) 确保输入始终相同,例如通过为 Random 实例选择特定种子, 3) 多次运行每个测试以获得结果的统计准确性(方差、置信区间等)。如果您想做一个真正的基准测试,我建议您使用基准框架,而不是手动执行并获得毫无意义的结果
  • @Dici 2) 我做了什么。 3)我做了什么。应用统计数据有点棘手,例如间隔是非常无用的,由于 GC 或任何完全破坏结果的原因而关闭了一个值。 1)我没有这样做,我以为重复运行多次就足够了,貌似我错了。我会尝试修复它。
  • 咳咳,统计分析并不是因为 GC 等随机效应而毫无意义,因为这些原因,它是相关的。您重复相同的实验越多,您消除测量周围的噪音就越多,并且能够获得显着的结果。我们不知道您的衡量标准有多好,因为我们没有置信区间。置信区间可能很大,这会使基准变得不显着。另外,我不知道我怎么错过了new Random(1)。你做得很好,我的错:)
猜你喜欢
  • 1970-01-01
  • 2015-11-14
  • 2017-01-21
  • 2012-02-10
  • 2010-12-21
  • 1970-01-01
  • 2014-06-08
  • 2019-07-19
  • 2013-05-21
相关资源
最近更新 更多