问题描述
级联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
语句将如何影响结果?
此处的值c0
至c20
严格按顺序增加,因此产生了愤怒。例如:
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;
解决方法
首先,让我们分析您的第一个代码段
if (i > 0) {
// ...
} else if (i > 1) {
// ...
} else if (i > 2) {
//...
这些else if
条件都没有道理,
如果
i
不大于0
,则等于或小于0
因此您的else if
条件应具有这些值(即 等于或小于0
)。
现在,让我们分析第二个代码段:
if (i > 10) {
if (i > 15) {
if (i > 18) {
//...
您会看到
i
是否大于10
,这是第二个if
条件 肯定会被评估,如果第二个是正确的,第三个 一个肯定会得到评估。
因此,您正在比较苹果和橙子。
如果分支相同,则性能不应有任何差异。
,有两个不同的因素在性能中起主要作用。
算法
暂时忘记有关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
一千次。如果n
为11,则doSomething
被运行1331次。如果我们在x轴和y轴上用n
制作图表,则“多久调用一次doSomething”,它的外观就像y = x^3
。
这是一个非常快增长的图表。即使doSomething
的速度很快或具有非常不规则的性能,但对于n
的某个足够大的值,仅由于我们在此处执行大量循环操作,性能还是很糟糕的。
关键字是足够大的值。这意味着最终该代码(O(n^3)
)的核心性能特征将是唯一相关的因素。
但是CPU可以立即循环执行1331次,与doSomething
较小时n
所做的事实相比,它深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正在等待年龄。
鉴于用完全不同的代码甚至可能是不同的线程执行的操作可能导致刷新缓存页面,显然完全不可能来尝试猜测花费了多少纳秒。
>这还意味着,如果占用的内存超过了所需的内存,则代码的运行速度可能不会变慢,但是由于这会导致另一页从缓存中退出,因此其他地方的代码也会运行。如果试图计时该代码似乎永远不会导致执行速度降低,那么将其归咎于性能下降是否公平?
复杂。非常复杂。
您可以使用JMH对代码进行基准测试。它试图通过非常频繁地运行代码来掩盖随机噪声,并且持续足够长的时间使热点开始运行(热点会注意到方法运行了很多,并将对其进行分析并将其重写为高度优化的机器代码。因为通常VM花费了99%的时间在不到1%的已加载方法中,并且分析和重写是一个非常昂贵的过程,java不会为所有代码执行此操作,而是仅针对运行次数很多的代码执行此操作,因此等待它开始进行真正的测量。
您的代码
鉴于我们在示例中谈论的是大约20条if语句,所以您永远不会注意到。我们处于该性能曲线的“左侧”,这在算法性能上是无关紧要的,而这全都与高速缓存未命中,CPU管道的多变等有关。通常,您可以考虑少于1000条指令的任何代码并且不会导致任何高速缓存未中。 CPU速度非常快,瓶颈在于它们必须等待硬件中的其他设备赶上来,以及等待那些可怕的高速缓存未命中。
尽管如此,原则上呢?第一个示例需要执行n次检查,而第二个示例仅执行log(n)检查。 log(n)通常被称为“与即时性一样好”,但是因为即使n是一百万,log(n)也只能是20。
换句话说,除非您实际上拥有成千上万个if
语句,否则所有这些东西都无关紧要,然后,也许也许引人注目,但前提是您必须大量运行该方法。 / p>
更一般地说,您甚至不需要log(n)性能,可以直接跳到正确的位置来使用log(1)。这正是switch
语句的作用。
因此,得出以下结论:
- 不,没关系。您永远不会知道,而且可能会遇到每个方法限制的65536个最大字节码的硬性,这比您将看到的性能差异要强。
- 如果您想要理论上最快的方法来做这样的决策树,请使用
switch
语句。 - 由于CPU和热点是如此复杂,因此完全不可能通过猜测如此小的fry代码更改来“使程序运行得更快”。那不是它的工作方式:编写干净,灵活的代码(使代码尽可能地可读)。然后,一旦您的应用程序完成,请运行一个探查器,该探查器将告诉您代码库中的哪1%占用了99%的资源,以及如何做到这一点,如果您觉得自己的应用程序速度太慢,则可以使用它来使代码更好。如果您编写干净的代码,这将很容易。如果您徒劳地尝试编写丑陋的代码以使其更快,那将是很难的,除非您有无限的开发时间,否则最终将导致混乱。所以永远不要过早优化。
- 如果您知道算法的复杂性很差(通常是
O(x^2)
或更糟),那一切就在窗外。您希望最终会得到足够大的输入,而这将很重要。然后,过早地优化和发明更好的算法。如果您尝试通过一百万个不同的选项来决策树,请绝对使用哈希图,并且不要遍历一百万个清单。
对于级联的if-else,所有表达式均按从上到下的显示顺序从高到低进行计算,直到if-else条件的值为true
。
性能取决于获取条件的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) {
最好将&&
编写为:
if (i>10 && i>15 && i>18 && i>19 && i>20){ // note i >10 && i< upperbound instead
}
Collapsible if statements should be merged
这里i > 10
,将得出true
,并将对右边的其他&&
个条件进行评估。
因此,对于将第一个表达式本身评估为false的输入(即输入i
是满足条件i <= 10
的整数),这样做会更好。
像其他答案一样,建议尝试对代码进行基准测试,例如使用jmh,这样可以更好地了解优化后代码的性能。