更新:最小示例,展示了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 :(得分: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
中;其余部分在动态链接器/启动开销中,部分来自打印。