Noe*_*elC 4 c++ assembly sse intel visual-studio-2015
在使用浮点数据的C++代码中,通常使用舍入从float转换为int.例如,一种用途是生成转换表.
考虑这段代码:
// Convert a positive float value and round to the nearest integer
int RoundedIntValue = (int) (FloatValue + 0.5f);
Run Code Online (Sandbox Code Playgroud)
C/C++语言将(int)强制转换定义为截断,因此必须添加0.5f以确保向上舍入到最接近的正整数(当输入为正时).对于上述内容,VS2015的编译器生成以下代码:
movss xmm9, DWORD PTR __real@3f000000 // 0.5f
addss xmm0, xmm9
cvttss2si eax, xmm0
Run Code Online (Sandbox Code Playgroud)
以上工作,但可能更有效......
英特尔的设计人员显然认为使用单个指令解决问题非常重要,只需要做的就是:转换为最接近的整数值:cvtss2si(注意,在助记符中只有一个't').
如果cvtss2si要在上面的序列中替换cvttss2si指令,那么就会消除三个指令中的两个(就像使用额外的xmm寄存器一样,这可能会导致更好的整体优化).
那么我们如何编写C++语句来使用一个cvtss2si指令完成这个简单的工作呢?
我一直在四处寻找,尝试以下内容,但即使使用优化器执行任务,它也不能归结为可以/应该完成工作的一台机器指令:
int RoundedIntValue = _mm_cvt_ss2si(_mm_set_ss(FloatValue));
Run Code Online (Sandbox Code Playgroud)
不幸的是,上面似乎倾向于清除永远不会使用的整个寄存器向量,而不是仅使用一个32位值.
movaps xmm1, xmm0
xorps xmm2, xmm2
movss xmm2, xmm1
cvtss2si eax, xmm2
Run Code Online (Sandbox Code Playgroud)
也许我在这里错过了一个明显的方法.
你能提供一套建议的C++指令,最终会生成单个cvtss2si指令吗?
这是Microsoft编译器中的优化缺陷,并且已向 Microsoft 报告该错误.正如其他评论家所提到的,GCC,Clang和ICC的现代版本都产生了预期的代码.对于像这样的功能:
int RoundToNearestEven(float value)
{
return _mm_cvt_ss2si(_mm_set_ss(value));
}
Run Code Online (Sandbox Code Playgroud)
所有编译器,但微软将发出以下目标代码:
cvtss2si eax, xmm0
ret
Run Code Online (Sandbox Code Playgroud)
而Microsoft的编译器(从VS 2015 Update 3开始)发出以下内容:
movaps xmm1, xmm0
xorps xmm2, xmm2
movss xmm2, xmm1
cvtss2si eax, xmm2
ret
Run Code Online (Sandbox Code Playgroud)
同样被视作双精度版本,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.
在lrint由C标准库提供的功能家庭应该做你想做的,而事实上编译成简单cvt*一些其他的编译器指令,但是是非常微软的编译器次优的.它们永远不会内联,因此您始终需要支付函数调用的成本.另外,函数内部的代码是次优的.这两个都被报告为错误,但我们仍在等待修复.标准库提供的其他转换功能也存在类似的问题,包括lround朋友.
x87 FPU提供执行类似任务的FIST/ FISTP指令,但C和C++语言标准要求使用强制截断,而不是舍入到最接近(默认的FPU舍入模式),因此编译器有义务插入一堆代码,用于更改当前的舍入模式,执行转换,然后将其更改回来.这非常慢,除了使用内联汇编之外,没有办法指示编译器不要这样做.除了64位编译器无法使用内联汇编之外,MSVC的内联汇编语法也无法指定输入和输出,因此您可以双向支付双重负载和存储惩罚.即使不是这种情况,您仍然需要支付将SSE寄存器中的浮点值复制到内存中,然后再复制到x87 FPU堆栈上的成本.
内在函数很棒,并且通常可以让您生成比编译器生成的代码更快的代码,但它们并不完美.如果你像我一样,发现自己经常分析二进制文件的反汇编,你会发现自己经常感到失望.不过,这里你最好的选择是使用内在的.
至于为什么优化器以它的方式发出代码,我只能推测,因为我不在Microsoft编译器团队工作,但我的猜测是因为许多其他cvt*指令具有错误的依赖性,代码 -发电机需要解决.例如,a cvtss2sd不会修改目标XMM寄存器的高64位.这种部分寄存器更新会导致停顿并减少指令级并行的机会.这在循环中尤其是一个问题,其中寄存器的高位形成第二个循环携带的依赖链,即使我们实际上并不关心它们的内容.因为cvtss2sd指令的执行直到前一指令完成才开始,所以延迟大大增加.但是,通过首先执行xorss或movss指令,寄存器的高位被清除,从而破坏了依赖性并避免了停顿的可能性.这是一个有趣的例子,其中较短的代码不等于更快的代码.编译器团队开始在VS 2010附带的编译器中为标量转换插入这些依赖性破坏性指令,并且可能过度应用了启发式算法.
今天发布的 Visual Studio 15.6 似乎终于解决了这个问题。现在我们看到内联该函数时使用了一条指令:
inline int ConvertFloatToRoundedInt(float FloatValue)
{
return _mm_cvt_ss2si(_mm_set_ss(FloatValue)); // Convert to integer with rounding
}
Run Code Online (Sandbox Code Playgroud)
令我印象深刻的是,微软终于得到了全额退款。
| 归档时间: |
|
| 查看次数: |
792 次 |
| 最近记录: |