将uint32类型的向量转换为float类型的向量的最有效方法是什么?

5

x86没有将无符号int32转换为浮点数的SSE指令。实现这个操作最有效的指令序列是什么?

编辑: 澄清一下,我想执行以下标量操作的向量序列:

unsigned int x = ...
float res = (float)x;

编辑2:这里是一个简单的算法,用于进行标量转换。

unsigned int x = ...
float bias = 0.f;
if (x > 0x7fffffff) {
    bias = (float)0x80000000;
    x -= 0x80000000;
}
res = signed_convert(x) + bias;

你是指截断/四舍五入/...吗?能否给出一个所需输入/输出的最小示例? - Joachim Isaksson
我有点困惑,你是想将 int 转换为 float 还是将 float 转换为 int 还是两者兼而有之?你能否更正问题的标题和/或正文,使其不那么含糊不清? - Alexey Frunze
3个回答

4
您的朴素标量算法不能提供完全正确的转换 - 在某些输入上会遭受双舍入的影响。例如:如果x0x88000081,那么正确舍入为浮点数的结果是2281701632.0f,但您的标量算法将返回2281701376.0f

我能想到的一个正确的转换如下(如我所说,这只是我脑海中的一个简单示例,可能可以在某个地方节省指令):

movdqa   xmm1,  xmm0    // make a copy of x
psrld    xmm0,  16      // high 16 bits of x
pand     xmm1, [mask]   // low 16 bits of x
orps     xmm0, [onep39] // float(2^39 + high 16 bits of x)
cvtdq2ps xmm1, xmm1     // float(low 16 bits of x)
subps    xmm0, [onep39] // float(high 16 bits of x)
addps    xmm0,  xmm1    // float(x)

常数的值如下:

mask:   0000ffff 0000ffff 0000ffff 0000ffff
onep39: 53000000 53000000 53000000 53000000

这个操作将每个通道的高16位和低16位分别转换为浮点数,然后将这些转换后的值相加。由于每半段仅有16位宽度,所以转换为浮点数不会造成任何舍入误差。当这两个半段相加时才会发生舍入;因为加法是一个正确舍入的操作,所以整个转换都是正确舍入的。
相比之下,您的基础实现首先将低31位转换为浮点数,这会产生一次舍入,然后有条件地将2^31添加到该结果中,这可能会导致第二次舍入。在转换中有两个单独的舍入点时,除非您非常小心地处理它们的出现,否则不应该期望结果能正确地舍入。

你能否解释一下你的答案? - zr.
@zr:你对它有什么困惑? - Stephen Canon
乍一看,我不理解这个数学问题。为什么你的方案能得到正确的答案?并不是我在说它不正确... - zr.
@zr。每个单独的步骤都非常简单,所以如果你坐下来按照它走一遍,应该就很容易理解了。话虽如此,如果你在某个具体步骤上有困难,我很乐意解释为什么它会像注释所说的那样做。 - Stephen Canon
请问您能否解释一下如何处理高16位?我不理解OR后面跟着SUB的含义。 - zr.
@zr:如果你看一下浮点数2^39的有效数字位,第i位的值为2^(16+i)。因此,2^39 | (x >> 16)恰好是(2^39 + (x & 0xffff0000))作为一个浮点数,而不需要进行显式转换。减去2^39就可以消除偏差,留下(float)(x & 0xffff0000)。 - Stephen Canon

1

这是基于旧但有用的苹果AltiVec-SSE迁移文档中的示例,不幸的是该文档现在已经无法在http://developer.apple.com上获取:

inline __m128 _mm_ctf_epu32(const __m128i v)
{
    const __m128 two16 = _mm_set1_ps(0x1.0p16f);

    // Avoid double rounding by doing two exact conversions
    // of high and low 16-bit segments
    const __m128i hi = _mm_srli_epi32((__m128i)v, 16);
    const __m128i lo = _mm_srli_epi32(_mm_slli_epi32((__m128i)v, 16), 16);
    const __m128 fHi = _mm_mul_ps(_mm_cvtepi32_ps(hi), two16);
    const __m128 fLo = _mm_cvtepi32_ps(lo);

    // do single rounding according to current rounding mode
    return _mm_add_ps(fHi, fLo);
}

这也是一个很好的答案。我想知道它在准确性和性能方面与Stephen Canon的解决方案相比如何。 - zr.
另一个解决方案看起来不错,但是上面的代码有测试的优势,而且它使用了内置函数,这使得它更具可移植性。虽然在性能方面可能没有太大的区别。 - Paul R
如果您正在循环中执行此操作,则使用2个移位设置lo将成为瓶颈,而不是使用 _mm_and_si128(v,_mm_set_epi32(0x0000FFFF))(例如Stephen的答案)。例如Haswell每个时钟周期只能执行1个向量移位,并且FPU指令也会竞争端口0。 _mm_and_si128可以在端口5上运行,而其他指令都不使用该端口。 - Peter Cordes

1

在你提问时还没有这个选项,但是AVX512F添加了vcvtudq2ps


最好引用链接中的一些内容,这样如果链接消失或更改,答案仍然有用。现在这个回答基本上只是一个链接。 - Michael Petch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接