订购if ... else if语句的概率是什么?

Car*_*ton 184 c++ optimization performance if-statement branch-prediction

具体来说,如果我有一系列if... else if语句,并且我事先知道每个语句将评估的相对概率,true它按照概率的顺序对它们进行排序有多大差异?例如,我应该更喜欢这个:

if (highly_likely)
  //do something
else if (somewhat_likely)
  //do something
else if (unlikely)
  //do something
Run Code Online (Sandbox Code Playgroud)

对此?:

if (unlikely)
  //do something
else if (somewhat_likely)
  //do something
else if (highly_likely)
  //do something
Run Code Online (Sandbox Code Playgroud)

很明显,排序版本会更快,但是为了便于阅读或存在副作用,我们可能希望对它们进行非最佳排序.在您实际运行代码之前,很难判断CPU在分支预测方面的表现如何.

因此,在尝试这个过程中,我最终回答了我自己的问题,但我还想听听其他意见/见解.

重要提示:此问题假定if语句可以任意重新排序,而不会对程序的行为产生任何其他影响.在我的回答中,三个条件测试是互斥的,不会产生副作用.当然,如果必须按某种顺序评估陈述以达到某些预期的行为,那么效率问题就没有实际意义.

Yak*_*ont 94

作为一般规则,大多数(如果不是所有)英特尔CPU都假设前向分支不会在他们第一次看到它们时采用.见Godbolt的作品.

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

因此,在紧密的循环中,错误排序的影响将相对较小.分支预测器将学习哪一组分支是最有可能的,如果你在循环中有非常重要的工作量,那么小的差异就不会增加太多.

在一般代码中,默认情况下大多数编译器(缺少其他原因)将按照您在代码中订购的方式大致订购生产的机器代码.因此,如果语句在失败时是前向分支.

因此,您应该按照降低可能性的顺序对您的分支进行排序,以便从"第一次遇到"获得最佳分支预测.

微型基准测试在一组条件下多次循环,并且微不足道的工作将由指令计数等的微小影响主导,并且几乎没有相对分支预测问题.因此,在这种情况下,您必须进行分析,因为经验法则不可靠.

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

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

当然,如果某些测试比其他测试便宜得多,这一切都会消失.

  • 同样值得考虑的是测试本身有多昂贵:如果一次测试只是稍微有点可能,但是很多*更贵,那么可能值得将其他测试放在第一位,因为不会进行昂贵的测试可以节省成本超过分支预测等的节省. (19认同)

Car*_*ton 43

我做了下面的测试时间的两个不同的执行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;
}
Run Code Online (Sandbox Code Playgroud)

使用带有/ O2的MSVC2017,结果显示排序版本始终比未排序版本快约28%.根据luk32的评论,我也改变了两个测试的顺序,这显示出明显的差异(22%对28%).该代码在Intel Xeon E5-2697 v2上的Windows 7下运行.当然,这是非常特定于问题的,不应被解释为确凿的答案.

  • 快30%?你的意思是它的速度大约是它不必执行的额外if语句的百分比吗?似乎是一个非常合理的结果. (21认同)
  • 这个微基准测试的一个问题是,CPU将决定哪个分支最有可能,并在重复循环时对其进行缓存.如果未在小的紧密循环中检查分支,则分支预测缓存可能没有它们,并且如果CPU猜测零分支预测缓存引导错误,则成本可能会高得多. (12认同)
  • OP应该小心,因为更改`if ... else if`语句可能会对逻辑流经代码的方式产生重大影响."不太可能"的检查可能不会经常出现,但在检查其他人之前,可能需要先检查"不太可能"的情况. (9认同)
  • 这个基准不太可靠.使用**gcc 6.3.0编译**:`g ++ -O2 -march = native -std = c ++ 14`确实给排序的条件语句略微优势,但大部分时间,两者之间的百分比差异跑步率约为5%.有好几次,它实际上更慢(由于差异).我很确定订购`如果这样的话就不值得担心; PGO可能会完全处理任何此类案件 (6认同)
  • 你是如何对它进行基准的?哪个编译器,cpu等?我很确定这个结果不便携. (5认同)
  • 有趣的是,一旦我打开PGO,排序版本往往运行得更慢.编译器能够为反向排序版本生成更小的程序集.这并不是说您应该按照反向排序的可能性顺序编写代码,但如果您考虑担心这一点,则应首先启用编译器优化(设置目标arch,LTO,如果适用,并使用PGO) (3认同)
  • 出于好奇,您是否尝试过逆转基准订单?我怀疑它会有所作为,但它很有趣. (2认同)
  • "......那个排序的版本......"它们都是*排序的.这种说法含糊不清,令人困惑. (2认同)
  • 这是一个例子,为什么你不应该自己优化编译器设计要做的事情(PGO).这里的最佳解决方案是使用Hu-Tucker形状的分支树(保留最佳深度树的顺序).在这个简单的例子中,只需要2个比较(而不是你的代码有4个).此外,这段代码可以很容易地编写为无分支(并且一些编译器实际上可能会这样做).PGO将为您做所有这些. (2认同)

luk*_*k32 29

不,你不应该,除非你确定目标系统受到影响.默认情况下,请阅读可读性.

我非常怀疑你的结果.我稍微修改了你的例子,因此反向执行更容易.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;
    }
}
Run Code Online (Sandbox Code Playgroud)


And*_*kyy 23

只需我5美分.看来if语句的排序效果应该取决于:

  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;
}
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

数据

数据数组包含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;
}
Run Code Online (Sandbox Code Playgroud)

结果

以下结果适用于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
Run Code Online (Sandbox Code Playgroud)

分析

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
Run Code Online (Sandbox Code Playgroud)

对于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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

但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
Run Code Online (Sandbox Code Playgroud)

为什么会这样?因为分支预测器未命中.以下是上述每个案例的分支未命中数:

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
Run Code Online (Sandbox Code Playgroud)

所以在我的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
Run Code Online (Sandbox Code Playgroud)

但是对于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
Run Code Online (Sandbox Code Playgroud)

所以,提示也有帮助,但只是一点点.

总的结论是:始终对代码进行基准测试,因为结果可能会出人意料.

希望有所帮助.

  • i5 尼哈勒姆?i5 天湖?仅仅说“i5”并不是很具体。另外,我假设你使用了 `g++ -O2` 或 `-O3 -fno-tree-vectorize`,但你应该这么说。 (2认同)

小智 18

基于这里的一些其他答案,看起来唯一真正的答案是:它取决于.它至少取决于以下内容(尽管不一定按重要性顺序排列):

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

确切知道的唯一方法是对特定情况进行基准测试,最好是在与代码最终运行的目标系统相同(或非常类似)的系统上.如果打算在具有不同硬件,操作系统等的一组不同系统上运行,那么最好对多个变体进行基准测试,以确定哪个最佳.在一种类型的系统上使用一种排序编译代码并在另一种类型的系统上编译另一种顺序甚至可能是个好主意.

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

  1. 依赖先前条件的结果的条件,
  2. 那么计算条件的成本
  3. 每个分支的相对概率.


jpa*_*jpa 12

我通常看到这个解决高性能代码的方式是保持最可读的顺序,但为编译器提供提示.以下是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);
Run Code Online (Sandbox Code Playgroud)

这里的假设是访问检查将通过,并且没有返回错误res.试图重新排序这些if子句中的任何一个只会混淆代码,但是likely()unlikely()宏实际上通过指出什么是正常情况以及什么是异常来帮助提高可读性.

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

  • 如果您可以解释如何定义`possible()`和`impossible()`宏,并包含有关相应编译器功能的一些信息,这将更有帮助. (4认同)

NoI*_*Guy 6

还取决于您的编译器和您正在编译的平台.

理论上,最可能的条件应该使控制跳跃尽可能少.

通常情况下,最可能的情况应该是:

if (most_likely) {
     // most likely instructions
} else …
Run Code Online (Sandbox Code Playgroud)

最流行的asm是基于条件分支,当条件为时跳转.那个C代码很可能会转换成这样的伪asm:

jump to ELSE if not(most_likely)
// most likely instructions
jump to end
ELSE:
…
Run Code Online (Sandbox Code Playgroud)

这是因为跳转使得cpu取消执行管道并因为程序计数器改变而停止(对于支持真正常见的管道的体系结构).然后是关于编译器,它可能会或可能不会应用一些复杂的优化,以获得统计上最可能的条件,以使控件减少跳跃.

  • 您声明条件分支在条件为真时发生,但"伪asm"示例显示相反的情况.此外,不能说条件跳转(更少的是所有跳转)使管道停滞,因为现代CPU通常具有分支预测.实际上,如果预计分支被采用但是*不被采用,则管道将被停止.我仍然会尝试按概率的降序对条件进行排序,但编译器和CPU对它的作用依赖于*高度*依赖于实现. (2认同)
  • “最流行的 asm 是基于在条件为真时跳转的条件分支”.. 那将是哪些 ISA?对于 x86 和 ARM,这当然不是真的。对于基本的 ARM CPU(以及非常古老的 x86 CPU,即使对于复杂的 bps,它们通常仍然以该假设开始然后适应),分支预测器假设前向分支 * 不* 被采用,而后向分支总是如此,所以与索赔是真实的。 (2认同)

Sur*_*urt 6

我决定使用 Lik32 代码在我自己的机器上重新运行测试。由于我的 windows 或编译器认为高分辨率是 1ms,我不得不改变它,使用

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

vector<int> rand_vec(10000000);
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

所以这并没有告诉我们太多,除了最后一种情况不需要分支预测。

现在我尝试了 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.
Run Code Online (Sandbox Code Playgroud)

那么为什么顺序高,低,中然后更快(边际)

因为最不可预测的是最后,因此永远不会通过分支预测器运行。

          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.
Run Code Online (Sandbox Code Playgroud)

所以分支将被预测为

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
Run Code Online (Sandbox Code Playgroud)

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

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

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

让我们假设在我的 Nehalem CPU 上有 17 个周期的错误预测惩罚,并且每次检查需要 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
Run Code Online (Sandbox Code Playgroud)

和“排序”相同

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
Run Code Online (Sandbox Code Playgroud)

(8.26-7.24)/8.26 = 13.8% vs. ~9% 测量值(接近测量值!?!)。

所以OP的明显性并不明显。

通过这些测试,其他具有更复杂代码或更多数据依赖性的测试肯定会有所不同,因此请衡量您的情况。

更改测试顺序会改变结果,但这可能是因为循环开始的对齐方式不同,理想情况下应该在所有较新的 Intel CPU 上对齐 16 个字节,但在这种情况下并非如此。