【问题标题】:Could arrays of function pointers be used to remove branches函数指针数组可以用来删除分支吗
【发布时间】:2018-09-07 10:39:23
【问题描述】:

函数指针数组能否通过删除分支来提高性能?

(function[number])();

快于

if(number == 0)
    function1();
else if (number == 1)
    function2();

为了性能? (假设这是对性能非常关键的代码)

编辑:数字是 0 或 1(因为它应该只是 0 或 1,所以使用其他)

【问题讨论】:

  • 测量一下你就知道了。
  • 这与函数调用开销无关,而是与分支有关。
  • 如果它真的是“性能关键”代码,你必须同时担心这两个问题。
  • 编译器优化会去掉它被测试的数千次。

标签: c++ optimization function-pointers


【解决方案1】:

不太可能有帮助。函数指针会导致与条件相同的停顿*。如今,两者都有预测器,但条件预测器的成功率更高。不足为奇,因为他们只需要预测一点。

* 停顿:指令处理的延迟,由现代 CPU 中指令解码器的流水线特性引起。指令解码在实际执行之前开始几个周期,但本质上是关于未来将执行哪条指令的推测。如果这种推测是错误的,则必须丢弃许多已解码的指令,并延迟指令执行,直到解码器赶上。

【讨论】:

  • 用一个计算的跳转替换一个 single 条件分支不太可能有帮助。如果错误预测的分支的平均数量足够高(在许多平台上,如果平均错误预测数小于 1,如果大于 1,则不太可能造成太大伤害)。
【解决方案2】:

正如 MSalters 已经非常粗暴地指出的那样,它不太可能有帮助。此外,几乎无法预测它是否有帮助。我为您创建了两个示例,您可以在其中看到 - 一个使用指针数组,第二个使用 if-else-clause。

Example 1

#include <array>
#include <functional>



int test1(int* i)
{
    return *i * *i  * *i;
}

int test2(int* i)
{
    return *i * *i* *i* *i* *i* *i;
}

std::array<int (*)(int*), 2> funcs{test1, test2};

int testing(int v)
{
    int testme = v + 4 ;
    return funcs[v](&testme);
}

及其组件

test1(int*):                             # @test1(int*)
    movl    (%rdi), %ecx
    movl    %ecx, %eax
    imull   %ecx, %eax
    imull   %ecx, %eax
    retq
test2(int*):                             # @test2(int*)
    movl    (%rdi), %ecx
    movl    %ecx, %eax
    imull   %ecx, %eax
    imull   %ecx, %eax
    imull   %eax, %eax
    retq
testing(int):                            # @testing(int)
    pushq   %rax
    leal    4(%rdi), %eax
    movl    %eax, 4(%rsp)
    movslq  %edi, %rax
    leaq    4(%rsp), %rdi
    callq   *funcs(,%rax,8)
    popq    %rcx
    retq
funcs:
    .quad   test1(int*)
    .quad   test2(int*)

这里是Example2

int test1(int* i)
{
    return *i * *i  * *i;
}

int test2(int* i)
{
    return *i * *i* *i* *i* *i* *i;
}


int testing(int v)
{
    int testme = v + 4 ;
    return (v == 0) ? test1(&testme) : test2(&testme);
}

与其对应的程序集

test1(int*):                             # @test1(int*)
    movl    (%rdi), %ecx
    movl    %ecx, %eax
    imull   %ecx, %eax
    imull   %ecx, %eax
    retq
test2(int*):                             # @test2(int*)
    movl    (%rdi), %ecx
    movl    %ecx, %eax
    imull   %ecx, %eax
    imull   %ecx, %eax
    imull   %eax, %eax
    retq
testing(int):                            # @testing(int)
    leal    4(%rdi), %eax
    movl    %eax, %ecx
    imull   %eax, %ecx
    imull   %eax, %ecx
    testl   %edi, %edi
    movl    $1, %eax
    cmovnel %ecx, %eax
    imull   %ecx, %eax
    retq

在指令计数方面它们非常相似(也许专家可以在这里帮助我们并进一步解释)。 if-else-statement 中断了这条指令cmovnel。我怀疑它会产生很大的不同。

我认为您将始终必须在您的确切设置中进行测量。在示例情况下,编译器优化总是难以预测。

这里不可能有真假答案

【讨论】:

  • 您似乎两次都使用了三元运算符。
  • @me' 多哈。愚蠢地复制过去。这也改变了答案的其余部分!感谢您指出这个问题。
  • 在第二种情况下,它完全内联 test1/test2(并且test2test1 * test1)...
  • 是的,这些该死的编译器在编写示例时总是比我聪明。这就是为什么总是对你的真实代码进行基准测试是一个好建议的主要原因;)
【解决方案3】:

假设您的numberunsigned int 类型,您的数组将必须包含UINT_MAX 条目,除了function[0] 之外所有条目都相同。如果不是这种情况,您的第一个代码 (function[number])(); 会省略一个条件不是未定义的行为:(function[number != 0])();

总结一下:#1 是(正确写成)一个条件和两个间接。巨型阵列的另一种选择将破坏缓存局部性。 #2 是一个条件和一个直接的函数调用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-11-12
    • 1970-01-01
    • 1970-01-01
    • 2015-08-09
    • 2012-08-17
    • 1970-01-01
    • 2016-03-20
    相关资源
    最近更新 更多