由指针分配或增量(严格别名?)引起的性能增量

Performance delta caused by pointer assignment or increment (strict aliasing?)

更新: 演示 Clang 7.0 中问题的最小示例 - https://wandbox.org/permlink/G5NFe8ooSKg29ZuS
https://godbolt.org/z/PEWiRk

基于 256 次迭代(Visual Studio 2017)的方法,我正在经历从 0μs 到 500-900μs 的函数性能变化:

void* SomeMethod()
{
    void *result = _ptr; // _ptr is from malloc

    // Increment original pointer
    _ptr = static_cast<uint8_t*>(_ptr) + 32776;    // (1)

    // Set the back pointer
    *static_cast<ThisClass**>(result) = this;      // (2)

    return result;
}

如果我注释行 (1) 或 (2),则该方法的时间为 0μs。包含这两条线导致每个函数调用的时间在 2μs 和 4μs 之间。

我不相信我违反了严格的别名规则,当通过 CompilerExplorer 观察时,我可以看到设置后向指针(第 (2) 行)仅生成一条指令:

mov QWORD PTR [rax], rcx

这让我想知道它是否可能是导致编译器无法优化的严格别名,而唯一的影响似乎是第 1 行代码的 1 条额外指令。

供参考,递增原始指针(第 (1) 行)生成两条指令:

lea     rdx, QWORD PTR [rax+32776]
mov     QWORD PTR [rcx], rdx

为了完整起见,这里是完整的汇编输出:

mov     rax, QWORD PTR [rcx]
lea     rdx, QWORD PTR [rax+32776]
mov     QWORD PTR [rcx], rdx
mov     QWORD PTR [rax], rcx
ret     0

导致性能差异的原因可能是什么?我现在的假设是代码在 CPU 的缓存中表现不佳,但我无法理解为什么包含一条移动指令会导致这种情况?

如果您对这两行中的任何一行进行注释,您要么重复存储到同一地址(并且可能在循环中优化),要么根本不存储。毫不奇怪,时间短得无法测量,四舍五入到 0 微秒。

在您链接的测试代码中,您在每个存储区跨越 32kiB,在新分配的内存 上,没有预热。您可能会在每次迭代时遇到软页面错误和写时复制。 (malloced 内存可能全部延迟映射到相同的物理零页。)

256 次迭代也完全不足以将 CPU 提高到正常/涡轮时钟速度,超出怠速。

在我的 i7-6700k Arch Linux destkop(空闲 800MHz,正常时钟速度 3.9GHz,调速器 /energy_performance_preference = balance_performance(不是默认的 balance_power,所以它上升得更快):

我用 gcc8.2.1 编译,运行 生成的可执行文件在 while ./a.out ;do :;done 循环中,因此 CPU 将保持高时钟速度。该程序打印的时间有点像 1.125us +-。这听起来可能适合页面错误 + 将页面清零 + 更新页表和刷新 TLB。


使用 Linux perf stat,我 运行 计算了 100 次平均计数。 ("rate" 次要统计列有伪造的单位,因为 Arch 还没有更新修复的性能错误。所以它实际上测量的是 4.4GHz(我认为这是伪造的,Turbo 在我的 CPU 让粉丝安静。)

peter@volta:/tmp$ perf stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,instructions,uops_issued.any,uops_executed.thread,dtlb_store_misses.miss_causes_a_walk,tlb_flush.dtlb_thread,dtlb_load_misses.miss_causes_a_walk -r100 ./a.out


 Performance counter stats for './a.out' (100 runs):

              1.15 msec task-clock                #    0.889 CPUs utilized            ( +-  0.33% )
                 0      context-switches          #   40.000 M/sec                    ( +- 49.24% )
                 0      cpu-migrations            #    0.000 K/sec                  
               191      page-faults               # 191250.000 M/sec                  ( +-  0.09% )
         4,343,915      cycles                    # 4343915.040 GHz                   ( +-  0.33% )  (82.06%)
           819,685      branches                  # 819685480.000 M/sec               ( +-  0.05% )
         4,581,597      instructions              #    1.05  insn per cycle           ( +-  0.05% )
         6,366,610      uops_issued.any           # 6366610010.000 M/sec              ( +-  0.05% )
         6,287,015      uops_executed.thread      # 6287015440.000 M/sec              ( +-  0.05% )
             1,271      dtlb_store_misses.miss_causes_a_walk # 1270910.000 M/sec                 ( +-  0.21% )
     <not counted>      tlb_flush.dtlb_thread                                         (0.00%)
     <not counted>      dtlb_load_misses.miss_causes_a_walk                                     (0.00%)

        0.00129289 +- 0.00000489 seconds time elapsed  ( +-  0.38% )

这些计数包括内核模式,但是 这是 256 次循环迭代的 191 个页面错误,因此 大量 此程序花费的大部分时间都在内核.

并且一旦我们返回用户 space,超过 1000 个存储导致 dTLB 未命中,而二级 TLB 中也未命中,需要页面遍历。但是没有负载。

我们可能可以通过分配更多的内存来获得更清晰的数据,这样我们就可以增加 Count 而不会出现段错误。使用 perf record 进行的分析表明,只有大约 20% 的程序总时间花在了 main 上;其余的是动态链接器/启动开销,部分来自打印。