simd:将输入的 log2 向上舍入(ceil),同时将负对数钳制为零?

simd: round up (ceil) the log2 of an input, while clamping negative logs to zero?

是否有任何 simd 可以 ceil 一个浮点数(向上舍入)然后将它转换为 unsigned int 而不换行? (即任何负数变为 0)?

也许_mm_cvttps_epi32?虽然不确定舍入和换行(我想我得到了 ub):

__m128 indices = _mm_mul_ps(phaseIncrement.v, mMinTopFreq.v);
indices = sse_mathfun_log_ps(indices) / std::log(2.0f);
__m128i finalIndices = _mm_cvttps_epi32(indices);

或任何奇特的方式?我正在使用 -O3 -march=nehalem -funsafe-math-optimizations -fno-omit-frame-pointer 进行编译(因此允许使用 sse4.1)。

编辑 根据 Peter Cordes 的建议,这是重新访问代码的一个版本:

__m128 indexesFP = _mm_mul_ps(phaseIncrement.v, mMinTopFreq.v);
indexesFP = sse_mathfun_log_ps(indexesFP) * (1.0f / std::log(2.0f));
indexesFP = _mm_ceil_ps(indexesFP);
__m128i indexes = _mm_cvttps_epi32(indexesFP);
indexes = _mm_max_epi32(indexes, _mm_set_epi32(0, 0, 0, 0));
for (int i = 0; i < 4; i++) {
    int waveTableIndex = _mm_extract_epi32(indexes, i);
    waveTablesData[i] = &mWaveTables[waveTableIndex];
}

有什么可以改进的吗?

since the range will be limited (such as [0, 16] in the extreme case

哦,这不需要对大于 INT_MAX、直到 UINT_MAX 的数字起作用?这比问题顶部所述的问题要容易得多。是的,只是 _mm_ceil_ps 并使用带符号的转换到 epi32 (int32_t) 并使用 _mm_min_epi32 作为上限,可能 _mm_max_epi32 作为下限。 (只有一条指令而不是 shift/and)。

或者可能 _mm_sub_ps 到 range-shift 到 -16..0 / _mm_cvttps_epi32 截断(向上接近零),然后整数从零减去。 _mm_ceil_ps 在大多数 CPU 上花费 2 微指令,所以这大约是 break-even,尽管将 FP 操作换成整数。但需要更多设置。

整数 min/max 比 FP 更便宜(延迟更低,吞吐量更高),因此更喜欢转换后钳位。 Out-of-range 浮点数转换为 INT_MIN(high-bit 设置,其他为零,英特尔称之为“不定整数”值)因此将固定为 0。


如果您在不进行其他 FP 计算的循环中有很多这样的事情要做,请将此循环的 MXCSR 舍入模式更改为向 +Inf 舍入。使用 _mm_cvtps_epi32(它使用当前的 FP 舍入模式,如 lrint / (int)nearbyint)而不是 ceil + cvtt(截断)。


这个use-case:ceil(log2(float))

您可以直接将其从 FP 位模式中提取出来,然后根据 non-zero 尾数向上舍入。二进制浮点数已经包含一个 power-of-2 指数字段,因此您只需要稍微修改一下即可将其提取出来。

喜欢 _mm_and_ps / _mm_cmpeq_epi32 / _mm_add_epi32 添加 -1 尾数为零的 FP 值的比较结果,因此您对待 2 的幂与任何事物都不同更高。

应该比计算带有小数部分的 FP 对数基数 e 更快,即使它只是一个快速近似值。小于 1.0 且偏置指数为负的值可能需要一些额外处理。

此外,由于您需要所有四个索引,可能更快地存储到一个包含 4 个 uint32_t 值的数组并访问它,而不是使用 movd + 3x pextrd

对于带有 non-zero 尾数的浮点数,一个更好的舍入到下一个指数的方法是简单地将 0x007fffff 的整数加到 bit-pattern 上。 (23 个设置位:https://en.wikipedia.org/wiki/Single-precision_floating-point_format)。

// we round up the exponent separately from unbiasing.
// Might be possible to do better
__m128i ceil_log2_not_fully_optimized(__m128 v)
{
    // round up to the next power of 2 (exponent value) by carry-out from mantissa field into exponent
    __m128i floatbits = _mm_add_epi32(_mm_castps_si128(v), _mm_set1_epi32(0x007fffff));    

    __m128i exp = _mm_srai_epi32(floatbits, 23);   // arithmetic shift so negative numbers stay negative
    exp = _mm_sub_epi32(exp, _mm_set1_epi32(127));  // undo the bias
    exp = _mm_max_epi32(exp, _mm_setzero_si128());  // clamp negative numbers to zero.
    return exp;
}

如果指数字段已经是 all-ones,则表示带有 all-zero 尾数的 +Inf,否则为 NaN。因此,如果输入已经是 NaN,则只有从第一个加法开始的进位传播才能翻转符号位。 +Inf 被视为高于 FLT_MAX 的一个指数。 0.00.01 应该都出来 0,如果我没看错的话。

根据 Godbolt 上的 GCC,我是这么认为的:https://godbolt.org/z/9G9orWj16 GCC 没有完全 constant-propagate 通过它,所以我们实际上可以看到 pmaxsd 的输入,并看到0.00.01 分别出来 max(0, -127)max(0,-3) = 0。而3.04.0都出来了max(0, 2) = 2


我们甚至可以将 +0x7ff... 的想法与向指数字段添加负数来消除偏差相结合。

或者为了使 carry-out 正确进入符号位,从中减去,在尾数字段中使用 1 因此 all-zero 尾数将传播一个借位并再减去一个来自指数场?但是小于偏差的小指数仍然可以 carry/borrow 出来并翻转符号位。但是,如果我们无论如何都要将这些值限制为零,如果它们以小的正值而不是负值出现,那可能没问题。

我还没有弄清楚这方面的细节;如果我们需要处理 original 输入为零,这可能是个问题。如果我们可以假设原始符号位已被清除,但 log(x) 可能为负(即低于偏差的指数字段),这应该可以正常工作;在这种情况下,将指数字段执行到符号位正是我们想要的,因此 srai 将其保持为负数,而 max 选择 0.

    // round up to the next power of 2 (exponent value) while undoing the bias
    const uint32_t f32_unbias = ((-127)<<23) + 0x007fffffU;
    ???
    profit