Performance 通过提前计算条件,避免管道停滞

Performance 通过提前计算条件,避免管道停滞,performance,language-agnostic,compiler-optimization,cpu-architecture,branch-prediction,Performance,Language Agnostic,Compiler Optimization,Cpu Architecture,Branch Prediction,在谈论ifs的性能时,我们通常会谈论预测失误如何会阻碍管道的运行。我看到的建议解决方案有: 对于通常只有一个结果的情况,信任分支预测器;或 如果合理的话,避免使用一点魔法来分支;或 在可能的情况下有条件地移动 我找不到的是,我们是否可以尽早计算病情,以便尽可能地提供帮助。因此,不是: ... work if (a > b) { ... more work } 这样做: bool aGreaterThanB = a > b; ... work if (aGreaterThan

在谈论ifs的性能时,我们通常会谈论预测失误如何会阻碍管道的运行。我看到的建议解决方案有:

  • 对于通常只有一个结果的情况,信任分支预测器;或
  • 如果合理的话,避免使用一点魔法来分支;或
  • 在可能的情况下有条件地移动
  • 我找不到的是,我们是否可以尽早计算病情,以便尽可能地提供帮助。因此,不是:

    ... work
    if (a > b) {
        ... more work
    }
    
    这样做:

    bool aGreaterThanB = a > b;
    ... work
    if (aGreaterThanB) {
        ... more work
    }
    
    long sum_sentinel(list_head list) {
        int sum = 0;
        for (list_node* cur = list.first; cur; cur = cur->next) {
            sum += cur->value;
        }
        return sum;
    }
    
    这样的事情是否可以完全避免这个条件上的停顿(取决于管道的长度以及我们可以在bool和if之间投入的工作量)?它不必像我写的那样,但是有没有一种方法可以早期评估条件,这样CPU就不必尝试预测分支了


    此外,如果这有帮助的话,编译器可能会这样做吗?

    无序执行肯定是一件事(不仅仅是编译器,甚至处理器芯片本身也可以对指令进行重新排序),但与预测失误相比,它更能帮助解决数据依赖性导致的管道暂停问题

    控制流场景中的好处在某种程度上受到以下事实的限制:在大多数体系结构上,条件分支指令仅基于标志寄存器而不是基于通用寄存器作出决定。除非中间的“工作”非常不寻常,否则很难提前设置标志寄存器,因为大多数指令都会更改标志寄存器(在大多数体系结构上)

    也许是确定了

    TST (reg)
    J(condition)
    

    (reg)
    提前设置得足够远时,可将失速降至最低。这当然需要处理器的大量帮助,而不仅仅是编译器。处理器设计者可能会针对更一般的情况进行优化,即设置分支标志的指令提前(无序)执行,结果标志通过管道转发,提前结束暂停。

    Yes,允许尽早计算分支条件,以便尽早解决任何预测失误,并尽早开始重新填充管道前端部分,这可能是有益的。在最好的情况下,如果已经有足够的工作来完全隐藏前端气泡,那么错误预测是可以避免的

    不幸的是,在无序的CPU上,early有一个稍微微妙的定义,因此尽早解析分支并不像在源代码中移动行那样简单——您可能需要更改条件的计算方式

    什么不起作用 不幸的是,早期版本没有提到源文件中条件/分支的位置,也没有提到与比较或分支相对应的汇编指令的位置。因此,在基本层面上,它的工作原理与您的示例中的不同

    即使源代码级别的定位很重要,在您的示例中也可能不起作用,因为:

    您已将条件的计算向上移动并将其分配给
    bool
    ,但可能预测失误的不是测试(
    b)
    更改为
    if(AGREATERTANB)

    除此之外,您转换代码的方式不太可能愚弄大多数编译器。优化编译器不会按照您编写代码的顺序逐行发出代码,而是根据源代码级别的依赖关系来安排适当的时间。更早地启动条件可能会被忽略,因为编译器希望将检查放在它自然会去的地方:大约在带有标志寄存器的体系结构上的分支之前

    例如,考虑以下两个简单函数的实现,它们遵循您所建议的模式。第二个函数将条件向上移动到函数的顶部

    int test1(int a, int b) {
        int result = a * b;
        result *= result;
        if (a > b) {
            return result + a;
        }
        return result + b * 3;
    }
    
    int test2(int a, int b) {
        bool aGreaterThanB = a > b;
        int result = a * b;
        result *= result;
        if (aGreaterThanB) {
            return result + a;
        }
        return result + b * 3;
    }
    
    我检查了gcc、clang2和MSVC,并对这两个函数进行了编译(编译器之间的输出不同,但对于每个编译器,这两个函数的输出是相同的)。例如,使用
    gcc
    编译
    test2
    会导致:

    test2(int, int):
      mov eax, edi
      imul eax, esi
      imul eax, eax
      cmp edi, esi
      jg .L4
      lea edi, [rsi+rsi*2]
    .L4:
      add eax, edi
      ret
    
    cmp
    指令对应于
    a>b
    条件,gcc将其向下移动,越过所有“工作”,并将其放在条件分支
    jg
    旁边

    什么有效 那么,如果我们知道对源代码中操作顺序的简单操作不起作用,那么什么起作用呢?事实证明,您所能做的任何事情都可以将数据流图中的分支条件“向上”移动,从而允许更早地解决预测失误,从而提高性能。我不打算深入讨论现代CPU是如何依赖于数据流的,但您可以在最后找到一个有进一步阅读的指针的示例

    遍历链表 下面是一个涉及链表遍历的真实示例

    考虑将所有值求和到一个以null结尾的链表的任务,该链表还将其长度1存储为列表头结构的一个成员。实现为一个
    list\u head
    对象和零个或多个列表节点(具有单个
    int值
    有效负载)的链表,定义如下:

    struct list_node {
        int value;
        list_node* next;
    };
    
    struct list_head {
        int size;
        list_node *first;
    };
    
    规范搜索循环将使用最后一个节点中的
    节点->下一个==nullptr
    哨兵来确定is已到达列表的末尾,如下所示:

    bool aGreaterThanB = a > b;
    ... work
    if (aGreaterThanB) {
        ... more work
    }
    
    long sum_sentinel(list_head list) {
        int sum = 0;
        for (list_node* cur = list.first; cur; cur = cur->next) {
            sum += cur->value;
        }
        return sum;
    }
    
    这就跟你得到的一样简单

    但是,这会将结束求和的分支(第一个
    cur==null
    )放在节点到节点指针跟踪的末尾,这是数据流图中最长的依赖关系。如果这麦麸
    ** Running benchmark group Tests written in C++ **
                         Benchmark   Cycles   BR_MIS
           Linked-list w/ Sentinel    12.19     0.00
              Linked-list w/ count    12.40     0.00
    
    ** Running benchmark group Tests written in C++ **
                         Benchmark   Cycles   BR_MIS
           Linked-list w/ Sentinel    43.87     0.88
              Linked-list w/ count    27.48     0.89