X86 使用SSE计算绝对值的最快方法
我知道有3种方法,但据我所知,通常只使用前2种:X86 使用SSE计算绝对值的最快方法,x86,vectorization,sse,simd,absolute-value,X86,Vectorization,Sse,Simd,Absolute Value,我知道有3种方法,但据我所知,通常只使用前2种: 使用和ps或和notps屏蔽符号位 优点:如果掩码已经在寄存器中,则一条快速指令,这使得它非常适合在循环中多次执行此操作 缺点:掩码可能不在寄存器中,或者更糟,甚至不在缓存中,这会导致很长的内存提取 从零减去该值求反,然后得到原始值和求反值的最大值 优点:固定成本,因为不需要任何东西,比如面具 缺点:如果条件理想,将始终比掩码方法慢,并且在使用maxps指令之前,我们必须等待subps完成 与选项2类似,将原始值从零减去求反,然后使用和p
和ps
或和notps
屏蔽符号位
- 优点:如果掩码已经在寄存器中,则一条快速指令,这使得它非常适合在循环中多次执行此操作
- 缺点:掩码可能不在寄存器中,或者更糟,甚至不在缓存中,这会导致很长的内存提取
- 优点:固定成本,因为不需要任何东西,比如面具
- 缺点:如果条件理想,将始终比掩码方法慢,并且在使用
指令之前,我们必须等待maxps
完成subps
和ps
将结果与原始值“按位和”。我运行了一个测试,将其与方法2进行比较,它的行为似乎与方法2相同,除了处理NaN
s时,在这种情况下,结果将与方法2的结果不同
- 优点:应该比方法2略快,因为
通常比和ps
快maxps
- 缺点:当涉及
s时,这会导致任何意外行为吗?可能不是,因为一个NaN
仍然是一个NaN
,即使它是NaN
的不同值,对吗NaN
欢迎提出想法和意见。TL;DR:在几乎所有情况下,使用pcmpeq/shift生成掩码,并使用andps。它具有迄今为止最短的关键路径(与内存中的常量绑定),并且不能缓存未命中 如何用内在论做到这一点 让编译器在未初始化的寄存器上发出
pcmpeqd
,可能会很棘手。gcc/icc的最佳方式是
__m128 abs_mask(void){
// with clang, this turns into a 16B load,
// with every calling function getting its own copy of the mask
__m128i minus1 = _mm_set1_epi32(-1);
return _mm_castsi128_ps(_mm_srli_epi32(minus1, 1));
}
// MSVC is BAD when inlining this into loops
__m128 vecabs_and(__m128 v) {
return _mm_and_ps(abs_mask(), v);
}
__m128 sumabs(const __m128 *a) { // quick and dirty no alignment checks
__m128 sum = vecabs_and(*a);
for (int i=1 ; i < 10000 ; i++) {
// gcc, clang, and icc hoist the mask setup out of the loop after inlining
// MSVC doesn't!
sum = _mm_add_ps(sum, vecabs_and(a[i])); // one accumulator makes addps latency the bottleneck, not throughput
}
return sum;
}
可能您只需要将掩码作为16B常量存储在内存中。希望每个使用它的函数都不会重复。在32位代码中,将掩码放在内存常量中可能更有帮助,因为在32位代码中,只有8个XMM寄存器,因此veCAB
可以在内存源操作数没有可用寄存器的情况下使用ANDPS来保持常量
TODO:了解如何避免在常量内联的任何地方重复该常量。可能使用全局常量,而不是匿名set1
,会更好。但是您需要初始化它,但我不确定intrinsic是否可以作为全局\uuum128
变量的初始化器。您希望它位于只读数据部分,而不是在程序启动时运行构造函数
或者,使用
__m128i minus1; // undefined
#if _MSC_VER && !__INTEL_COMPILER
minus1 = _mm_setzero_si128(); // PXOR is cheaper than MSVC's silly load from the stack
#endif
minus1 = _mm_cmpeq_epi32(minus1, minus1); // or use some other variable here, which will probably cost a mov insn without AVX, unless the variable is dead.
const __m128 absmask = _mm_castsi128_ps(_mm_srli_epi32(minus1, 1));
额外的PXOR非常便宜,但它仍然是uop,代码大小仍然是4字节。如果有人有更好的解决方案来克服MSVC不愿意发出我们想要的代码,请留下评论或编辑。但是,如果内联到一个循环中,这是不好的,因为pxor/pcmp/psrl都将在循环中
使用movd
加载32位常量并使用shufps
进行广播可能还可以(不过,您可能需要手动将其从循环中取出)。这是3条指令(mov立即到GP reg、movd、shufps),在AMD上,movd速度较慢,在AMD上,矢量单元在两个整数核之间共享。(他们的超读版本。)
选择最佳asm序列 好的,让我们看看这个,比如说通过天湖的Intel Sandybridge,还有一点提到Nehalem。有关我是如何解决这个问题的,请参阅微囊指南和说明时间。我还使用了Skylake的号码,有人在论坛上的帖子中链接了这些号码
假设我们想要
abs()
的向量位于xmm0
中,并且是FP代码典型的长依赖链的一部分
因此,让我们假设任何不依赖于xmm0
的操作都可以在xmm0
准备就绪之前开始几个周期。我已经测试过了,如果内存操作数的地址不是dep链的一部分(即,不是关键路径的一部分),那么带有内存操作数的指令不会给依赖链增加额外的延迟
我不完全清楚,当它是微熔合uop的一部分时,内存操作可以在多早开始。据我所知,重新订购缓冲区(ROB)与融合的UOP一起工作,并跟踪UOP从发行到报废(168(SnB)到224(SKL)条目)。还有一个调度器在未使用的域中工作,只保存输入操作数就绪但尚未执行的UOP。uop可以在解码(或从uop缓存加载)的同时发送到ROB(融合)和调度程序(未融合),在天湖有97个 还有人说Skylake每个时钟处理6个UOP。据我所知,Skylake将每个时钟将整个uop缓存线(最多6个uop)读入uop缓存和ROB之间的缓冲区。进入ROB/调度程序的问题仍然是4-wide。(即使
nop
仍然是每个时钟4个)。这个缓冲区有帮助
何处导致以前的Sandybridge Microach设计出现瓶颈。我以前以为这个“问题队列”就是这个缓冲区,但显然不是
无论它如何工作,如果地址不在关键路径上,调度程序都足够大,可以及时从缓存中获取数据
1a:带内存操作数的掩码
- 字节:7个insn,16个数据。(平均值:8英寸)
- 融合域uops:1*n
- 添加到关键路径的延迟:1c(假设一级缓存命中)
- 吞吐量:1/c。(受2个负载/立方英尺的限制)
- 当insn发出时,如果
已准备就绪,“延迟”:一级缓存命中时约4cxmm0
1b:来自寄存器的掩码
- 字节:10 insn+16数据。(AVX:12 insn字节)
- 融合域uops:1+1*n
- 潜伏期
__m128i minus1; // undefined #if _MSC_VER && !__INTEL_COMPILER minus1 = _mm_setzero_si128(); // PXOR is cheaper than MSVC's silly load from the stack #endif minus1 = _mm_cmpeq_epi32(minus1, minus1); // or use some other variable here, which will probably cost a mov insn without AVX, unless the variable is dead. const __m128 absmask = _mm_castsi128_ps(_mm_srli_epi32(minus1, 1));
ANDPS xmm0, [mask] # in the loop
movaps xmm5, [mask] # outside the loop ANDPS xmm0, xmm5 # in a loop # or PAND xmm0, xmm5 # higher latency, but more throughput on Nehalem to Broadwell # or with an inverted mask, if set1_epi32(0x80000000) is useful for something else in your loop: VANDNPS xmm0, xmm5, xmm0 # It's the dest that's NOTted, so non-AVX would need an extra movaps
# outside a loop PCMPEQD xmm5, xmm5 # set to 0xff... Recognized as independent of the old value of xmm5, but still takes an execution port (p1/p5). PSRLD xmm5, 1 # 0x7fff... # port0 # or PSLLD xmm5, 31 # 0x8000... to set up for ANDNPS ANDPS xmm0, xmm5 # in the loop. # port5
VXORPS xmm5, xmm5, xmm5 # outside the loop VSUBPS xmm1, xmm5, xmm0 # inside the loop VMAXPS xmm0, xmm0, xmm1
# inside the loop XORPS xmm1, xmm1 # not on the critical path, and doesn't even take an execution unit on SnB and later SUBPS xmm1, xmm0 MAXPS xmm0, xmm1
VXORPS xmm5, xmm5, xmm5 # outside the loop. Without AVX: zero xmm1 inside the loop VSUBPS xmm1, xmm5, xmm0 # inside the loop VANDPS xmm0, xmm0, xmm1
PSLLD xmm0, 1 PSRLD xmm0, 1