是否有更直接的方法将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指令的执行在前一条指令完成之前无法开始,因此延迟会大大增加。但是,通过首先执行xorssmovss指令,寄存器的高位被清除,从而破坏了依赖性并避免了停顿的可能性。这是一个有趣的例子,其中较短的代码不等于更快的代码。编译器团队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并使用截断转换?的主要内容,如果未能解决你的问题,请参考以下文章

是否总是可以将`int`转换为`float`

java中,强制转换符把float转换为int时,按四舍五入,还是直接丢掉小数部分?

javascript 怎么将float强制转换为int类型

为啥 int 被提升为 double 而不是 float 以进行隐式转换

Python,确定字符串是否应转换为Int或Float

如何检查字符串是不是能够转换为 float 或 int [重复]