由指针分配或增量(严格别名?)引起的性能增量
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,在新分配的内存 上,没有预热。您可能会在每次迭代时遇到软页面错误和写时复制。 (malloc
ed 内存可能全部延迟映射到相同的物理零页。)
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
上;其余的是动态链接器/启动开销,部分来自打印。
更新: 演示 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,在新分配的内存 上,没有预热。您可能会在每次迭代时遇到软页面错误和写时复制。 (malloc
ed 内存可能全部延迟映射到相同的物理零页。)
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
上;其余的是动态链接器/启动开销,部分来自打印。