具体来说,如果我有一系列if
... else if
语句,并且我事先知道每个语句将评估为true
的相对概率,那么差异有多大执行时间是否按概率顺序对它们进行排序?例如,我应该更喜欢这个:
if (highly_likely)
//do something
else if (somewhat_likely)
//do something
else if (unlikely)
//do something
到这个?:
if (unlikely)
//do something
else if (somewhat_likely)
//do something
else if (highly_likely)
//do something
很明显,排序版本会更快,但是为了便于阅读或存在副作用,我们可能希望以非最佳方式对它们进行排序。在实际运行代码之前,很难判断CPU在分支预测方面的表现如何。
所以,在尝试这个过程中,我最后回答了一个针对特定案例的问题,但我也希望听到其他意见/见解。
重要提示:此问题假定if
语句可以任意重新排序,而不会对程序的行为产生任何其他影响。在我的回答中,三个条件测试是互斥的,不会产生副作用。当然,如果必须按某种顺序评估这些陈述以达到某种预期的行为,那么效率问题就没有实际意义。
答案 0 :(得分:94)
作为一般规则,大多数(如果不是所有)英特尔CPU都假定前向分支不会在他们第一次看到它们时采用。请参阅Godbolt's work。
之后,分支进入分支预测缓存,过去的行为用于通知未来的分支预测。
因此,在紧密的循环中,错误排序的影响将相对较小。分支预测器将学习哪一组分支是最有可能的,如果你在循环中有非常重要的工作量,那么小的差异就不会增加太多。
在一般代码中,默认情况下大多数编译器(缺少其他原因)将按照您在代码中订购的方式大致订购生产的机器代码。因此,如果语句在失败时是前向分支。
因此,您应该按照降低可能性的顺序对您的分支进行排序,以便从“第一次遇到”中获得最佳分支预测。
微基准测试在一系列条件下多次循环,并且微不足道的工作将由指令计数等的微小影响主导,并且几乎没有相对分支预测问题。因此,在这种情况下,必须配置,因为经验法则不可靠。
最重要的是,矢量化和许多其他优化适用于微小的紧密循环。
因此,在一般代码中,将最可能的代码放在if
块中,这将导致最少的未缓存分支预测未命中。在紧密循环中,按照一般规则开始,如果您需要了解更多信息,除了简介之外别无选择。
如果有些测试比其他测试便宜得多,那么这一切都会消失。
答案 1 :(得分:45)
我做了以下测试来计算两个不同if
... else if
块的执行时间,一个按概率顺序排序,另一个按相反顺序排序:
#include <chrono>
#include <iostream>
#include <random>
#include <algorithm>
#include <iterator>
#include <functional>
using namespace std;
int main()
{
long long sortedTime = 0;
long long reverseTime = 0;
for (int n = 0; n != 500; ++n)
{
//Generate a vector of 5000 random integers from 1 to 100
random_device rnd_device;
mt19937 rnd_engine(rnd_device());
uniform_int_distribution<int> rnd_dist(1, 100);
auto gen = std::bind(rnd_dist, rnd_engine);
vector<int> rand_vec(5000);
generate(begin(rand_vec), end(rand_vec), gen);
volatile int nLow, nMid, nHigh;
chrono::time_point<chrono::high_resolution_clock> start, end;
//Sort the conditional statements in order of increasing likelyhood
nLow = nMid = nHigh = 0;
start = chrono::high_resolution_clock::now();
for (int& i : rand_vec) {
if (i >= 95) ++nHigh; //Least likely branch
else if (i < 20) ++nLow;
else if (i >= 20 && i < 95) ++nMid; //Most likely branch
}
end = chrono::high_resolution_clock::now();
reverseTime += chrono::duration_cast<chrono::nanoseconds>(end-start).count();
//Sort the conditional statements in order of decreasing likelyhood
nLow = nMid = nHigh = 0;
start = chrono::high_resolution_clock::now();
for (int& i : rand_vec) {
if (i >= 20 && i < 95) ++nMid; //Most likely branch
else if (i < 20) ++nLow;
else if (i >= 95) ++nHigh; //Least likely branch
}
end = chrono::high_resolution_clock::now();
sortedTime += chrono::duration_cast<chrono::nanoseconds>(end-start).count();
}
cout << "Percentage difference: " << 100 * (double(reverseTime) - double(sortedTime)) / double(sortedTime) << endl << endl;
}
将MSVC2017与/ O2一起使用,结果显示排序版本始终比未排序版本快约28%。根据luk32的评论,我也改变了两个测试的顺序,这显示出明显的差异(22%对28%)。该代码在Intel Xeon E5-2697 v2上的Windows 7下运行。当然,这是非常特定问题的,不应该被解释为一个确凿的答案。
答案 2 :(得分:28)
不,你不应该,除非你确定目标系统受到影响。默认情况下请阅读。
我非常怀疑你的结果。我已经修改了你的例子,所以反向执行更容易。 Ideone相当一致地表明逆序更快,但并不多。在某些运行中,即使偶尔也会翻转。我说结果是不确定的。 coliru报告也没有真正的差异。我可以稍后在我的odroid xu4上查看Exynos5422 CPU。
问题在于现代CPU具有分支预测器。有许多逻辑专门用于预取数据和指令,而现代x86 CPU在这方面相当聪明。像ARM或GPU这样的一些更纤薄的架构可能容易受此影响。但它实际上高度依赖于编译器和目标系统。
我想说分支排序优化是非常脆弱和短暂的。只做一些非常微调的步骤。
代码:
#include <chrono>
#include <iostream>
#include <random>
#include <algorithm>
#include <iterator>
#include <functional>
using namespace std;
int main()
{
//Generate a vector of random integers from 1 to 100
random_device rnd_device;
mt19937 rnd_engine(rnd_device());
uniform_int_distribution<int> rnd_dist(1, 100);
auto gen = std::bind(rnd_dist, rnd_engine);
vector<int> rand_vec(5000);
generate(begin(rand_vec), end(rand_vec), gen);
volatile int nLow, nMid, nHigh;
//Count the number of values in each of three different ranges
//Run the test a few times
for (int n = 0; n != 10; ++n) {
//Run the test again, but now sort the conditional statements in reverse-order of likelyhood
{
nLow = nMid = nHigh = 0;
auto start = chrono::high_resolution_clock::now();
for (int& i : rand_vec) {
if (i >= 95) ++nHigh; //Least likely branch
else if (i < 20) ++nLow;
else if (i >= 20 && i < 95) ++nMid; //Most likely branch
}
auto end = chrono::high_resolution_clock::now();
cout << "Reverse-sorted: \t" << chrono::duration_cast<chrono::nanoseconds>(end-start).count() << "ns" << endl;
}
{
//Sort the conditional statements in order of likelyhood
nLow = nMid = nHigh = 0;
auto start = chrono::high_resolution_clock::now();
for (int& i : rand_vec) {
if (i >= 20 && i < 95) ++nMid; //Most likely branch
else if (i < 20) ++nLow;
else if (i >= 95) ++nHigh; //Least likely branch
}
auto end = chrono::high_resolution_clock::now();
cout << "Sorted:\t\t\t" << chrono::duration_cast<chrono::nanoseconds>(end-start).count() << "ns" << endl;
}
cout << endl;
}
}
答案 3 :(得分:23)
只需5美分。看来if语句的排序效果应该取决于:
每个if语句的概率。
迭代次数,因此分支预测器可以启动。
可能/不太可能的编译器提示,即代码布局。
为了探索这些因素,我对以下功能进行了基准测试:
for (i = 0; i < data_sz * 1024; i++) {
if (data[i] < check_point) // highly likely
s += 3;
else if (data[i] > check_point) // samewhat likely
s += 2;
else if (data[i] == check_point) // very unlikely
s += 1;
}
for (i = 0; i < data_sz * 1024; i++) {
if (data[i] == check_point) // very unlikely
s += 1;
else if (data[i] > check_point) // samewhat likely
s += 2;
else if (data[i] < check_point) // highly likely
s += 3;
}
for (i = 0; i < data_sz * 1024; i++) {
if (likely(data[i] < check_point)) // highly likely
s += 3;
else if (data[i] > check_point) // samewhat likely
s += 2;
else if (unlikely(data[i] == check_point)) // very unlikely
s += 1;
}
for (i = 0; i < data_sz * 1024; i++) {
if (unlikely(data[i] == check_point)) // very unlikely
s += 1;
else if (data[i] > check_point) // samewhat likely
s += 2;
else if (likely(data[i] < check_point)) // highly likely
s += 3;
}
数据数组包含0到100之间的随机数:
const int RANGE_MAX = 100;
uint8_t data[DATA_MAX * 1024];
static void data_init(int data_sz)
{
int i;
srand(0);
for (i = 0; i < data_sz * 1024; i++)
data[i] = rand() % RANGE_MAX;
}
以下结果适用于Intel i5 @ 3,2 GHz和G ++ 6.3.0。第一个参数是check_point(即高可能性if语句的%%概率),第二个参数是data_sz(即迭代次数)。
---------------------------------------------------------------------
Benchmark Time CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4 4660 ns 4658 ns 150948
ordered_ifs/50/8 25636 ns 25635 ns 27852
ordered_ifs/75/4 4326 ns 4325 ns 162613
ordered_ifs/75/8 18242 ns 18242 ns 37931
ordered_ifs/100/4 1673 ns 1673 ns 417073
ordered_ifs/100/8 3381 ns 3381 ns 207612
reversed_ifs/50/4 5342 ns 5341 ns 126800
reversed_ifs/50/8 26050 ns 26050 ns 26894
reversed_ifs/75/4 3616 ns 3616 ns 193130
reversed_ifs/75/8 15697 ns 15696 ns 44618
reversed_ifs/100/4 3738 ns 3738 ns 188087
reversed_ifs/100/8 7476 ns 7476 ns 93752
ordered_ifs_with_hints/50/4 5551 ns 5551 ns 125160
ordered_ifs_with_hints/50/8 23191 ns 23190 ns 30028
ordered_ifs_with_hints/75/4 3165 ns 3165 ns 218492
ordered_ifs_with_hints/75/8 13785 ns 13785 ns 50574
ordered_ifs_with_hints/100/4 1575 ns 1575 ns 437687
ordered_ifs_with_hints/100/8 3130 ns 3130 ns 221205
reversed_ifs_with_hints/50/4 6573 ns 6572 ns 105629
reversed_ifs_with_hints/50/8 27351 ns 27351 ns 25568
reversed_ifs_with_hints/75/4 3537 ns 3537 ns 197470
reversed_ifs_with_hints/75/8 16130 ns 16130 ns 43279
reversed_ifs_with_hints/100/4 3737 ns 3737 ns 187583
reversed_ifs_with_hints/100/8 7446 ns 7446 ns 93782
对于4K迭代和(几乎)100%高度喜欢的声明概率,差异是巨大的223%:
---------------------------------------------------------------------
Benchmark Time CPU Iterations
---------------------------------------------------------------------
ordered_ifs/100/4 1673 ns 1673 ns 417073
reversed_ifs/100/4 3738 ns 3738 ns 188087
对于4K迭代和高度喜欢的声明的50%概率,差异大约为14%:
---------------------------------------------------------------------
Benchmark Time CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4 4660 ns 4658 ns 150948
reversed_ifs/50/4 5342 ns 5341 ns 126800
对于高度喜欢的语句(几乎)100%概率的4K和8K迭代之间的差异大约是两倍(如预期的那样):
---------------------------------------------------------------------
Benchmark Time CPU Iterations
---------------------------------------------------------------------
ordered_ifs/100/4 1673 ns 1673 ns 417073
ordered_ifs/100/8 3381 ns 3381 ns 207612
但4K和8K迭代之间的差异为50%概率的高度喜欢的声明是5.5倍:
---------------------------------------------------------------------
Benchmark Time CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4 4660 ns 4658 ns 150948
ordered_ifs/50/8 25636 ns 25635 ns 27852
为什么会这样?因为分支预测器未命中。以下是每个上述案例的分支遗漏:
ordered_ifs/100/4 0.01% of branch-misses
ordered_ifs/100/8 0.01% of branch-misses
ordered_ifs/50/4 3.18% of branch-misses
ordered_ifs/50/8 15.22% of branch-misses
因此,在我的i5上,分支预测器因不太可能的分支和大数据集而失败。
对于4K迭代,50%概率的结果稍差,而接近100%的概率则略好一些:
---------------------------------------------------------------------
Benchmark Time CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4 4660 ns 4658 ns 150948
ordered_ifs/100/4 1673 ns 1673 ns 417073
ordered_ifs_with_hints/50/4 5551 ns 5551 ns 125160
ordered_ifs_with_hints/100/4 1575 ns 1575 ns 437687
但是对于8K迭代,结果总是好一点:
---------------------------------------------------------------------
Benchmark Time CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/8 25636 ns 25635 ns 27852
ordered_ifs/100/8 3381 ns 3381 ns 207612
ordered_ifs_with_hints/50/8 23191 ns 23190 ns 30028
ordered_ifs_with_hints/100/8 3130 ns 3130 ns 221205
所以,提示也有帮助,但只是一点点。
总体结论是:始终对代码进行基准测试,因为结果可能会出人意料。
希望有所帮助。
答案 4 :(得分:18)
根据这里的其他一些答案,看起来唯一真正的答案是:取决于。它至少取决于以下内容(虽然不一定按重要性顺序排列):
唯一可以确定的方法是对特定情况进行基准测试,最好是在与代码最终运行的目标系统相同(或非常类似)的系统上。如果打算在具有不同硬件,操作系统等的一组不同系统上运行,那么最好对多个变体进行基准测试,以确定哪个最佳。在一种类型的系统上使用一种顺序编译代码并在另一种类型的系统上编译另一种顺序甚至可能是个好主意。
我个人的经验法则(对于大多数情况,在没有基准的情况下)是基于以下订单:
答案 5 :(得分:12)
我通常看到这个解决高性能代码的方法是保持最可读的顺序,但为编译器提供提示。以下是Linux kernel中的一个示例:
if (likely(access_ok(VERIFY_READ, from, n))) {
kasan_check_write(to, n);
res = raw_copy_from_user(to, from, n);
}
if (unlikely(res))
memset(to + (n - res), 0, res);
这里的假设是访问检查将通过,并且res
中不会返回任何错误。尝试重新排序其中任何一个if子句只会混淆代码,但likely()
和unlikely()
宏实际上通过指出正常情况和异常是什么来帮助提高可读性。
这些宏的Linux实现使用GCC specific features。似乎clang和Intel C编译器支持相同的语法,但是MSVC doesn't have such feature。
答案 6 :(得分:6)
还取决于您的编译器和您正在编译的平台。
理论上,最可能的条件应该使控制跳跃尽可能少。
通常,最可能的条件应该是第一个:
if (most_likely) {
// most likely instructions
} else …
最受欢迎的asm基于条件分支,当条件为 true 时跳转。该C代码可能会转换为伪asm:
jump to ELSE if not(most_likely)
// most likely instructions
jump to end
ELSE:
…
这是因为跳转使得cpu取消执行管道并因为程序计数器改变而停止(对于支持真正常见的管道的架构)。 然后是关于编译器,它可能会或可能不会应用一些复杂的优化,以获得统计上最可能的条件,以使控件减少跳跃。
答案 7 :(得分:4)
我决定使用Lik32代码在我自己的机器上重新运行测试。我不得不改变它,因为我的Windows或编译器认为高分辨率是1ms,使用
mingw32-g ++。exe -O3 -Wall -std = c ++ 11 -fexceptions -g
vector<int> rand_vec(10000000);
GCC对两个原始代码进行了相同的转换。
请注意,只测试了两个第一个条件,因为第三个条件必须始终为真,GCC在这里是一种Sherlock。
反向
.L233:
mov DWORD PTR [rsp+104], 0
mov DWORD PTR [rsp+100], 0
mov DWORD PTR [rsp+96], 0
call std::chrono::_V2::system_clock::now()
mov rbp, rax
mov rax, QWORD PTR [rsp+8]
jmp .L219
.L293:
mov edx, DWORD PTR [rsp+104]
add edx, 1
mov DWORD PTR [rsp+104], edx
.L217:
add rax, 4
cmp r14, rax
je .L292
.L219:
mov edx, DWORD PTR [rax]
cmp edx, 94
jg .L293 // >= 95
cmp edx, 19
jg .L218 // >= 20
mov edx, DWORD PTR [rsp+96]
add rax, 4
add edx, 1 // < 20 Sherlock
mov DWORD PTR [rsp+96], edx
cmp r14, rax
jne .L219
.L292:
call std::chrono::_V2::system_clock::now()
.L218: // further down
mov edx, DWORD PTR [rsp+100]
add edx, 1
mov DWORD PTR [rsp+100], edx
jmp .L217
And sorted
mov DWORD PTR [rsp+104], 0
mov DWORD PTR [rsp+100], 0
mov DWORD PTR [rsp+96], 0
call std::chrono::_V2::system_clock::now()
mov rbp, rax
mov rax, QWORD PTR [rsp+8]
jmp .L226
.L296:
mov edx, DWORD PTR [rsp+100]
add edx, 1
mov DWORD PTR [rsp+100], edx
.L224:
add rax, 4
cmp r14, rax
je .L295
.L226:
mov edx, DWORD PTR [rax]
lea ecx, [rdx-20]
cmp ecx, 74
jbe .L296
cmp edx, 19
jle .L297
mov edx, DWORD PTR [rsp+104]
add rax, 4
add edx, 1
mov DWORD PTR [rsp+104], edx
cmp r14, rax
jne .L226
.L295:
call std::chrono::_V2::system_clock::now()
.L297: // further down
mov edx, DWORD PTR [rsp+96]
add edx, 1
mov DWORD PTR [rsp+96], edx
jmp .L224
所以这并没有告诉我们多少,除了最后一个案例不需要分支预测。
现在我尝试了if的所有6个组合,前2个是原始的反向和排序。高是> = 95,低是&lt; 20,mid是20-94,每次迭代10000000次。
high, low, mid: 43000000ns
mid, low, high: 46000000ns
high, mid, low: 45000000ns
low, mid, high: 44000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns
high, low, mid: 44000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 45000000ns
high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns
high, low, mid: 42000000ns
mid, low, high: 46000000ns
high, mid, low: 46000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 43000000ns
high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 44000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns
high, low, mid: 43000000ns
mid, low, high: 48000000ns
high, mid, low: 44000000ns
low, mid, high: 44000000ns
mid, high, low: 45000000ns
low, high, mid: 45000000ns
high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns
high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns
high, low, mid: 43000000ns
mid, low, high: 46000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 45000000ns
low, high, mid: 44000000ns
high, low, mid: 42000000ns
mid, low, high: 46000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 45000000ns
low, high, mid: 44000000ns
1900020, 7498968, 601012
Process returned 0 (0x0) execution time : 2.899 s
Press any key to continue.
那么为什么订单高,低,中等然后更快(略微)
因为最不可预测的是最后一个,因此永远不会通过分支预测器运行。
if (i >= 95) ++nHigh; // most predictable with 94% taken
else if (i < 20) ++nLow; // (94-19)/94% taken ~80% taken
else if (i >= 20 && i < 95) ++nMid; // never taken as this is the remainder of the outfalls.
因此,分支将被预测为采取,采取和剩余
6%+(0.94 *)20%误预测。
“排序”
if (i >= 20 && i < 95) ++nMid; // 75% not taken
else if (i < 20) ++nLow; // 19/25 76% not taken
else if (i >= 95) ++nHigh; //Least likely branch
分支将被预测未被采取,而不是被采取和Sherlock。
25%+(0.75 *)24%误预测
给出18-23%的差异(测量的差异为~9%),但我们需要计算周期而不是错误预测%。
假设17个周期对我的Nehalem CPU进行了错误预测处罚,并且每次检查需要1个周期才能发出(4-5条指令),并且循环也需要一个周期。数据依赖性是计数器和循环变量,但一旦错误预测不合适,它就不会影响时间。
因此,对于“反向”,我们得到时间(这应该是计算机体系结构中使用的公式:定量方法IIRC)。
mispredict*penalty+count+loop
0.06*17+1+1+ (=3.02)
(propability)*(first check+mispredict*penalty+count+loop)
(0.19)*(1+0.20*17+1+1)+ (= 0.19*6.4=1.22)
(propability)*(first check+second check+count+loop)
(0.75)*(1+1+1+1) (=3)
= 7.24 cycles per iteration
和“已排序”
相同0.25*17+1+1+ (=6.25)
(1-0.75)*(1+0.24*17+1+1)+ (=.25*7.08=1.77)
(1-0.75-0.19)*(1+1+1+1) (= 0.06*4=0.24)
= 8.26
(8.26-7.24)/8.26 = 13.8%对比~9%(接近测量值!?!)。
所以OP的显而易见性并不明显。
通过这些测试,其他具有更复杂代码或更多数据依赖性的测试肯定会有所不同,因此请测量您的情况。
更改测试的顺序改变了结果,但这可能是因为循环开始的不同对齐,理想情况下在所有较新的Intel CPU上对齐16个字节,但在这种情况下不是。
答案 8 :(得分:3)
按照你喜欢的顺序排列它们。当然,分支可能会更慢,但分支不应该是计算机正在进行的大部分工作。
如果您正在处理性能关键部分的代码,那么肯定会使用逻辑顺序,配置文件引导优化和其他技术,但对于一般代码,我认为它更像是一种风格选择。
答案 9 :(得分:2)
如果您已经知道if-else语句的相对概率,那么出于性能目的,最好使用排序方式,因为它只会检查一个条件(真实条件)。
编译器将以未分类的方式检查所有不必要的条件,并且需要时间。