如果按概率排序if ... else if语句有什么作用?


187

具体来说,如果我有一系列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语句可以任意重新排序,而对程序的行为没有任何其他影响。在我的回答中,这三个条件测试是互斥的,不会产生副作用。当然,如果必须以某种顺序对语句进行评估才能实现某些所需的行为,那么效率问题就不那么重要了。


35
您可能需要添加一条条件是互斥的说明,否则两个版本不等效
idclev 463035818

28
一个自我回答的问题如何在一小时内得到20多个投票,但答案却很差,这很有趣。没有在OP上调用任何东西,但是支持者应该提防跳上乐队旅行。这个问题可能很有趣,但是结果令人怀疑。
luk32

3
我相信这可以描述为短路评估的一种形式,因为碰到一个比较就否认碰到了另一个比较。当一个快速比较(例如boolean)可以阻止我进行另一种可能涉及大量资源的字符串操作,正则表达式或数据库交互的比较时,我个人喜欢这样的实现。
MonkeyZeus

11
一些编译器提供了收集有关分支的统计信息并将其反馈给编译器的功能,以使其能够进行更好的优化。

11
如果这样的性能对您而言很重要,则您可能应该尝试使用Profile Guided Optimization并将手动结果与编译器的结果进行比较
Justin

Answers:


96

通常,大多数(如果不是全部)英特尔CPU都假定在第一次看到前向分支时就不会采用它们。参见Godbolt的作品

之后,分支进入分支预测缓存,并且过去的行为用于通知将来的分支预测。

因此,在一个紧密的循环中,错误排序的影响将相对较小。分支预测器将学习最有可能的分支集,如果循环中的工作量非常少,那么细小的差别就不会太多。

在一般代码中,默认情况下(出于其他原因)大多数编译器将按与您在代码中对其排序的方式大致相同的顺序对生成的机器代码进行排序。因此,if语句在失败时是前向分支。

因此,您应该按照降低可能性的顺序对分支进行排序,以便从“首次相遇”中获得最佳的分支预测。

在一组条件下紧密循环多次并完成琐碎工作的微基准测试将主要受指令计数等的微小影响所支配,而很少涉及相对分支预测问题。因此,在这种情况下,您必须,因为经验法则是不可靠的。

最重要的是,矢量化和许多其他优化适用于微小的紧密循环。

因此,在一般代码中,将最可能的代码放在 if块中,这将导致最少的未缓存分支预测未命中。在紧密的循环中,请遵循一般规则开始,如果您需要了解更多信息,除了进行概要分析之外别无选择。

当然,如果某些测试比其他测试便宜得多,那么所有这些都会消失。


19
这也是值得考虑的测试本身多么昂贵的是:如果一个测试只是稍微更容易,但很多更昂贵,那么它可能是值得投入的其他测试第一,因为从没有使得昂贵的测试,储蓄将可能超过了分支预测等节省的资金
psmears

您提供的链接不支持您的结论作为一般规则,大多数(如果不是全部)英特尔CPU都假定在第一次看到前向分支时就不会采用它们。实际上,这仅适用于相对晦涩的Arrendale CPU,其结果首先显示出来。主流的Ivy Bridge和Haswell结果根本不支持这一点。对于看不见的分支,Haswell看起来非常接近“总是预测失败”,而Ivy Bridge根本不清楚。
BeeOnRope

通常可以理解,CPU并没有像过去那样真正使用静态预测。确实,现代英特尔可能正在使用概率TAGE预测器。您只需将分支历史记录哈希到各种历史记录表中,然后选择与最长历史记录匹配的表即可。它使用“标签”来尝试避免混叠,但是标签只有几位。如果您错过了所有历史记录长度,则可能会做出一些默认预测,该预测不一定取决于分支方向(在Haswell上,我们可以说显然不是这样)。
BeeOnRope

44

我组成了以下测试,以计时两个不同的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%vs 28%)。该代码在Windows 7下的Intel Xeon E5-2697 v2上运行。当然,这是非常特定于问题的,不应解释为最终答案。


9
但是,OP应该小心,因为更改if... else if语句可能会对逻辑在代码中的流动方式产生重大影响。该unlikely检查可能不会经常出现,但也可能是业务需要检查的unlikely第一检查别人面前条件。
卢克·布鲁克斯

21
快30%?您的意思是说它快了没必要执行的额外if语句的百分比?似乎是一个相当合理的结果。
UKMonkey

5
您是如何进行基准测试的?哪个编译器,cpu等?我很确定这个结果不是可移植的。
luk32

12
此微基准测试的问题在于,CPU将计算出最有可能出现的分支,并在您反复循环时对其进行缓存。如果分支没有在一个紧密的小循环中进行检查,则分支预测缓存中可能没有分支,并且如果CPU猜测零分支预测缓存指导为错误,则成本可能会更高。
Yakk-亚当·内夫罗蒙特

6
此基准不太可靠。使用gcc 6.3.0编译:g++ -O2 -march=native -std=c++14确实使排序后的条件语句略有优势,但是在大多数情况下,两次运行之间的百分比差为5%。几次,它实际上要慢一些(由于差异)。我相当确定,if像这样订购s值得担心。PGO可能会完全处理任何此类案件
贾斯汀(Justin)

30

不,您不应该这样做,除非您确实确定目标系统受到影响。默认情况下,按可读性进行。

我高度怀疑您的结果。我已经修改了您的示例,因此反转执行更加容易。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;
    }
}

当我切换已排序和反向排序的if块的顺序时,在性能上会有大约30%的差异,就像在代码中所做的一样。我不确定为什么Ideone和coliru没有区别。
卡尔顿

当然很有趣。我将尝试获取其他系统的一些数据,但是直到我不得不处理它之前,它可能要花费一天的时间。这个问题很有趣,尤其是根据您的结果,但是它们是如此壮观,以至于我不得不对其进行交叉核对。
luk32

如果问题是什么呢?答案不能为
PJTraill

对。但是我没有收到有关原始问题更新的通知。他们使答案表述过时了。抱歉。稍后我将编辑内容,以指出它回答了原始问题并显示了一些证明原始观点的结果。
luk32

值得重复一遍:“默认情况下,出于可读性考虑。” 编写可读代码通常会比使人更难解析的代码试图获得微小的性能提升(绝对值)要好。
安德鲁·布雷扎(AndrewBrēza)

26

只是我的5美分。如果语句应取决于以下内容,则排序的效果似乎是:

  1. 每个if语句的概率。

  2. 迭代次数,因此分支预测变量可以启动。

  3. 可能/不太可能的编译器提示,即代码布局。

为了探索这些因素,我对以下功能进行了基准测试:

ordered_ifs()

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;
}

reversed_ifs()

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;
}

ordered_ifs_with_hints()

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;
}

reversed_ifs_with_hints()

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

分析

1.顺序很重要

对于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

2.重要的迭代次数

(几乎)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

但是,对于高度喜欢的语句,有50%的概率发生4K和8K迭代之间的差异是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上,分支预测器因不太可能的分支和大数据集而严重失败。

3.提示有所帮助

对于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

因此,提示也有帮助,但只有一点点。

总体结论是:始终对代码进行基准测试,因为结果可能会令人惊讶。

希望有帮助。


1
i5 Nehalem?i5 Skylake?只是说“ i5”不是很具体。另外,我假设您使用g++ -O2-O3 -fno-tree-vectorize,但是您应该这样说。
彼得·科德斯

有趣的是,with_hints对于有序和逆向还是有所不同。如果您将源链接到某个地方,那会很好。(例如Godbolt链接,最好是完整链接,这样链接缩短不会腐烂。)
Peter Cordes

1
分支预测器即使在4K输入数据大小下也能够很好地预测,即能够通过记住数千周期中一个周期的循环中的分支结果来“打破”基准,这一事实证明了现代的强大功能。分支预测变量。请记住,在某些情况下,预测变量对对齐等内容非常敏感,因此很难对某些变化得出强有力的结论。例如,您在不同情况下注意到提示的行为相反,但是可以通过提示随机更改代码布局来解释它,这会影响预测变量。
BeeOnRope

1
@PeterCordes我的主要观点是,虽然我们可以尝试预测更改的结果,但我们仍然可以更好地衡量更改前后的性能...没错,我应该提到它已使用-O3和处理器进行了优化是i5-4460 @ 3.20GHz
Andriy Berestovskyy

19

根据此处的其他一些答案,看起来唯一的真实答案是:它取决于。它至少取决于以下内容(尽管不一定按重要性顺序排列):

  • 每个分支的相对概率。 这是最初提出的问题。根据现有的答案,似乎在某些情况下按概率排序会有所帮助,但情况并非总是如此。如果相对概率差别不大,则不太可能改变它们的顺序。但是,如果第一个条件发生在99.999%的时间上,而下一个条件是剩余的几分之一,那么我会假设在时间安排上将最有可能的问题放在第一位将是有益的。
  • 计算每个分支的真假条件的成本。 如果一个分支相对于另一个分支测试条件的时间成本确实很高,那么这可能会对时序和效率产生重大影响。例如,考虑一个以1个时间单位进行计算(例如,检查布尔变量的状态)的条件与另一个以数十个,数百个,数千个,甚至数百万个时间单位进行计算(例如,检查变量的内容)的条件。磁盘上的文件或针对大型数据库执行复杂的SQL查询)。假设代码每次都按顺序检查条件,则较快的条件应该优先(除非它们首先依赖于其他条件)。
  • 编译器/解释器 某些编译器(或解释器)可能包括一种会影响性能的优化(并且其中一些仅在编译和/或执行过程中选择了某些选项时才会出现)。因此,除非您使用完全相同的编译器在同一系统上对两个相同代码的编译和执行进行基准测试,唯一的区别是所讨论的分支的顺序,否则您将不得不为编译器的变体留出一些余地。
  • 操作系统/硬件 如luk32和Yakk所述,各种CPU都有自己的优化(操作系统也是如此)。因此,基准再次容易受到此处变化的影响。
  • 代码块执行的频率 如果很少访问包含分支的块(例如,在启动过程中仅访问一次),则对分支的放置顺序无关紧要。另一方面,如果在代码的关键部分代码在该代码块上不起作用,则排序可能会很重要(取决于基准)。

唯一可以确定的方法是对您的特定情况进行基准测试,最好是在与最终将运行代码的目标系统相同(或非常相似)的系统上。如果要在具有不同硬件,操作系统等的一组不同系统上运行,那么最好对多个变体进行基准测试以找出最佳方案。最好用一种命令在一种类型的系统上编译另一种命令在另一种类型的系统上编译代码。

我个人的经验法则(在大多数情况下,在没有基准的情况下)是根据以下条件进行排序:

  1. 取决于先前条件的结果的条件,
  2. 然后计算条件的成本
  3. 每个分支的相对概率。

13

我通常看到的为高性能代码解决的方法是保持最易读的顺序,但向编译器提供提示。这是Linux内核的一个示例:

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()andunlikely()宏通过指出什么是正常情况以及什么是例外情况,实际上有助于提高可读性。

这些宏的Linux实现使用GCC特定的功能。似乎clang和Intel C编译器支持相同的语法,但是MSVC没有这种功能


4
如果您可以解释likely()unlikely()宏的定义方式,并包括有关相应编译器功能的一些信息,则这将更为有用。
Nate Eldredge

1
AFAIK,这些提示“仅”更改代码块的内存布局,是或否将导致跳转。这可能具有性能优势,例如需要(或不需要)读取内存页面。但这并没有重新排列一长串else-ifs中条件的评估顺序
Hagen von Eitzen 17-10-22

@HagenvonEitzen嗯,这是一个好主意,它不会影响else if编译器是否足够聪明地知道条件是互斥的顺序。
jpa

7

还取决于您的编译器和要编译的平台。

从理论上讲,最可能出现的情况应使控制跳变尽可能小。

通常,最可能的条件应该是:

if (most_likely) {
     // most likely instructions
} else 

最受欢迎的asm基于条件分支,当condition为true时会跳转。该C代码很可能会转换为此类伪asm:

jump to ELSE if not(most_likely)
// most likely instructions
jump to end
ELSE:

这是因为跳转使cpu取消了执行管道并因程序计数器已​​更改而停止(对于支持真正常见的管道的体系结构)。然后是关于编译器的问题,该编译器可能会或可能不会应用一些复杂的优化,以使统计上最有可能的条件使控件产生更少的跳跃。


2
您说条件为真时就会发生条件分支,但“伪asm”示例却相反。同样,不能说条件跳转(少得多的所有跳转)使流水线停滞了,因为现代CPU通常具有分支预测功能。实际上,如果预计将采用该分支但采用该分支,则管道将停止。我仍将尝试按概率的降序对条件进行排序,但是编译器和CPU对其进行的处理与实现高度相关。
Arne Vogel

1
我输入了“ not(most_likely)”,因此,如果most_likely为true,则控件将继续运行而不会跳转。
NoImaginationGuy

1
“最流行的asm是基于在条件为真时会跳转的条件分支”。对于x86或ARM来说肯定不是正确的。对于基本的ARM CPU(以及非常古老的x86处理器,甚至对于复杂的bps,它们通常仍从该假设开始然后适应)地狱,分支预测器假定采用前向分支,而始终采用后向分支,因此与主张相反是真的。
Voo

1
我尝试过的编译器几乎都使用了上面提到的方法进行简单测试。请注意,clang实际上对test2and使用了不同的方法test3:由于启发式方法表明a < 0== 0test可能为假,因此决定在两条路径上克隆该函数的其余部分,以便能够condition == false通过该路径。这仅是可行的,因为该函数的其余部分很短:在本例中,test4我又添加了一个操作,这又回到了我上面概述的方法。
BeeOnRope

1
@ArneVogel-正确预测的已采取分支不会完全使现代CPU上的管道停顿,但它们通常比未采取分支严重得多:(1)它们表示控制流不是连续的,因此其余的指令jmp不在有用,因此即使进行预测,浪费的获取/解码带宽也不会浪费(2)(2)即使现代大内核每个周期只能进行一次获取,因此将硬限制设置为1个分支/周期(现代OH英特尔可以做到2个未使用/周期)(3 )分支预测很难处理连续的分支,并且在快速预测器和慢速预测器的情况下……
BeeOnRope

6

我决定使用Lik32代码在自己的计算机上重新运行测试。我不得不更改它,因为我的Windows或编译器认为高分辨率为1ms,使用

mingw32-g ++。exe -O3 -Wall -std = c ++ 11 -fexceptions -g

vector<int> rand_vec(10000000);

GCC对两个原始代码进行了相同的转换。

请注意,仅测试了第一个条件,因为第三个条件必须始终为真,在这里,GCC是一种夏洛克。

逆转

.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,低是<20,中是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

分支将被预测为不采用,不采用和夏洛克。

25%+(0.75 *)24%的错误预测

给出18-23%的差异(测得的〜9%差异),但我们需要计算周期而不是错误地预测%。

假设我的Nehalem CPU上有17个周期的错误预测惩罚,并且每次检查需要1个周期才能发出(4-5条指令),而循环也需要1个周期。数据相关性是计数器和循环变量,但是一旦错误预测消失了,它就不会影响时序。

因此,对于“反向”,我们可以获得时间(这应该是“计算机体系结构:定量方法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字节对齐,但在这种情况下则不然。


4

按照您喜欢的逻辑顺序放置它们。当然,分支可能会变慢,但分支不应成为计算机正在执行的大部分工作。

如果您正在处理代码的性能关键部分,那么可以肯定使用逻辑顺序,配置文件引导的优化和其他技术,但是对于一般代码,我认为它实际上更多地是一种样式选择。


6
分支预测失败的代价很高。在微基准测试中,它们成本低廉,因为x86具有庞大的分支预测变量表。在相同条件下发生紧密循环会导致CPU比您更可能了解CPU。但是,如果您在代码中遍布分支,则可以使分支预测缓存用尽插槽,并且cpu会采用默认值。知道默认猜测是什么可以节省整个代码库的周期。
Yakk-Adam Nevraumont

@Yakk Jack的答案是这里唯一正确的答案。如果您的编译器能够进行优化,则不要进行会降低可读性的优化。如果编译器为您执行了此操作,则不会进行连续折叠,消除死代码,循环展开或任何其他优化,对吗?编写您的代码,使用配置文件引导的优化(此设计旨在解决此问题,因为编码人员会猜测,因此可以解决此问题),然后查看编译器是否对其进行了优化。最后,您还是不想在性能关键代码中有任何分支。
Christoph Diegelmann '17

@Christoph我不会包含我知道已经死了的代码。我不会使用i++什么时候++i会做,因为我知道i++对于某些迭代器而言,很难进行最优化,++i而差异(对我而言)并不重要。这是关于避免悲观。将最有可能的代码块作为默认习惯不会导致明显的可读性下降(并且实际上可能会有所帮助!),同时导致代码对分支预测的友好(从而为您提供统一的小性能提升,无法重新捕获) (稍后再进行微优化))
Yakk-Adam Nevraumont

3

如果您已经知道if-else语句的相对概率,则出于性能目的,最好使用排序方式,因为它只会检查一个条件(真实条件)。

编译器将以未分类的方式不必要地检查所有条件,这将花费时间。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.