问题描述
翻阅 intel 内在指南,我看到了这个指令。查看命名模式,含义应该很清楚:“将 128 位寄存器左移固定位数”,但事实并非如此。实际上,它移动了固定数量的字节,这使得它与 _mm_bslli_si128
完全相同。
- 这是疏忽吗?它不应该像
_mm_slli_epi32
或_mm_slli_epi64
这样的位移位吗? - 如果不是,我应该在哪种情况下在
_mm_bslli_si128
上使用它? - 是否有正确执行此操作的汇编指令?
- 通过较小的班次来模拟这一点的最佳方法是什么?
解决方法
1 这不是疏忽。该指令确实按字节移动,即 8 位的倍数。
2 无所谓,_mm_slli_si128
和 _mm_bslli_si128
是等价的,都编译成 pslldq
SSE2 指令。
至于仿真,假设您有 C++/17,我会这样做。如果您正在编写 C++/14,请将 if constexpr
替换为普通的 if
,同时向 static_assert
添加一条消息。
template<int i>
inline __m128i shiftLeftBits( __m128i vec )
{
static_assert( i >= 0 && i < 128 );
// Handle couple trivial cases
if constexpr( 0 == i )
return vec;
if constexpr( 0 == ( i % 8 ) )
return _mm_slli_si128( vec,i / 8 );
if constexpr( i > 64 )
{
// Shifting by more than 8 bytes,the lowest half will be all zeros
vec = _mm_slli_si128( vec,8 );
return _mm_slli_epi64( vec,i - 64 );
}
else
{
// Shifting by less than 8 bytes.
// Need to propagate a few bits across 64-bit lanes.
__m128i low = _mm_slli_si128( vec,8 );
__m128i high = _mm_slli_epi64( vec,i );
low = _mm_srli_epi64( low,64 - i );
return _mm_or_si128( low,high );
}
}
,
TL:DR:它们是同义词; bslli
名称较新,大约与新的 AVX-512 内在函数同时引入(2015 年之前的某个时间,在 SSE2 _mm_slli_si128
被广泛使用之后很长时间)。我觉得它更清晰,并会推荐它用于新的开发。
SSE/AVX2/AVX-512 没有元素大小大于 64 的位移。(或任何其他位粒度操作,如 add
,除了真正 128 完全独立的纯垂直按位布尔值操作,不是一个大的宽操作。或者为了 AVX-512 掩码和广播加载目的,可以在 dword 或 qword 块中,如 _mm512_xor_epi32
/ vpxord
)
您必须以某种方式模拟它,这对于编译时常量计数可能相当有效,因此您可以根据 c >= 64
在策略之间进行选择,将 c%8
的特殊情况减少到一个字节 -转移。现有的 SO Q&A 涵盖了这一点,或查看 @Soonts 对此问题的回答。
运行时变量计数会很糟糕;您必须分支或同时进行两种方式和混合,这与 _mm_sll_epi64(v,_mm_cvtsi32_si128(i))
可以编译为 movd
/ psllq xmm,xmm
的元素位移不同。不幸的是,不存在字节洗牌/移位指令的硬件可变计数版本,仅适用于位移版本。
bslli
/ bsrli
是相同 asm 指令的新的、更清晰的内部名称
所有 4 个主要 x86 编译器的当前版本 (Godbolt) 都支持 b
名称,我建议将它们用于新开发,除非您需要向后兼容硬壳旧编译器,或者出于某种原因,您喜欢旧名称,但不能同时将其与不同的操作区分开来。 (例如熟悉度;如果您不希望人们必须在手册中查找这个新奇的名称。)
- 从 4.8 开始的 gcc
- clang 从 3.7 开始
- ICC 从 ICC13 或更早的版本开始,Godbolt 就没有更旧的
- 自 19.14 或更早版本开始的 MSVC,Godbolt 没有任何更旧的
如果您查看内部函数指南,_mm_slli_si128
被列为 PSLLDQ
的内部函数,这是一个字节移位。这不是错误,只是英特尔的一个笑话,或者他们在 SSE2 时代用来为内在函数选择名称的任何过程。 (计算机科学中只有两个难题:缓存失效和命名事物)。
Asm 助记符也使用相同的模式,即不使字节洗牌看起来与位移位不同。 psllw xmm,1
/ pslld
/ psllq
/ pslldq
。同样,您只需要知道 128 位大小是特殊的,并且必须是字节洗牌而不是位移,因为 x86 从来没有。 (或者你必须查看手册。)
pslldq
的 asm 手册条目依次列出了它的形式的内在函数,有趣的是,对于 b
AVX-512BW 版本仅使用 __m512i
名称。我认为,当 SSE2 和 AVX2 是新的时,_mm_slli_si128
和 _mm256_slli_si256
是唯一可用的名称。当然,它晚于 SSE2 内在函数。
(请注意,si256
和 si512
版本只是 16 字节操作的 2 或 4 个副本,不是跨 128 位通道移动字节;有些东西是很少有其他问答要求。这通常会使像这样的 AVX2 版本的 shuffle 和 palignr
比其他情况下有用得多:要么根本不值得使用,要么需要额外的 shuffle。)
我认为这个新的 bslli
名称是在 AVX-512 推出时引入的。英特尔在那个时候为其他内在函数发明了一些新名称,而 AVX-512 加载/存储内在函数采用 void*
而不是 __m512i*
,这是对代码噪声量的重大改进,尤其是对于 C其中允许隐式转换为 void*
。 (创建一个未对齐的 __m512i*
is not actually a problem in C terms,但是你不能正常地取消它,所以这是一件看起来很奇怪的事情。)所以当时对内在命名进行了清理工作,我认为这是一部分。
(AVX-512 还让 Intel 有机会引入一些相当糟糕的名称,例如 _mm_loadu_epi32(const void*)
- 你猜这是一种执行 32 位 movd
加载的严格别名安全方式,对吗?不,不幸的是,它是 vmovdqu32 xmm,[mem]
的内在函数,没有掩码。它只是 _mm_loadu_si128
的指针 arg 具有不同的 C 类型。它的存在是为了与 {{1} 的命名模式保持一致}. 让 _mm_maskz_loadu_epi32
加载/存储 void*
和 __m128i
的内在函数会很好,但是如果它们具有这样的误导性名称(尤其是当您不使用 {{ 1}}/__m256i
版本在附近的代码中),我只会坚持那些繁琐的 mask
旧内在转换,因为我喜欢输入 maskz
三次。>.
我希望 asm 更易于维护(或者内在函数只是使用 asm 助记符),因为它更简洁;英特尔通常在命名助记符方面做得很好。
注意 _mm256_loadu_si256( (const __m256i*)(arr + i) )
和 256
之间的区别在某种程度上但并非完全有帮助:EPI = Extended (SSE 而不是 MMX) Packed Integer。 (打包意味着多个 SIMD 元素)。 epi16/32/64
表示一个完整的 128 位整数向量。
无法从名称推断出您不是只是对单个 128 位整数而不是压缩元素执行相同的操作。您只需要知道没有跨越 64 位边界的位粒度事物,只有 SIMD shuffle(以字节为单位)。这避免了构建真正宽的桶形移位器的组合爆炸,或者避免了在如此长的距离上进行 128 位加法或其他任何东西的进位传播。