您真的在 x86-64 模式下的 VGA 文本模式视频内存上使用 SIMD 吗?这很有趣,但在现实生活中实际上是合理的,并且可以作为一些 SIMD 数据操作的用例。
但是,如果您真的是从视频内存中读取数据,那么您可能正在执行未缓存的加载,这很糟糕,并且意味着您应该重新设计您的系统,这样您就不必这样做了。(有关建议,请参阅罗斯的回答)
在 USWC 视频内存上,您可以从 MOVNTDQA 获得很大的加速。请参阅英特尔的文章,以及我对 NT 负载的一些回答:在这里,尤其是在这篇文章中,我解释了 x86 ISA 手册中关于 NT 负载不覆盖内存排序语义的说法,因此它们不是弱排序的,除非你使用它们弱有序的内存区域。
正如您所怀疑的,您不会在 SIMD 指令集中找到复制指令;您必须在加载和存储之间的寄存器中自己进行数据处理。甚至没有一条 SSE/AVX 指令可以为您执行此操作。(不过,ARM NEON 的解压缩指令确实解决了整个问题)。
您应该使用 SSE2 PACKUSWB将两个(带符号的)int16_t 向量压缩成一个 uint8_t 向量。将每个字元素的高字节归零后,饱和到 0..255 根本不会修改您的数据。
这是一个真实的(未经测试的)循环,它对齐源指针以最小化跨越高速缓存行边界的惩罚,并使用一些寻址模式技巧来保存循环中的指令。
未对齐的负载对 Nehalem 和以后的负载几乎没有影响,主要是当它们越过缓存线边界时会产生额外的延迟。因此,如果您想使用来自视频内存的 NT 加载,这将非常有用。或者,如果您要在大副本末尾的 src 末尾之外阅读,这可能会很有用。
我们的加载次数是存储的两倍,因此如果加载/存储吞吐量是一个问题,对齐加载(而不是对齐存储)可能是最佳的。但是,有太多的 ALU 工作会导致缓存加载/存储吞吐量饱和,因此使用未对齐的加载(如 Paul R 的循环)保持简单应该在大多数 CPU 和用例上都能很好地工作。
mov edx, CMD_BUFFER ; or RIP-relative LEA, or hopefully this isn't even static in the first place and this instruction is something else
;; rdi = source ; yes this is "backwards", but if you already have the src pointer in rdi, don't waste instructions
;; rcx = count
;; rdx = dest
pcmpeqw xmm7, xmm7 ; all ones (0xFF repeating)
psrlw xmm7, 8 ; 0x00FF repeating: mask for zeroing the high bytes
;cmp ecx, 16
;jb fallback_loop ; just make CMD_BUFFER big enough that it's ok to copy 16 bytes when you only wanted 1. Assuming the src is also padded at the end so you can read without faulting.
;; First potentially-unaligned 32B of source data
;; After this, we only read 32B chunks of 32B-aligned source that contain at least one valid byte, and thus can't segfault at the end.
movdqu xmm0, [rdi] ; only diff from loop body: addressing mode and unaligned loads
movdqu xmm1, [rdi + 16]
pand xmm0, xmm7
pand xmm1, xmm7
packuswb xmm0, xmm1
movdqu [rdx], xmm0
;; advance pointers just to the next src alignment boundary. src may have different alignment than dst, so we can't just AND both of them
;; We can only use aligned loads for the src if it was at least word-aligned on entry, but that should be safe to assume.
;; There's probably a way to do this in fewer instructions.
mov eax, edi
add rdi, 32 ; advance 32B
and rdi, -32 ; and round back to an alignment boundary
sub eax, edi ; how far rdi actually advanced
shr eax, 1
add rdx, rax ; advance dst by half that.
;; if rdi was aligned on entry, the it advances by 32 and rdx advances by 16. If it's guaranteed to always be aligned by 32, then simplify the code by removing this peeled unaligned iteration!
;; if not, the first aligned loop iteration will overlap some of the unaligned loads/store, but that's fine.
;; TODO: fold the above calculations into this other loop setup
lea rax, [rdx + rdx]
sub rdi, rax ; source = [rdi + 2*rdx], so we can just increment our dst pointer.
lea rax, [rdx + rcx] ; rax = end pointer. Assumes ecx was already zero-extended to 64-bit
; jmp .loop_entry ; another way to check if we're already done
; Without it, we don't check for loop exit until we've already copied 64B of input to 32B of output.
; If small inputs are common, checking after the first unaligned vectors does make sense, unless leaving it out makes the branch more predictable. (All sizes up to 32B have identical branch-not-taken behaviour).
ALIGN 16
.pack_loop:
; Use SSE4.1 movntdqa if reading from video RAM or other UCSW memory region
movdqa xmm0, [rdi + 2*rdx] ; indexed addressing mode is ok: doesn't need to micro-fuse because loads are already a single uop
movdqa xmm1, [rdi + 2*rdx + 16] ; these could optionally be movntdqa loads, since we got any unaligned source data out of the way.
pand xmm0, xmm7
pand xmm1, xmm7
packuswb xmm0, xmm1
movdqa [rdx], xmm0 ; non-indexed addressing mode: can micro-fuse
add rdx, 16
.loop_entry:
cmp rdx, rax
jb .pack_loop ; exactly 8 uops: should run at 1 iteration per 2 clocks
;; copies up to 15 bytes beyond the requested amount, depending on source alignment.
ret
使用 AVX 的非破坏性第三操作数编码,负载可以折叠到 PAND ( vpand xmm0, xmm7, [rdi + 2*rdx]
) 中。但是索引寻址模式至少不能在某些 SnB 系列 CPU 上进行微融合,因此您可能希望展开,add rdi, 32
而add rdx, 16
不是使用相对于目标寻址源的技巧。
对于 2xload+and/pack/store,AVX 会将循环体减少到 4 个融合域微指令,加上循环开销。通过展开,我们可以开始接近英特尔 Haswell 的理论最大吞吐量,即每个时钟 2 个负载 + 1 个存储(尽管它无法维持这一点;存储地址微指令有时会窃取 p23 周期而不是使用 p7。英特尔的优化手册提供了一个真实的- 假设所有 L1 缓存命中率低于 96B 峰值吞吐量,则世界可持续吞吐量数,例如每时钟加载和存储约 84B(使用 32 字节向量)。)
您还可以使用字节混洗 ( SSSE3 PSHUFB ) 将向量的偶数字节打包到低 64 位中。(然后为每个 128 位加载执行一个 64 位 MOVQ 存储,或将两个下半部分与 PUNPCKLQDQ 组合)。但这很糟糕,因为(每个 128 位源数据向量)它是 2 次随机播放 + 2 次存储,或 3 次随机播放 + 1 次存储。您可以通过使用不同的洗牌掩码使合并更便宜,例如将偶数字节洗牌到一个向量的低半部分和另一个向量的上半部分。由于 PSHUFB 还可以免费将任何字节归零,因此您可以与 POR 结合使用(而不是稍微昂贵的 PBLENDW 或 AVX2 VPBLENDD)。这是 2 次洗牌 + 1 个布尔值 + 1 次存储,仍然是洗牌的瓶颈。
PACKUSWB 方法是 2 个布尔运算 + 1 个 shuffle + 1 个存储(瓶颈较少,因为 PAND 可以在更多执行端口上运行;例如,每个时钟 3 个,而每个时钟 1 个用于 shuffle)。
AVX512BW(在Skylake-avx512 上可用,但在 KNL 上不可用)提供
VPMOVWB ymm1/m256 {k1}{z}, zmm2
( __m256i _mm512_cvtepi16_epi8 (__m512i a)
),它使用截断而不是饱和打包。与 SSE 打包指令不同,它只需要 1 个输入并产生更窄的结果(可以是内存目标)。(vpmovswb
并且相似,并带有有符号或无符号饱和度。所有可用vpmovuswb
的相同大小的组合,例如,因此您不需要多个步骤。Q 和 D 源大小在 AVX512F 中)。pmovzx
vpmovqb xmm1/m64 {k1}{z}, zmm2
memory-dest 功能甚至通过 C/C++ 内在函数公开,从而可以方便地在 C 中编写掩码存储。(这是一个很好的变化,从pmovzx
不方便使用内在函数并使编译器发出pmovzx
负载的地方)。
AVX512VBMI(预计在英特尔 Cannonlake 中使用)可以使用一个 VPERMT2B 对一个 512b 输出进行两个输入,给定一个随机掩码,该掩码从两个输入向量中获取偶数字节并产生一个结果向量。
如果 VPERM2TB 比 VPMOVWB 慢,那么一次对一个向量使用 VPMOVWB 可能是最好的。即使它们具有相同的吞吐量/延迟/uop-count,增益也可能非常小,以至于不值得制作另一个版本并检测 AVX512VBMI 而不是 AVX512BW。(CPU 不可能在没有 AVX512BW 的情况下拥有 AVX512VBMI,尽管这是可能的)。