在AVX512中,有一个固有的_mm256_lzcnt_epi32
,它返回一个向量,对于8个32位元素中的每个元素,该向量包含输入向量元素中前导零位的数量。
有没有一种仅使用AVX和AVX2指令来实现此目标的有效方法?
当前,我正在使用一个循环,该循环提取每个元素并应用_lzcnt_u32
函数。
相关:要对一个大的位图进行位扫描,请参见Count leading zeros in __m256i word,该位图使用pmovmskb
->位扫描可以找到要对哪个字节进行标量位扫描。
这个问题是关于当您实际上要使用全部8个结果而不仅仅是选择一个时,对8个单独的32位元素进行8个单独的lzcnts的操作。
答案 0 :(得分:5)
float
代表指数格式的数字,因此int-> FP转换为我们提供了指数字段中编码的最高置位位置。
我们希望int
-> float
的幅度四舍五入到向下(将值截断为0),而不是默认的最近舍入值。可能会四舍五入,使0x3FFFFFFF
看起来像0x40000000
。如果您在不进行任何FP数学运算的情况下进行了很多此类转换,则可以将MXCSR 1 中的舍入模式设置为截断,然后在完成后将其设置回去。
否则,您可以使用v & ~(v>>8)
来保留8个最高有效位,并将某些或所有较低位保持零,包括MSB下方可能设置的第8位。这足以确保所有舍入模式都不会舍入到2的下一个幂。它总是保持8个最高有效位,因为v>>8
移入8个零,所以倒数就是8个。在低位位置,无论MSB处于什么位置,都会从高位开始经过8个零,因此它将永远不会清除任何整数的最高有效位。根据MSB下方的设置位排列方式,它可能会或可能不会清除以下8个最高有效位。
转换后,我们在位模式上使用整数移位将指数(和符号位)移至底部,并通过饱和减法消除偏置。如果在原始32位输入中未设置任何位,则使用min
将结果设置为32。
__m256i avx2_lzcnt_epi32 (__m256i v) {
// prevent value from being rounded up to the next power of two
v = _mm256_andnot_si256(_mm256_srli_epi32(v, 8), v); // keep 8 MSB
v = _mm256_castps_si256(_mm256_cvtepi32_ps(v)); // convert an integer to float
v = _mm256_srli_epi32(v, 23); // shift down the exponent
v = _mm256_subs_epu16(_mm256_set1_epi32(158), v); // undo bias
v = _mm256_min_epi16(v, _mm256_set1_epi32(32)); // clamp at 32
return v;
}
脚注1:fp-> int转换可使用截断(cvtt),但int-> fp转换仅可使用默认舍入(取决于MXCSR)。
AVX512F为512位向量引入了舍入模式替代,它将解决问题__m512 _mm512_cvt_roundepi32_ps( __m512i a, int r);
。但是,所有带有AVX512F的CPU也都支持AVX512CD,因此您可以只使用_mm512_lzcnt_epi32
。对于AVX512VL,_mm256_lzcnt_epi32
答案 1 :(得分:3)
@aqrit的答案似乎更聪明地使用了FP比特hacks 。我在下面的答案是基于我首先找到的一个用于标量的bithack,因此它并未尝试避免double
(比int32
宽,这对于SIMD是一个问题) )。
它使用HW签名的int
-> float
转换和饱和整数减来处理设置的MSB(负浮点数),而不是将位填充到尾数中以进行手动uint-> double。如果您可以将MXCSR设置为在其中的许多_mm256_lzcnt_epi32
中向下取整,则效率更高。
https://graphics.stanford.edu/~seander/bithacks.html#IntegerLogIEEE64Float建议将整数填充到大double
的尾数中,然后减去以得到FPU硬件以获得标准化的double
。 (我认为这有点神奇,uint32_t
-> double
在How to efficiently perform double/int64 conversions with SSE/AVX?中使用了@Mysticial解释的技术(适用于uint64_t
最多2 52 -1)
然后抓住double
的指数位并消除偏差。
我认为整数log2与lzcnt是相同的东西,但可能以2的幂乘以1。
Standford Graphics比特黑客页面列出了您可以使用的其他无分支比特黑客,它们可能仍然比8倍标量lzcnt
更好。
如果您知道自己的数字始终很小(例如小于2 ^ 23),则可以使用float
进行此操作,并避免拆分和混合。
int v; // 32-bit integer to find the log base 2 of int r; // result of log_2(v) goes here union { unsigned int u[2]; double d; } t; // temp t.u[__FLOAT_WORD_ORDER==LITTLE_ENDIAN] = 0x43300000; t.u[__FLOAT_WORD_ORDER!=LITTLE_ENDIAN] = v; t.d -= 4503599627370496.0; r = (t.u[__FLOAT_WORD_ORDER==LITTLE_ENDIAN] >> 20) - 0x3FF;
上面的代码通过将整数存储在尾数中(而指数设置为252)来加载具有32位整数(无填充位)的64位(IEEE-754浮点数)双精度数。铸造的双精度数减去252(表示为双精度数),这会将结果指数设置为输入值v的对数2。剩下的就是将指数位移到位置(右20位)并减去偏差,即0x3FF(十进制为1023)。
要在AVX2上执行此操作,请将set1_epi32(0x43300000)
和_mm256_castps_pd
的奇/偶半数混合并平移和混合以获得__m256d
。 _mm256_castpd_si256
并将下半部分/上半部分移位/混合到位,然后遮罩以得到指数。
在AVX2上,对FP位模式进行整数运算非常有效,在FP数学指令的输出上进行整数移位时,旁路延迟只有1个周期的额外延迟。
(待办事项:使用C ++内在函数编写,编辑欢迎语或其他人可以将其发布为答案。)
我不确定您是否可以使用int
-> double
转换进行任何操作,然后读取指数字段。负数没有前导零,而正数给出的指数取决于幅度。
如果您确实希望这样做,则可以一次走一条128位通道,改组以供xmm-> ymm压缩int32_t
->压缩double
转换。
答案 2 :(得分:2)
该问题也被标记为AVX
,但是AVX
中没有整数处理的说明,这意味着需要在支持AVX
但不{ {1}}。我在下面显示了经过详尽测试的版本,但是有点行人版本。这里的基本思想与其他答案一样,前导零的计数由整数到浮点转换期间发生的浮点归一化确定。结果的指数与前导零的计数一一对应,只是在参数为零的情况下结果是错误的。从概念上讲:
AVX2
其中clz (a) = (158 - (float_as_uint32 (uint32_to_float_rz (a)) >> 23)) + (a == 0)
是重新解释的强制转换,而float_as_uint32()
是从无符号整数到带截断的浮点的转换。正常的舍入转换可能会将转换结果提高到下一个2的幂,从而导致前导零位的计数不正确。
uint32_to_float_rz()
既不提供单条截断整数到浮点数的转换,也不提供无符号整数的转换。此功能需要被仿真。只要不改变转换结果的大小,模拟就不需要精确。截断部分由aqrit's answer的 invert-右移-andn 技术处理。要使用有符号转换,我们在转换前将数字减半,然后在转换后加倍并递增:
SSE
此方法在下面的float approximate_uint32_to_float_rz (uint32_t a)
{
float r = (float)(int)((a >> 1) & ~(a >> 2));
return r + r + 1.0f;
}
中转换为SSE
内在函数。
sse_clz()