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

时间:2019-02-01 18:15:36

标签: performance assembly visual-c++ intel cpu-cache

更新:最小示例,展示了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的缓存中不能很好地运行,但是我无法弄清楚为什么包含一条移动指令会导致这种情况?

1 个答案:

答案 0 :(得分:2)

如果您注释了其中任一行,则可能是重复存储到同一地址(并且可能在循环中进行了优化),或者根本没有存储。时间短得不可思议,并且舍入到0微秒也就不足为奇了。

在您链接的测试代码中,您要在没有预热的情况下在新分配的内存上每个存储大步前进32kiB 。您可能会在每次迭代中遇到软页面错误和写时复制。 (malloc版的内存可能全部懒洋洋地映射到了相同的物理零页。)

256次迭代也完全不足以将CPU提升到正常/ turbo时钟速度,超出空闲速度。

在我的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次。 (“速率”辅助统计信息列中存在伪造的单位,原因是Arch尚未更新此错误,因此该漏洞的测量结果为4.4GHz(我认为这是伪造的,在我的CPU上禁用了Turbo,以保持风扇安静)

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% )

这些计数包括内核模式,但是这是191个页面错误,可进行256次循环迭代,因此该程序所花费的大部分时间都在内核中。 < / p>

一旦回到用户空间,就会有超过1000家商店导致dTLB丢失,而第二级TLB中也丢失了dTLB,这需要进行页面遍历。但是没有负载。

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