【问题标题】:Is there a performance difference between cascading if-else statements and nested if statements级联 if-else 语句和嵌套 if 语句之间是否存在性能差异
【发布时间】:2021-02-23 16:28:27
【问题描述】:

级联 if-else 语句之间是否存在性能差异,例如

if (i > c20) {
// ...
} else if (i > c19) {
// ...
} else if (i > c18) {
// ...
} else if (i > c17) {
// ...
} else if (i > c16) {
// ...
} else if (i > c15) {
// ...
} else if (i > c14) {
// ...
} else if (i > c13) {
// ...
} else if (i > c12) {
// ...
} else if (i > c11) {
// ...
} else if (i > c10) {
// ...
} else if (i > c9) {
// ...
} else if (i > c8) {
// ...
} else if (i > c7) {
// ...
} else if (i > c6) {
// ...
} else if (i > c5) {
// ...
} else if (i > c4) {
// ...
} else if (i > c3) {
// ...
} else if (i > c2) {
// ...
} else if (i > c1) {
// ...
} else if (i > c0) {
// ...
} else {
// ...
}

和嵌套的 if 语句,如:

if (i > c10) {
    if (i > c15) {
        if (i > c18) {
            if (i > c19) {
                if (i > c20) {
                    // ...
                } else {
                    // ...
                }
            } else {
                //...
            }
        } else {
            if (i > c17) {
                // ...
            } else {
                // ...
            }
        }
    } else {
        if (i > c13) {
            if (i > c14) {
                // ...
            } else {
                // ...
            }
        } else {
            if (i > c12) {
                // ...
            } else {
                // ...
            }
        }
    }
} else {
    if (i > c5) {
        if (i > c8) {
            if (i > c9) {
                //...
            } else {
                //...
            }
        } else {
            if (i > c7) {
                // ...
            } else {
                // ...
            }
        }
    } else {
        if (i > c3) {
            if (i > c4) {
                // ...
            } else {
                // ...
            }
        } else {
            if (i > c2) {
                // ...
            } else {
                if (i > c0) {
                    if (i > c1) {
                        // ...
                    }
                } else {
                    // ...
                }
            }
        }
    }
}

如果存在差异,一个比另一个快的原因是什么?一种形式能否带来:更好的 JIT 编译、更好的缓存策略、更好的分支预测、更好的编译器优化等?我对Java 中的性能特别感兴趣,但我想知道它在 C/C++、C# 等其他语言中可能与谁相似或不同。

i 的不同分布、检查的范围和/或不同数量的if 语句将如何影响结果?


这里的值c0c20 是严格递增的顺序,因此引起了愤怒。例如:

c0 = 0;
c1 = 10;
c2 = 20;
c3 = 30;
c4 = 40;
c5 = 50;
c6 = 60;
c7 = 70;
c8 = 80;
c9 = 90;
c10 = 100;
c11 = 110;
c12 = 120;
c13 = 130;
c14 = 140;
c15 = 150;
c16 = 160;
c17 = 170;
c18 = 180;
c19 = 190;
c20 = 200;

c0 = 0;
c1 = 1;
c2 = 2;
c3 = 3;
c4 = 4;
c5 = 5;
c6 = 6;
c7 = 7;
c8 = 8;
c9 = 9;
c10 = 10;
c11 = 11;
c12 = 12;
c13 = 13;
c14 = 14;
c15 = 15;
c16 = 16;
c17 = 17;
c18 = 18;
c19 = 19;
c20 = 20;

【问题讨论】:

  • 嗯,第一个是线性时间O(n) 检查(您运行 if 语句直到 nth 结果)。第二个实际上更类似于O(log n) 算法,因为您实际上是在拆分可能值的范围以在每个 if 分支上检查,因此意味着第二个会更快。总而言之,数组索引或哈希图仍然会超过这两种解决方案(接近 O(1)),并且在此过程中的写入时间要短得多
  • 分支预测、缓存、推测执行等使得在这里预测任何东西基本上是不可能和不合理的。
  • 我认为你的例子是错误的:if(i>0) 为假使所有其余的i>1..n 条件也为假。如果它是真的,那么 else 条件根本不会被检查。所以你的第一个例子完全等同于if(i>0) ...;,没有其他条件,因为它们都是第一个条件的子集(只有当它为真时才能为真)。
  • @PeterCordes 感谢您指出错误。我更正了。

标签: java if-statement control-flow branch-prediction


【解决方案1】:

首先,我们来分析一下你的第一个代码sn-p

if (i > 0) {
// ...
} else if (i > 1) {
// ...
} else if (i > 2) {
//...

这些else if 条件都没有意义,因为,

如果i不大于0,则等于或小于0 因此您的else if 条件应该具有这些值(即 等于或小于0)。

现在,让我们分析一下你的第二个代码 sn-p:

if (i > 10) {
    if (i > 15) {
        if (i > 18) {
           //...

如您所见,如果i 大于10,则第二个if 条件 肯定会被评估,如果第二个是真的,第三个 肯定会被评估的。

因此,您是在比较苹果和橙子。

如果分支相同,则性能应该没有任何差异。

【讨论】:

  • 第一个示例中存在错误,现已更正。
【解决方案2】:

有两个不同的因素对性能起着重要作用。

算法

暂时忘记 CPU 工作方式的变幻莫测。想象一下这段代码:

for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
        for (int k = 0; k < n; k++) {
            doSomething();
        }
    }
}

doSomething 的性能可以而且可能会有很大的不同,即使它是一个非常简单的工作(嘿,热点编译是一件事!),但即便如此,如果 n 是 10,这将运行 doSomething a千倍。如果 n 为 11,则 doSomething 运行 1331 次。如果我们在 x 轴和 y 轴上用n 制作一个图表'doSomething 被调用的频率',它看起来像y = x^3

这是一个增长非常快的图表。即使doSomething 速度很快,或者性能非常不规则,对于足够大的n 值,性能也会很糟糕,因为我们在这里执行了大量的循环。

关键词是一些足够大的价值。这意味着最终此代码 (O(n^3)) 的核心性能特征将是唯一相关的因素。

但是一个 CPU 可以在瞬间循环 1331 次,如果 n 很小,那么与 doSomething 相比,它的 3 个循环深这一事实将显得苍白无力。 'n' 什么时候变得如此之大,你所做的大量循环确保无论在 doSomething 中发生什么事情都会变慢?这取决于你的 CPU。但它最终会发生。

让我换种说法:如果我们制作一个不同的图表,其中 x 轴 = n 和 y 轴 = 专用 CPU 花费的毫秒数来完成计算,那么聊天最初看起来会一团糟,作为你的 winamp 或其他什么的随机中断会导致奇怪的峰值,并且缓存未命中会在其他地方导致另一个峰值,但是当图表向右移动时,如果你向右走得足够远,它就会开始变成y= x^3

CPU 也不是可以发明更高效算法的魔法机器。如果您编写具有O(n^3) 特征的算法,编译器、热点和 CPU 流水线极不可能只是让“性能恐怖的长尾”消失,除非在微不足道的情况下(doSomething 字面上什么都不做,热点知道它,因此通过删除整个调用和整个循环结构来优化)。

本地

但是当我们不看尾部而看开头时会发生什么?那么这是任何人的猜测。 CPU 具有流水线,这使得预测变得更加困难。您可以说:“这条指令需要 8 个 CPU 周期,它是一个 4GHz CPU,因此运行它应该花费 20 亿分之一秒”的日子已经早已一去不复返了 - CPU 的天堂几十年来一直没有这样工作:他们在运行另一个操作码的同时解析一个操作码,并且涉及多个内核。

此外,这些天来,CPU 访问主内存需要 数百个周期,实际上它们甚至不能再这样做了:它们只能在缓存页面上操作,如果当您尝试读取或写入内存时,指令将被翻译到缓存中的正确条目。如果页面没有被缓存,CPU 将冻结,而 CPU 周围的基础设施会找到一个要清除的页面,并将整个页面从主内存加载到 CPU 缓存中(它具有使事情复杂化的层次结构),并且 CPU 正在等待很长时间。

鉴于在完全不同的代码甚至不同的线程中完成的操作可能会导致您的缓存页面被刷新,显然完全不可能猜测某件事需要多少纳秒。 p>

这也意味着,如果您占用的内存超出所需,您的代码可能不会运行得更慢,但因为它会导致另一个页面从缓存中被逐出,所以其他地方的代码现在会这样做。如果尝试对这段代码进行计时似乎永远不会导致执行速度变慢,那么将性能下降“归咎于”这段代码是否公平?

复杂。很复杂。

您可以使用 JMH 对代码进行基准测试。它试图通过非常频繁地运行代码来消除随机噪声,并且足够长的时间让热点启动(热点会注意到一个方法运行了很多次,并将对其进行分析并将其重写为高度优化的机器代码。因为通常虚拟机 99% 的时间花费在不到 1% 的加载方法上,分析和重写是一个非常昂贵的过程,java 并不是对所有代码都这样做,只对运行很多的代码,所以你有等待它开始进行真正的测量)。

您的代码

鉴于我们在示例中讨论了大约 20 个 if 语句,您永远不会注意到。我们处于该性能曲线的“左侧”,算法性能无关紧要,这完全与缓存未命中、CPU 管道的变幻莫测等有关。通常,您可以考虑少于 1000 条指令的任何代码并且这会导致没有缓存未命中是瞬时的。 CPU 非常快,瓶颈在于它们必须等待硬件中的其他设备赶上来,以及那些可怕的缓存未命中。

然而,作为一个原则?第一个示例需要执行 n 个检查,而第二个示例只执行 log(n) 个检查。 log(n) 通常被称为“和即时一样好”,但因为即使 n 是一百万,log(n) 也只有 20。

换句话说,除非你有数以千计的if 语句,否则这些东西都不重要,然后它可能,也许,可能会引起注意,但前提是你经常运行该方法。

更一般地说,您甚至不需要 log(n) 性能,您可以通过直接跳转到正确的位置来使用 log(1)。这正是switch 语句所做的。

因此,总结:

  1. 不,没关系。您永远不会知道,并且可能会遇到每个方法的最大 65536 字节码的硬限制,而不是您会看到性能差异。
  2. 如果您想要理论上最快的方法来创建这样的决策树,请使用 switch 语句。
  3. 由于 CPU 和热点如此复杂,因此完全不可能通过猜测如此小的代码更改来“让您的程序运行得更快”。这不是它的工作原理:编写干净、灵活的代码(尽可能使其具有可读性)。然后,一旦你的应用程序完成,运行一个分析器,它会告诉你哪 1% 的代码库占用了 99% 的资源以及它是如何做到的,如果你觉得你的应用程序太慢,使用它来改进你的代码。如果您编写干净的代码,那将很容易。如果您徒劳地编写丑陋的代码以使其更快,那将很困难,除非您有无限的开发时间,否则您最终会遇到更慢的混乱。所以永远不要过早地优化。
  4. 如果您知道算法复杂性不好(O(x^2) 或更糟,通常),并且您希望最终得到足够大的输入,这将是重要的。然后,是的,过早地优化和发明更好的算法。如果您试图通过一百万个不同的选项来进行决策树,请务必使用哈希图,并且不要循环遍历一百万个大列表。

【讨论】:

  • 他们在运行另一个操作码的同时解析一个操作码 - 每个内核每个周期解码 4 或 5 个操作码。它们不仅是流水线的,而且是超标量的。 :Plighterra.com/papers/modernmicroprocessors。但是,是的,关键是您不能为单独的指令添加一个“成本”数字来找到整体性能。 The 3 main bottlenecks 是特定端口的前端、后端延迟和后端吞吐量,适用于不受内存限制或因分支错误预测而停滞的代码。
  • 换句话说,这些东西都不重要 - 我几乎不会这么说。这个线性序列与 log n 分支代码可以在最内层循环内,例如在运行字节码的解释器中。尽管在这种情况下(密集情况),通常您希望通过switch(这可能会鼓励编译器制作跳转表)或手动使用函数指针表进行 O(1) 调度。 (或者任何 Java 等价物,我忘记了。)
  • ...它可能是,然后它仍然无关紧要。除非确实如此。您可以声称“嘿,它可能是关键路径!”关于任何代码行,你是对的。但如果这就是你为了“性能”而仔细审查和调整你写的每一行的原因,那么你做的非常非常错误,这就是为什么我非常犹豫是否在没有大量警告的情况下把它放在那里。如果需要这种级别的性能压缩,那么 OP 应该运行一个分析器,找出要关注的方法。如果出现这个,无论如何。那么这很重要。
  • OP 询问了两种不同策略的性能。在每个性能答案中包含通常并不重要的性能的长篇大论是没有意义的。 (快速警告很好)。了解权衡可能会很有趣;有时它对可读性是中性的,有时它很重要。当然,分支预测是棘手的,在线性与一些常见情况优先与二进制搜索之间的权衡很可能取决于周围的代码,而不仅仅是上下文。因此,除了线性链明显较差的平均/最坏情况性能之外,这里很难说它有什么用处。
【解决方案3】:

对于级联 if-else,所有表达式按照它们出现的顺序从上到下进行求值,直到求值为 true 的 if-else 条件。

性能取决于在获得true 之前要评估的表达式数量。

以你的情况为例:

if (i > 0) {
// ...
} else if (i > 1) { 
// ...
} else if (i > 2) {
// ...
} else if (i > 3) {
// ...
} else if (i > 4) {

对于任何整数输入i,如果它是value is less than or equal to 0,则必须执行整个块,而i greater than 0 将在评估第一个if 本身后退出。

对于下一个示例,如果使用短路运算符编写,则等效且更具可读性。

if (i > 10) {
    if (i > 15) {
        if (i > 18) {
            if (i > 19) {
                if (i > 20) {

使用&amp;&amp; 写成这样更好:

 if (i>10 && i>15 && i>18 && i>19 && i>20){ // note i >10 && i< upperbound instead
 }

Collapsible if statements should be merged

在此处i &gt; 10,将产生true,并将导致对右侧其余&amp;&amp;条件的评估。

因此,对于导致第一个表达式本身评估为 false 的输入,即当输入 i 是满足条件 i &lt;= 10 的整数时,这会更好地执行。

与其他答案一样,建议尝试对代码进行基准测试,例如使用 jmh,这将更好地了解优化后代码的执行情况。

【讨论】:

  • 是的,OP 的示例已损坏(仅需要 i&gt;0 检查,如果为真则跳过其他检查,如果为假则跳过其他检查)。但是那些 ifs 不能 折叠成 &amp;&amp;:每个都有一个单独的 else 块。如果您以这种方式执行外部 if,则必须重复条件(以否定形式),并希望编译器能够 CSE 并重新考虑嵌套分支。
  • @PeterCordes 我纠正了第一个示例中的错误。
  • @SumindaSirinathS.Dharmasena 性能取决于在真实结果满足 if 块顺序之前进行的 if-else 表达式评估的数量。因此,由于这个原因,如果输入中的大部分 i 满足条件 i > c20(cascading-if-else 中的第一个 if),则 cascading-if-else 的性能会更好。 if inside if 需要更多的评估来进行比较。
猜你喜欢
  • 2013-09-01
  • 2019-09-21
  • 2017-08-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多