当调用绑定方法this->UpdateB = std::bind(&Test::Update, this);  (使用 using 调用test.UpdateB())时,其整体性能比直接调用函数要慢得多。test.Update()
性能下降也会影响该方法中完成的工作。
使用站点快速工作台,我运行下面的代码片段并得到以下结果
#include <functional>
#include <benchmark/benchmark.h>
typedef unsigned u32;    
typedef uint64_t u64;       
constexpr auto nP = nullptr;    
constexpr bool _F = false;      
constexpr bool _T = true;       
constexpr u64 HIGH_LOAD = 1000000000;
constexpr u64 LOW_LOAD = 10;
struct Test {
    u32 counter{100000};
    u64 soak{0};
    u64 load{10};
    bool isAlive{_T};
    std::function<bool()> UpdateB;
    Test() { UpdateB = std::bind( &Test::Update, this); }
    bool Update() {
        if (counter > 0) { counter --; }
        u64 i = load;
        while(i--) { soak += 1; }
        isAlive = counter > 0;
        return isAlive;
    }    
};
static void DirectCallLowLoad(benchmark::State& state) {
  Test test;  
  test.load = LOW_LOAD;
  for (auto _ : state) { test.Update(); }
}
BENCHMARK(DirectCallLowLoad);
static void DirectCallHighLoad(benchmark::State& state) {
  Test test;  
  test.load = HIGH_LOAD;
  for (auto _ : state) { test.Update(); }
}
BENCHMARK(DirectCallHighLoad);
static void BoundCallLowLoad(benchmark::State& state) {
  Test test;   
  test.load = LOW_LOAD;
  for (auto _ : state) { test.UpdateB();  }
}
BENCHMARK(BoundCallLowLoad);
static void BoundCallHighLoad(benchmark::State& state) {
  Test test;   
  test.load = HIGH_LOAD;
  for (auto _ : state) { test.UpdateB(); }
}
BENCHMARK(BoundCallHighLoad);
期望是...
BoundCallHighLoadDirectCallHighLoad
与方法的 load 相比,调用开销的影响较小,因此性能会更接近。
DirectCallLowLoad性能将明显优于DirectCallHighLoad(绑定调用相同。)
绑定调用不会比直接调用慢近 5 倍。
我的代码有什么问题吗?
为什么绑定调用这么慢?
如果我使用
    std::function<bool(Test*)> UpdateB;
    Test() { UpdateB = &Test::Update;  }  // Test constructor
     
    // call using
    test.UpdateB(&test);
情况变得更糟,调用test.UpdateB(&test);比直接调用慢很多数量级test.Update(),处理负载几乎没有区别。
首先,有一个“Show Noop bar”可以看到一条noop指令的开销(理论上1个周期)。结果如下:
因此,我们可以清楚地看到 DirectCallLowLoad 和 DirectCallHightLoad 已被优化掉,并且基准测试存在偏差。事实上,CPU 在大约 2 个周期内执行 100 亿次迭代是不可能的。事实上,甚至不可能进行 10 次迭代。同样的事情也适用于另外两个,尽管有额外的开销。
这段代码之所以被优化,是因为 Clang 能够知道重复soak += 1; load次数相当于添加load到soak. 事实上,它可以执行更高级的数学运算,例如1+2+3+...+N = N*(N-1)/2. 欺骗编译器并不容易,但一种解决方案是计算数学上证明很难的东西,例如计算冰雹序列。如果编译器能够对此进行优化,那么它将能够证明尚未证明的 Collatz 猜想。请注意,如果编译器无法知道初始值(这应该是 QuickBench 循环状态的目的),那就更好了。另请注意,任何随机数一般也可能完成这项工作。
UpdateBUpdate由于额外的运行时间接性,速度较慢。虽然编译器理论上可以通过专门化来优化此类代码,但默认情况下它们通常不会优化,因为它太昂贵了。事实上,在这个特定的基准测试中,如果没有任何帮助,他们几乎无法做到这一点,因为 QuickBench 循环状态应该阻止编译器对其进行优化。减少这种开销的一种解决方案是使用配置文件引导优化,以便编译器可以根据实践中的实际函数集进行推测性优化。话虽如此,由于循环状态,在此 QuickBench 中它仍然不够。
请注意,您可以直接在 QuickBench 中查看汇编代码,并且可以看到例如前两种情况的代码是内联的。这是第一个工作台的主循环:
       mov    %edi,0x8(%rsp)
3.26%  add    $0xfffffffffffffffc,%r12
       je     2131ca <DirectCallLowLoad(benchmark::State&)+0x13a>
       mov    %eax,%esi
0.50%  mov    %eax,%edi
5.56%  sub    $0x1,%edi
4.10%  cmovb  %r8d,%edi
       mov    %edi,%eax
0.44%  sub    $0x1,%eax
9.14%  cmovae %eax,%edi
8.73%  setb   %dl
2.70%  cmovb  %r8d,%eax
8.17%  sub    $0x1,%eax
7.46%  cmovae %eax,%edi
10.13% cmovb  %r8d,%eax
1.43%  setb   %cl
7.89%  sub    $0x1,%eax
1.80%  cmovae %eax,%edi
9.42%  setb   %bl
2.30%  cmovb  %r8d,%eax
8.42%  cmp    $0x1,%esi
       jae    213220 <DirectCallLowLoad(benchmark::State&)+0x190>
       test   %dl,%dl
       je     213220 <DirectCallLowLoad(benchmark::State&)+0x190>
1.24%  test   %cl,%cl
       je     213220 <DirectCallLowLoad(benchmark::State&)+0x190>
7.27%  test   %bl,%bl
0.03%  jne    213224 <DirectCallLowLoad(benchmark::State&)+0x194>
       jmp    213220 <DirectCallLowLoad(benchmark::State&)+0x190>
这是第三个(没有内联和间接):
99.73% cmpq   $0x0,0x38(%rsp)
       je     213681 <BoundCallLowLoad(benchmark::State&)+0xd1>
       mov    %r15,%rdi
       call   *0x40(%rsp)
0.27%  add    $0xffffffffffffffff,%rbx
       jne    213640 <BoundCallLowLoad(benchmark::State&)+0x90>
我们可以看到,编译器生成的代码首先检查值std::function(即rsp)是否有效(即空函数指针),然后在rbx为基准测试循环的每次迭代减少迭代计数器 ( ) 之前调用它。
至于内循环优化,代码Test::Update在 QuickBench 中看不到,但可以在GodBolt中看到。
Test::Update():                      # @Test::Update()
        mov     eax, dword ptr [rdi]
        test    eax, eax
        je      .LBB5_1
        add     eax, -1
        mov     dword ptr [rdi], eax
        mov     rcx, qword ptr [rdi + 16]
        test    rcx, rcx
        je      .LBB5_5
.LBB5_4:
        add     qword ptr [rdi + 8], rcx
.LBB5_5:
        test    eax, eax
        setne   al
        setne   byte ptr [rdi + 24]
        ret
.LBB5_1:
        xor     eax, eax
        mov     rcx, qword ptr [rdi + 16]
        test    rcx, rcx
        jne     .LBB5_4
        jmp     .LBB5_5
代码的重点是指令add     qword ptr [rdi + 8], rcx,基本上与C++中的指令相同soak += load;。
| 归档时间: | 
 | 
| 查看次数: | 234 次 | 
| 最近记录: |