如何优化热循环中的内存写入
Posted
技术标签:
【中文标题】如何优化热循环中的内存写入【英文标题】:How to optimize for writes to memory in hot loop 【发布时间】:2021-06-17 14:52:53 【问题描述】:我的代码中有一个循环,我花费了大部分 CPU 时间:
%%write_to_intermediate_head:
; uint64_t count_index = (arr[i] >> (8 * cur_digit)) & (RADIX - 1);
; We will use rsi to hold the digit we're currently examining
mov rsi, [rdx + rdi * 8] ; Load arr[i] into rsi
mov r9, %1 ; Load current digit were sorting on
shl r9, 3 ; Multiply by 8
shrx rsi, rsi, r9
and rsi, 255
; intermediate_arr[--digit_counts[count_index]] = arr[i];
; rdi is the loop counter i
; rsi is the count_index
; rcx is the digit_counts array
; rax is the intermediate_arr
dec qword [rcx + rsi * 8]
mov r9, [rcx + rsi * 8] ; --digit_counts[count_index]
mov rsi, [rdx + rdi * 8] ; arr[i]
mov [rax + r9 * 8], rsi
dec rdi
jnz %%write_to_intermediate_head
变量:digit_counts、arr 和intermediate_arr 都在内存中(堆和bss)。 AMD 分析器显示读取和写入这些内存位置花费了许多周期。有什么办法可以优化吗?
【问题讨论】:
用 C 语言编写并使用优化编译器? 【参考方案1】:您的计数是否真的需要是 qwords,或者您是否可以使用更窄的类型将 32 位的缓存占用量减少一半(或者更窄的甚至更少)?如果您遇到缓存未命中,如果 OoO exec 无法隐藏该延迟,这将意味着等待加载/存储的时间要多得多。
我猜想复制数据将是大部分内存带宽/缓存未命中。这看起来像基数排序,与数据相比,要管理的元数据量很小。 (但至少让它在缓存中命中会有所帮助,使其更能抵抗你丢弃的所有其他数据的驱逐。)
不管你做什么,基数排序的访问模式本质上对缓存不太友好,尽管它并不可怕。您将写入分散到 256 个可能的目的地,同时更新指针。但是这 256 个目的地是顺序流,所以如果幸运的话,它们可以在 L1d 缓存中命中。
希望这些目的地不是 4k 的倍数(最初或大部分时间),否则它们会在 L1d 缓存中为同一行设置别名并导致冲突未命中。 (即强制驱逐您即将写入的另一个部分写入的缓存行。)
您有一些冗余的加载/存储,这可能是加载/存储执行单元的瓶颈,但如果这不是瓶颈,那么缓存会很好地吸收它们。本节主要是关于调整循环以使用更少的微指令、改进无缓存未命中的最佳情况以及减少 OoO exec 的隐藏延迟。
使用内存目标dec
,然后重新加载dec
的存储在后端加载/存储操作的总和以及 OoO exec 隐藏的延迟方面显然效率低下。 (尽管在 AMD 上,dec mem
仍然是前端的单个 uop,而在 Intel 上是 3;https://uops.info/ 和 https://agner.org/optimize/)。
同样,您不是使用相同的 RDI 两次加载 [rdx + rdi * 8] ; arr[i]
吗? SHRX 可以复制和移动,因此您甚至不会通过保留该加载结果以供以后节省微指令。 (您还可以对arr[i]
使用简单的非索引寻址模式,通过像add rdi,8
和cmp rdi, endp
/jne
这样的指针增量,其中end 是您之前使用lea endp, [rdx + size*8]
计算的结果。循环forward 对于某些硬件预取器来说,数组上的转发可能会更好。)
x86-64 有 15 个寄存器,因此如果您需要更多用于此内部循环,请在函数的顶部/底部保存/恢复一些保留调用的寄存器(如 RBX 或 RBP)。或者在必要时将一些外循环的东西溢出到内存中。
mov r9, %1
看起来是循环不变的,因此将 shl r9, 3
计算提升到循环之外,并且不要覆盖内部循环内的 R9。
您确实需要对提取的字节进行零扩展,但and rsi, 255
的效率不如movzx eax, sil
。 (或者更好的是,选择像 ECX 这样的寄存器,其低字节可以在没有 REX 前缀的情况下访问)。不过,AMD 无法像英特尔那样在 movzx 上进行 mov-elimination,因此只需为 AMD 节省代码大小,但如果您在英特尔 CPU 上运行它,则会优化延迟。
或者更好,AMD Zen 有单微指令BMI1 bextr r64,r64,r64
,所以在寄存器的低 2 字节中准备一个开始/长度对。如前所述,这是循环不变的。即在循环之前mov ecx, %k1
/shl cl, 3
/mov ch, 0x8
(AMD 没有部分寄存器停顿,只是错误的依赖关系。在这种情况下是真的,因为我们想要合并。)如果那是内联 asm 语法,@987654345 @ 指定寄存器的 32 位版本。或者如果它是内存,无论如何你只是在加载,提升它会节省另一个负载!
(英特尔有 2-uop bextr
,大概是 shift 和 bzhi uop。)
或者如果你真的想加载两次,movzx esi, byte [rdi + rdx]
其中 RDI 是指向 arr[[i]
的指针,你可以递增或递减,RDX 是字节偏移量。但可能 BEXTR 是更好的选择。
【讨论】:
@lvrf:你也许可以检查你的数字计数和特殊情况的包装。就像在dec
之后包含jnz
,或者如果您使用字节偏移而不是计数,则包含jnc
;我不确定 AMD 在内存访问的寻址模式(不是 LEA)中是否关心比例因子是否为 1。但是使用 perf record 或其他任何东西来查看这些访问是否缓存未命中。例如英特尔有一个性能事件mem_load_retired.l1_miss
映射到特定指令; AMD 可能也有类似的东西。
@lvrf:是的,LEA 可以替换 mov/shl。它花费了几个字节的额外代码大小,但确实节省了 uop。 ([reg * 8]
必须编码为[reg*8 + disp32=0]
,因为 no-base-reg 编码是 ModRM/SIB 表示只有位移的方式)。 AMD 的延迟实际上可能更差,但在循环之外很好(缩放索引)。当然,如果您知道它是一个小整数(未设置高位),rorx eax, %k1, 32-3
也会进行复制和移位。 (不需要 64 位操作数大小,除了 LEA 寻址模式:x86-64 不带前缀字节的默认值是 op size = 32,addr size = 64。)
我已经将 arr[i]
的冗余负载移除到 rsi 中(我对此视而不见),我现在(平均而言)击败了我编写的 O2 优化 C 版本。确认一下,dec qword [rcx + rsi * 8]
/ mov r10, [rcx + rsi * 8]
比 mov r10, [rcx + r11 * 8]
/ dec r10
/ mov [rcx + r11 * 8], r10
(加载、十进制、存储)之类的慢。
@lvrf:摆脱 32 位计数的另一个想法:可能有两个版本的循环,如果要排序的总输入数组大小小于 2^32,那么你可以使用 32 位计数器版本。或者,如果要进一步排序的任何给定存储桶足够小,那么它可以使用 32 位计数器。
@lvrf: 资源:Agner Fog 的优化指南(尤其是微架构指南)是必不可少的阅读材料。 agner.org/optimize。另请参阅lighterra.com/papers/modernmicroprocessors re 管道和 OoO exec,以及 ROB / 存储缓冲区内容,以及 realworldtech.com/sandy-bridge re:特定微架构。 (不幸的是,大卫坎特没有发表过类似的禅宗深入探讨,但请参阅en.wikichip.org/wiki/amd/microarchitectures/zen_2#Architecture)。【参考方案2】:
其他优化。可以使用矩阵而不是数组同时为所有数字完成生成计数的初始传递。对于 64 位无符号整数,使用 1 字节数字执行 8 次传递足够接近理想值,因为计数 |索引将适合 L1 缓存。初始通道将计数存储在 [8][256] 矩阵中,32 位计数|索引应该足够了。
对于比缓存大得多的大数组,如果要排序的数据相当均匀,那么第一个基数排序过程可以是一个最高有效位通道,如果使用 1 字节数字,则创建 256 个 bin,目标是每个缓存中的 256 个 bin 中的一个,并在 256 个 bin 中的每一个上首先对最低有效位进行基数排序,一次一个 bin。如果数组更大,第一遍可以创建更多的 bin,512(9 位数字),1024(10 位数字),...,那么每个 bin 仍然可以使用 1 字节数字排序,最后一个数字较小通过。
【讨论】:
以上是关于如何优化热循环中的内存写入的主要内容,如果未能解决你的问题,请参考以下文章