有没有比添加 0.5f 和截断转换更直接的方法将 float 转换为 int 舍入?
Is there a more direct method to convert float to int with rounding than adding 0.5f and converting with truncation?
在处理浮点数据的 C++ 代码中,经常会发生从 float 到 int 的四舍五入转换。例如,一种用途是生成转换表。
考虑这段代码:
// Convert a positive float value and round to the nearest integer
int RoundedIntValue = (int) (FloatValue + 0.5f);
C/C++ 语言将 (int) 转换定义为截断,因此必须添加 0.5f 以确保四舍五入到最接近的正整数(当输入为正数时)。针对以上内容,VS2015的编译器生成如下代码:
movss xmm9, DWORD PTR __real@3f000000 // 0.5f
addss xmm0, xmm9
cvttss2si eax, xmm0
以上方法可行,但效率可能更高...
Intel 的设计师显然认为这是一个足够重要的问题,可以用一条指令来解决所需的事情:转换为最接近的整数值:cvtss2si(注意,助记符中只有一个 't') .
如果 cvtss2si 将替换上述序列中的 cvttss2si 指令,那么三个指令中的两个将被删除(使用额外的 xmm 寄存器,这可能会导致更好的整体优化)。
那么我们如何编写 C++ 语句来使用一条 cvtss2si 指令完成这项简单的工作?
我一直在四处寻找,尝试像下面这样的事情,但即使优化器在执行任务,它也不会归结为 could/should 完成工作的一条机器指令:
int RoundedIntValue = _mm_cvt_ss2si(_mm_set_ss(FloatValue));
不幸的是,上面的内容似乎一心想清除一整套永远不会被使用的寄存器向量,而不是仅仅使用一个 32 位值。
movaps xmm1, xmm0
xorps xmm2, xmm2
movss xmm2, xmm1
cvtss2si eax, xmm2
也许我在这里遗漏了一个明显的方法。
您能否提供一组最终生成单个 cvtss2si 指令的建议 C++ 指令?
这是微软编译器的一个优化缺陷,bughas been reported to Microsoft. As other commentators have mentioned, modern versions of GCC, Clang, and ICC all produce the expected code。对于像这样的函数:
int RoundToNearestEven(float value)
{
return _mm_cvt_ss2si(_mm_set_ss(value));
}
除 Microsoft 之外的所有编译器都会发出以下目标代码:
cvtss2si eax, xmm0
ret
而 Microsoft 的编译器(截至 VS 2015 更新 3)发出以下内容:
movaps xmm1, xmm0
xorps xmm2, xmm2
movss xmm2, xmm1
cvtss2si eax, xmm2
ret
双精度版本 cvtsd2si
(即,_mm_cvtsd_si32
内部函数)也是如此。
在优化器得到改进之前,没有更快的替代方案可用。幸运的是,当前生成的代码并不像看起来那么慢。移动和寄存器清除是可能最快的指令之一,其中一些指令可能仅在前端作为寄存器重命名来实现。而且它肯定比任何可能的替代方案都快——通常快几个数量级:
您提到的加 0.5 的技巧不仅会变慢,因为它必须加载常量并执行加法,而且在所有情况下都不会产生正确舍入的结果。
使用 _mm_load_ss
内在函数将浮点值加载到适合与 _mm_cvt_ss2si
内在函数一起使用的 __m128
结构是一种悲观化,因为它导致内存溢出,而不仅仅是寄存器到寄存器的移动。
(请注意,虽然 _mm_set_ss
对于 x86-64 而言 总是 更好,其中调用约定使用 SSE 寄存器传递浮点值,但我有 偶尔 观察到 _mm_load_ss
将在 x86-32 构建中生成比 _mm_set_ss
更优化的代码,但它高度依赖于多个因素,并且仅在多个内在函数被观察到用于复杂的代码序列。您的默认选择应为 _mm_set_ss
。)
用 reinterpret_cast<__m128&>(value)
(或道德等价物)代替 _mm_set_ss
内在的既不安全又低效。它导致从 SSE 寄存器溢出到内存; cvtss2si
指令然后使用该内存位置作为其源操作数。
声明一个临时 __m128
结构并对其进行值初始化是安全的,但效率更低。 Space 分配在整个结构的堆栈上,然后每个槽都填充 0 或浮点值。此结构的内存位置然后用作 cvtss2si
.
的源操作数
C 标准库提供的 lrint
函数族应该做你想做的,事实上在一些其他编译器上编译成直接的 cvt*
指令,但是在 Microsoft 的编译器上非常次优。它们从不内联,因此您总是要付出函数调用的代价。另外,函数内部的代码是次优的。 Both of these have been reported as bugs,但我们仍在等待修复。标准库提供的其他转换函数也有类似的问题,包括lround
和friends.
x87 FPU 提供了执行类似任务的 FIST
/FISTP
指令,但 C 和 C++ 语言标准要求转换 truncate ,而不是round-to-nearest-even(默认的FPU舍入模式),所以编译器有义务插入一堆代码来改变当前的舍入模式,执行转换,然后再改回来.这 非常 慢,并且除了使用内联汇编之外,没有办法指示编译器不要这样做。除了内联汇编不适用于 64 位编译器这一事实之外,MSVC 的内联汇编语法也无法提供指定输入和输出的方法,因此您要为这两种方式支付双倍的加载和存储惩罚。即使不是这种情况,您仍然需要支付将浮点值从 SSE 寄存器复制到内存,然后复制到 x87 FPU 堆栈的成本。
内在函数很棒,通常可以让您生成比编译器生成的代码更快的代码,但它们并不完美。如果您像我一样发现自己经常分析二进制文件的反汇编,您会发现自己经常感到失望。不过,您最好的选择是使用内部函数。
至于为什么 优化器以这种方式发出代码,我只能推测,因为我不在 Microsoft 编译器团队工作,但我的猜测是是因为许多其他 cvt*
指令具有错误的依赖关系,代码生成器需要解决这些问题。例如,cvtss2sd
不会修改目标 XMM 寄存器的高 64 位。这种 部分 寄存器更新会导致停顿并减少指令级并行性的机会。这在循环中尤其是一个问题,其中寄存器的高位形成第二个循环携带的依赖链,即使我们实际上并不关心它们的内容。因为 cvtss2sd
指令的执行要等到前面的指令完成后才能开始,所以延迟会大大增加。然而,通过先执行 xorss
或 movss
指令,寄存器的高位被清除,从而打破依赖性并避免停顿的可能性。这是一个有趣的例子,其中更短代码不等同于更快代码。编译器团队 started inserting these dependency-breaking instructions for scalar conversions in the compiler shipped with VS 2010,并且可能过度热心地应用了启发式算法。
Visual Studio 今天发布的 15.6 似乎终于解决了这个问题。我们现在看到内联此函数时使用了一条指令:
inline int ConvertFloatToRoundedInt(float FloatValue)
{
return _mm_cvt_ss2si(_mm_set_ss(FloatValue)); // Convert to integer with rounding
}
微软终于得到了圆满的回报,这让我印象深刻。
在处理浮点数据的 C++ 代码中,经常会发生从 float 到 int 的四舍五入转换。例如,一种用途是生成转换表。
考虑这段代码:
// Convert a positive float value and round to the nearest integer
int RoundedIntValue = (int) (FloatValue + 0.5f);
C/C++ 语言将 (int) 转换定义为截断,因此必须添加 0.5f 以确保四舍五入到最接近的正整数(当输入为正数时)。针对以上内容,VS2015的编译器生成如下代码:
movss xmm9, DWORD PTR __real@3f000000 // 0.5f
addss xmm0, xmm9
cvttss2si eax, xmm0
以上方法可行,但效率可能更高...
Intel 的设计师显然认为这是一个足够重要的问题,可以用一条指令来解决所需的事情:转换为最接近的整数值:cvtss2si(注意,助记符中只有一个 't') .
如果 cvtss2si 将替换上述序列中的 cvttss2si 指令,那么三个指令中的两个将被删除(使用额外的 xmm 寄存器,这可能会导致更好的整体优化)。
那么我们如何编写 C++ 语句来使用一条 cvtss2si 指令完成这项简单的工作?
我一直在四处寻找,尝试像下面这样的事情,但即使优化器在执行任务,它也不会归结为 could/should 完成工作的一条机器指令:
int RoundedIntValue = _mm_cvt_ss2si(_mm_set_ss(FloatValue));
不幸的是,上面的内容似乎一心想清除一整套永远不会被使用的寄存器向量,而不是仅仅使用一个 32 位值。
movaps xmm1, xmm0
xorps xmm2, xmm2
movss xmm2, xmm1
cvtss2si eax, xmm2
也许我在这里遗漏了一个明显的方法。
您能否提供一组最终生成单个 cvtss2si 指令的建议 C++ 指令?
这是微软编译器的一个优化缺陷,bughas been reported to Microsoft. As other commentators have mentioned, modern versions of GCC, Clang, and ICC all produce the expected code。对于像这样的函数:
int RoundToNearestEven(float value)
{
return _mm_cvt_ss2si(_mm_set_ss(value));
}
除 Microsoft 之外的所有编译器都会发出以下目标代码:
cvtss2si eax, xmm0
ret
而 Microsoft 的编译器(截至 VS 2015 更新 3)发出以下内容:
movaps xmm1, xmm0
xorps xmm2, xmm2
movss xmm2, xmm1
cvtss2si eax, xmm2
ret
双精度版本 cvtsd2si
(即,_mm_cvtsd_si32
内部函数)也是如此。
在优化器得到改进之前,没有更快的替代方案可用。幸运的是,当前生成的代码并不像看起来那么慢。移动和寄存器清除是可能最快的指令之一,其中一些指令可能仅在前端作为寄存器重命名来实现。而且它肯定比任何可能的替代方案都快——通常快几个数量级:
您提到的加 0.5 的技巧不仅会变慢,因为它必须加载常量并执行加法,而且在所有情况下都不会产生正确舍入的结果。
使用
_mm_load_ss
内在函数将浮点值加载到适合与_mm_cvt_ss2si
内在函数一起使用的__m128
结构是一种悲观化,因为它导致内存溢出,而不仅仅是寄存器到寄存器的移动。(请注意,虽然
_mm_set_ss
对于 x86-64 而言 总是 更好,其中调用约定使用 SSE 寄存器传递浮点值,但我有 偶尔 观察到_mm_load_ss
将在 x86-32 构建中生成比_mm_set_ss
更优化的代码,但它高度依赖于多个因素,并且仅在多个内在函数被观察到用于复杂的代码序列。您的默认选择应为_mm_set_ss
。)用
reinterpret_cast<__m128&>(value)
(或道德等价物)代替_mm_set_ss
内在的既不安全又低效。它导致从 SSE 寄存器溢出到内存;cvtss2si
指令然后使用该内存位置作为其源操作数。声明一个临时
的源操作数__m128
结构并对其进行值初始化是安全的,但效率更低。 Space 分配在整个结构的堆栈上,然后每个槽都填充 0 或浮点值。此结构的内存位置然后用作cvtss2si
.C 标准库提供的
lrint
函数族应该做你想做的,事实上在一些其他编译器上编译成直接的cvt*
指令,但是在 Microsoft 的编译器上非常次优。它们从不内联,因此您总是要付出函数调用的代价。另外,函数内部的代码是次优的。 Both of these have been reported as bugs,但我们仍在等待修复。标准库提供的其他转换函数也有类似的问题,包括lround
和friends.x87 FPU 提供了执行类似任务的
FIST
/FISTP
指令,但 C 和 C++ 语言标准要求转换 truncate ,而不是round-to-nearest-even(默认的FPU舍入模式),所以编译器有义务插入一堆代码来改变当前的舍入模式,执行转换,然后再改回来.这 非常 慢,并且除了使用内联汇编之外,没有办法指示编译器不要这样做。除了内联汇编不适用于 64 位编译器这一事实之外,MSVC 的内联汇编语法也无法提供指定输入和输出的方法,因此您要为这两种方式支付双倍的加载和存储惩罚。即使不是这种情况,您仍然需要支付将浮点值从 SSE 寄存器复制到内存,然后复制到 x87 FPU 堆栈的成本。
内在函数很棒,通常可以让您生成比编译器生成的代码更快的代码,但它们并不完美。如果您像我一样发现自己经常分析二进制文件的反汇编,您会发现自己经常感到失望。不过,您最好的选择是使用内部函数。
至于为什么 优化器以这种方式发出代码,我只能推测,因为我不在 Microsoft 编译器团队工作,但我的猜测是是因为许多其他 cvt*
指令具有错误的依赖关系,代码生成器需要解决这些问题。例如,cvtss2sd
不会修改目标 XMM 寄存器的高 64 位。这种 部分 寄存器更新会导致停顿并减少指令级并行性的机会。这在循环中尤其是一个问题,其中寄存器的高位形成第二个循环携带的依赖链,即使我们实际上并不关心它们的内容。因为 cvtss2sd
指令的执行要等到前面的指令完成后才能开始,所以延迟会大大增加。然而,通过先执行 xorss
或 movss
指令,寄存器的高位被清除,从而打破依赖性并避免停顿的可能性。这是一个有趣的例子,其中更短代码不等同于更快代码。编译器团队 started inserting these dependency-breaking instructions for scalar conversions in the compiler shipped with VS 2010,并且可能过度热心地应用了启发式算法。
Visual Studio 今天发布的 15.6 似乎终于解决了这个问题。我们现在看到内联此函数时使用了一条指令:
inline int ConvertFloatToRoundedInt(float FloatValue)
{
return _mm_cvt_ss2si(_mm_set_ss(FloatValue)); // Convert to integer with rounding
}
微软终于得到了圆满的回报,这让我印象深刻。