128 位值 - 从 XMM 寄存器到通用
128-bit values - From XMM registers to General Purpose
我有几个关于将 XMM 值移动到通用寄存器的问题。 SO上查到的所有问题都集中在相反的方向,即将gp寄存器中的值传输到XMM。
如何将 XMM 寄存器值(128 位)移动到两个 64 位通用寄存器?
movq RAX XMM1 ; 0th bit to 63th bit
mov? RCX XMM1 ; 64th bit to 127th bit
同样,如何将 XMM 寄存器值(128 位)移动到四个 32 位通用寄存器?
movd EAX XMM1 ; 0th bit to 31th bit
mov? ECX XMM1 ; 32th bit to 63th bit
mov? EDX XMM1 ; 64th bit to 95th bit
mov? ESI XMM1 ; 96th bit to 127 bit
您不能将 XMM 寄存器的高位直接移动到通用寄存器中。
您必须遵循一个两步过程,这可能涉及也可能不涉及内存往返或寄存器销毁。
在寄存器中 (SSE2)
movq rax,xmm0 ;lower 64 bits
movhlps xmm0,xmm0 ;move high 64 bits to low 64 bits.
movq rbx,xmm0 ;high 64 bits.
punpckhqdq xmm0,xmm0
is the SSE2 integer equivalent of movhlps xmm0,xmm0
。如果 xmm0 最后由整数指令而非 FP 写入,某些 CPU 可能会避免一两个周期的旁路延迟。
通过内存 (SSE2)
movdqu [mem],xmm0
mov rax,[mem]
mov rbx,[mem+8]
慢,但不破坏xmm寄存器(SSE4.1)
mov rax,xmm0
pextrq rbx,xmm0,1 ;3 cycle latency on Ryzen! (and 2 uops)
混合策略是可能的,例如存储到内存,movd/q e/rax,xmm0
所以它很快就准备好了,然后重新加载更高的元素。 (不过,存储转发延迟并不比 ALU 差多少。)这为不同的后端执行单元提供了微指令的平衡。 Store/reload 当你想要很多小元素时特别好。 (mov
/ movzx
加载到 32 位寄存器的成本很低,并且具有 2/时钟吞吐量。)
对于32位,代码类似:
在寄存器中
movd eax,xmm0
psrldq xmm0,xmm0,4 ;shift 4 bytes to the right
movd ebx,xmm0
psrldq xmm0,xmm0,4 ; pshufd could copy-and-shuffle the original reg
movd ecx,xmm0 ; not destroying the XMM and maybe creating some ILP
psrlq xmm0,xmm0,4
movd edx,xmm0
通过记忆
movdqu [mem],xmm0
mov eax,[mem]
mov ebx,[mem+4]
mov ecx,[mem+8]
mov edx,[mem+12]
不破坏 xmm 寄存器 (SSE4.1)(像 psrldq
/ pshufd
版本一样慢)
movd eax,xmm0
pextrd ebx,xmm0,1 ;3 cycle latency on Skylake!
pextrd ecx,xmm0,2 ;also 2 uops: like a shuffle(port5) + movd(port0)
pextrd edx,xmm0,3
64 位移位变体可以在 2 个周期内 运行。 pextrq
版本最少需要 4 个。对于 32 位,数字分别为 4 和 10。
在 Intel SnB 系列(包括 Skylake)上,shuffle+movq
或 movd
具有与 pextrq
/d
相同的性能。它解码为一个 shuffle uop 和一个 movd
uop,所以这并不奇怪。
在 AMD Ryzen 上,pextrq
的延迟显然比 shuffle + movq
低 1 个周期。根据Agner Fog's tables,pextrd/q
是3c 延迟,movd/q
也是如此。这是一个巧妙的技巧(如果它是准确的),因为 pextrd/q
确实解码为 2 微指令(相对于 movq
的 1)。
由于 shuffle 具有非零延迟,shuffle+movq
在 Ryzen 上总是比 pextrq
严格差(除了可能的前端解码/uop-cache 影响)。
用于提取所有元素的纯 ALU 策略的主要缺点是吞吐量:它需要大量的 ALU 微指令,并且大多数 CPU 只有一个执行单元/端口可以将数据从 XMM 移动到整数。 Store/reload 第一个元素的延迟更高,但吞吐量更高(因为现代 CPU 每个周期可以执行 2 次加载)。如果周围的代码受到 ALU 吞吐量的瓶颈,store/reload 策略可能会很好。也许用 movd
或 movq
做低元素,这样乱序执行就可以开始使用它,而其余的矢量数据正在通过存储转发。
将 32 位元素提取到整数寄存器的另一个值得考虑的选项(除了 Johan 提到的)是使用整数移位进行一些“洗牌”:
mov rax,xmm0
# use eax now, before destroying it
shr rax,32
pextrq rcx,xmm0,1
# use ecx now, before destroying it
shr rcx, 32
shr
可以在 Intel Haswell/Skylake 的 p0 或 p6 上 运行。 p6 没有向量 ALU,所以如果你想要低延迟但又想对向量 ALU 施加低压力,这个序列非常好。
或者如果你想把它们留在身边:
movq rax,xmm0
rorx rbx, rax, 32 # BMI2
# shld rbx, rax, 32 # alternative that has a false dep on rbx
# eax=xmm0[0], ebx=xmm0[1]
pextrq rdx,xmm0,1
mov ecx, edx # the "normal" way, if you don't want rorx or shld
shr rdx, 32
# ecx=xmm0[2], edx=xmm0[3]
以下处理 get 和 set 并且似乎有效(我认为这是 AT&T 语法):
#include <iostream>
int main() {
uint64_t lo1(111111111111L);
uint64_t hi1(222222222222L);
uint64_t lo2, hi2;
asm volatile (
"movq %3, %%xmm0 ; " // set high 64 bits
"pslldq , %%xmm0 ; " // shift left 64 bits
"movsd %2, %%xmm0 ; " // set low 64 bits
// operate on 128 bit register
"movq %%xmm0, %0 ; " // get low 64 bits
"movhlps %%xmm0, %%xmm0 ; " // move high to low
"movq %%xmm0, %1 ; " // get high 64 bits
: "=x"(lo2), "=x"(hi2)
: "x"(lo1), "x"(hi1)
: "%xmm0"
);
std::cout << "lo1: [" << lo1 << "]" << std::endl;
std::cout << "hi1: [" << hi1 << "]" << std::endl;
std::cout << "lo2: [" << lo2 << "]" << std::endl;
std::cout << "hi2: [" << hi2 << "]" << std::endl;
return 0;
}
我有几个关于将 XMM 值移动到通用寄存器的问题。 SO上查到的所有问题都集中在相反的方向,即将gp寄存器中的值传输到XMM。
如何将 XMM 寄存器值(128 位)移动到两个 64 位通用寄存器?
movq RAX XMM1 ; 0th bit to 63th bit mov? RCX XMM1 ; 64th bit to 127th bit
同样,如何将 XMM 寄存器值(128 位)移动到四个 32 位通用寄存器?
movd EAX XMM1 ; 0th bit to 31th bit mov? ECX XMM1 ; 32th bit to 63th bit mov? EDX XMM1 ; 64th bit to 95th bit mov? ESI XMM1 ; 96th bit to 127 bit
您不能将 XMM 寄存器的高位直接移动到通用寄存器中。
您必须遵循一个两步过程,这可能涉及也可能不涉及内存往返或寄存器销毁。
在寄存器中 (SSE2)
movq rax,xmm0 ;lower 64 bits
movhlps xmm0,xmm0 ;move high 64 bits to low 64 bits.
movq rbx,xmm0 ;high 64 bits.
punpckhqdq xmm0,xmm0
is the SSE2 integer equivalent of movhlps xmm0,xmm0
。如果 xmm0 最后由整数指令而非 FP 写入,某些 CPU 可能会避免一两个周期的旁路延迟。
通过内存 (SSE2)
movdqu [mem],xmm0
mov rax,[mem]
mov rbx,[mem+8]
慢,但不破坏xmm寄存器(SSE4.1)
mov rax,xmm0
pextrq rbx,xmm0,1 ;3 cycle latency on Ryzen! (and 2 uops)
混合策略是可能的,例如存储到内存,movd/q e/rax,xmm0
所以它很快就准备好了,然后重新加载更高的元素。 (不过,存储转发延迟并不比 ALU 差多少。)这为不同的后端执行单元提供了微指令的平衡。 Store/reload 当你想要很多小元素时特别好。 (mov
/ movzx
加载到 32 位寄存器的成本很低,并且具有 2/时钟吞吐量。)
对于32位,代码类似:
在寄存器中
movd eax,xmm0
psrldq xmm0,xmm0,4 ;shift 4 bytes to the right
movd ebx,xmm0
psrldq xmm0,xmm0,4 ; pshufd could copy-and-shuffle the original reg
movd ecx,xmm0 ; not destroying the XMM and maybe creating some ILP
psrlq xmm0,xmm0,4
movd edx,xmm0
通过记忆
movdqu [mem],xmm0
mov eax,[mem]
mov ebx,[mem+4]
mov ecx,[mem+8]
mov edx,[mem+12]
不破坏 xmm 寄存器 (SSE4.1)(像 psrldq
/ pshufd
版本一样慢)
movd eax,xmm0
pextrd ebx,xmm0,1 ;3 cycle latency on Skylake!
pextrd ecx,xmm0,2 ;also 2 uops: like a shuffle(port5) + movd(port0)
pextrd edx,xmm0,3
64 位移位变体可以在 2 个周期内 运行。 pextrq
版本最少需要 4 个。对于 32 位,数字分别为 4 和 10。
在 Intel SnB 系列(包括 Skylake)上,shuffle+movq
或 movd
具有与 pextrq
/d
相同的性能。它解码为一个 shuffle uop 和一个 movd
uop,所以这并不奇怪。
在 AMD Ryzen 上,pextrq
的延迟显然比 shuffle + movq
低 1 个周期。根据Agner Fog's tables,pextrd/q
是3c 延迟,movd/q
也是如此。这是一个巧妙的技巧(如果它是准确的),因为 pextrd/q
确实解码为 2 微指令(相对于 movq
的 1)。
由于 shuffle 具有非零延迟,shuffle+movq
在 Ryzen 上总是比 pextrq
严格差(除了可能的前端解码/uop-cache 影响)。
用于提取所有元素的纯 ALU 策略的主要缺点是吞吐量:它需要大量的 ALU 微指令,并且大多数 CPU 只有一个执行单元/端口可以将数据从 XMM 移动到整数。 Store/reload 第一个元素的延迟更高,但吞吐量更高(因为现代 CPU 每个周期可以执行 2 次加载)。如果周围的代码受到 ALU 吞吐量的瓶颈,store/reload 策略可能会很好。也许用 movd
或 movq
做低元素,这样乱序执行就可以开始使用它,而其余的矢量数据正在通过存储转发。
将 32 位元素提取到整数寄存器的另一个值得考虑的选项(除了 Johan 提到的)是使用整数移位进行一些“洗牌”:
mov rax,xmm0
# use eax now, before destroying it
shr rax,32
pextrq rcx,xmm0,1
# use ecx now, before destroying it
shr rcx, 32
shr
可以在 Intel Haswell/Skylake 的 p0 或 p6 上 运行。 p6 没有向量 ALU,所以如果你想要低延迟但又想对向量 ALU 施加低压力,这个序列非常好。
或者如果你想把它们留在身边:
movq rax,xmm0
rorx rbx, rax, 32 # BMI2
# shld rbx, rax, 32 # alternative that has a false dep on rbx
# eax=xmm0[0], ebx=xmm0[1]
pextrq rdx,xmm0,1
mov ecx, edx # the "normal" way, if you don't want rorx or shld
shr rdx, 32
# ecx=xmm0[2], edx=xmm0[3]
以下处理 get 和 set 并且似乎有效(我认为这是 AT&T 语法):
#include <iostream>
int main() {
uint64_t lo1(111111111111L);
uint64_t hi1(222222222222L);
uint64_t lo2, hi2;
asm volatile (
"movq %3, %%xmm0 ; " // set high 64 bits
"pslldq , %%xmm0 ; " // shift left 64 bits
"movsd %2, %%xmm0 ; " // set low 64 bits
// operate on 128 bit register
"movq %%xmm0, %0 ; " // get low 64 bits
"movhlps %%xmm0, %%xmm0 ; " // move high to low
"movq %%xmm0, %1 ; " // get high 64 bits
: "=x"(lo2), "=x"(hi2)
: "x"(lo1), "x"(hi1)
: "%xmm0"
);
std::cout << "lo1: [" << lo1 << "]" << std::endl;
std::cout << "hi1: [" << hi1 << "]" << std::endl;
std::cout << "lo2: [" << lo2 << "]" << std::endl;
std::cout << "hi2: [" << hi2 << "]" << std::endl;
return 0;
}