是否有更直接的方法将float转换为int而不是添加0.5f并使用截断转换?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了是否有更直接的方法将float转换为int而不是添加0.5f并使用截断转换?相关的知识,希望对你有一定的参考价值。
在使用浮点数据的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
以上工作,但可能更有效......
英特尔的设计人员显然认为使用单个指令解决问题非常重要,只需要做的就是:转换为最接近的整数值:cvtss2si(注意,在助记符中只有一个't')。
如果cvtss2si要在上面的序列中替换cvttss2si指令,那么就会消除三个指令中的两个(就像使用额外的xmm寄存器一样,这可能会导致更好的整体优化)。
那么我们如何编写C ++语句来使用一个cvtss2si指令完成这个简单的工作呢?
我一直在四处寻找,尝试以下内容,但即使使用优化器执行任务,它也不能归结为可以/应该完成工作的一台机器指令:
int RoundedIntValue = _mm_cvt_ss2si(_mm_set_ss(FloatValue));
不幸的是,上面似乎倾向于清除永远不会使用的整个寄存器向量,而不是仅使用一个32位值。
movaps xmm1, xmm0
xorps xmm2, xmm2
movss xmm2, xmm1
cvtss2si eax, xmm2
也许我在这里错过了一个明显的方法。
你能提供一套建议的C ++指令,最终会生成单个cvtss2si指令吗?
这是Microsoft编译器中的优化缺陷,以及对Microsoft的错误has been reported。正如其他评论家所提到的那样,modern versions of GCC, Clang, and ICC all produce the expected code。对于像这样的功能:
int RoundToNearestEven(float value)
{
return _mm_cvt_ss2si(_mm_set_ss(value));
}
所有编译器,但微软将发出以下目标代码:
cvtss2si eax, xmm0
ret
而Microsoft的编译器(从VS 2015 Update 3开始)发出以下内容:
movaps xmm1, xmm0
xorps xmm2, xmm2
movss xmm2, xmm1
cvtss2si eax, xmm2
ret
对于双精度版本cvtsd2si
(即_mm_cvtsd_si32
内在)也可以看到相同的情况。
在改进优化器之前,没有更快的替代方案可用。幸运的是,当前生成的代码并不像看起来那么慢。移动和寄存器清除是最快的指令之一,其中一些可能只在前端实现为寄存器重命名。它肯定比任何可能的替代品更快 - 通常是数量级:
- 添加0.5你提到的技巧不仅会慢,因为它必须加载常量并执行添加,它也不会在所有情况下产生正确的舍入结果。
- 使用
_mm_load_ss
内在函数将浮点值加载到适合与__m128
内在函数一起使用的_mm_cvt_ss2si
结构中是一种悲观,因为它会导致内存泄漏,而不仅仅是寄存器到寄存器的移动。 (请注意,虽然_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
结构并对其进行初始化是安全的,但效率更低。在堆栈上为整个结构分配空间,然后每个槽都填充0或浮点值。然后将此结构的内存位置用作cvtss2si
的源操作数。 - C标准库提供的
lrint
函数系列应该可以满足您的需要,实际上可以在其他编译器上编译成简单的cvt*
指令,但在Microsoft的编译器中非常不理想。它们永远不会内联,因此您始终需要支付函数调用的成本。另外,函数内部的代码是次优的。 Both of these一直是reported as bugs,但我们仍在等待修复。标准库提供的其他转换函数也存在类似问题,包括lround
和朋友。 - x87 FPU提供执行类似任务的
FIST
/FISTP
指令,但C和C ++语言标准要求使用强制截断,而不是舍入到最接近(默认的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
}
我印象深刻的是,微软终于得到了一个回合。
以上是关于是否有更直接的方法将float转换为int而不是添加0.5f并使用截断转换?的主要内容,如果未能解决你的问题,请参考以下文章
java中,强制转换符把float转换为int时,按四舍五入,还是直接丢掉小数部分?