inline int round(float a) { int i; __asm { fld a fistp i } return i; }
我的第一个冲动是丢弃它并用(int)std :: round替换调用(前C 11,如果今天发生的话会使用std :: lround),但过了一段时间我开始怀疑它是否有一些优点毕竟…
这个函数的用例都是[-100,100]中的值,所以即使int8_t也足够宽以保存结果.然而,fistp需要至少32位的内存变量,因此小于int32_t就像浪费更多.
现在,很明显将float转换为int并不是最快的方法,因为舍入模式必须按照标准切换到截断,然后再返回. C 11提供了std :: lround函数,它可以缓解这一特定问题,但考虑到值通过float-> long-> int而不是直接到达应该的位置,它似乎仍然更浪费.
另一方面,在函数中使用内联ASM,编译器无法将i优化到寄存器中(即使它可以,help期望一个内存变量),所以std :: lround看起来并不太糟糕……
然而,我所遇到的最紧迫的问题是假设(如此函数所做的那样)是多么安全,舍入模式将始终是舍入到最接近的,因为它显然是(没有检查).由于std :: lround必须保证独立于舍入模式的某种行为,这个假设,只要它成立,似乎总是使内联ASM轮更好的选择.
另外我很不清楚std :: fesetround设置的舍入模式和std :: lround替代std :: lrint使用的舍入模式以及help ASM指令中使用的舍入模式是否保证相同或至少同步.
这些是我的考虑因素,也就是我不知道如何做出保留或更换功能的明智决定.
现在回答问题:
根据对这些考虑因素的更为明智的看法或我没有想到的这些考虑因素,使用此功能似乎是否合适?
如果有风险,风险有多大?
推理是否存在为什么它不会比std :: lround或std :: lrint快?
没有性能成本可以进一步改进吗?
如果程序是为x86-64编译的,这种推理是否有任何改变?
解决方法
当你可以避免它时,不要使用内联asm.编译器不“理解”它的作用,因此无法通过它进行优化.例如如果在某个地方内联该函数使其参数成为编译时常量,那么它仍将fld一个常量并将其压入内存,然后将其加载回整数寄存器.纯C将让编译器传播常量并且只传播mov r32,imm32,或者进一步传播常量并将其折叠成其他东西.更不用说CSE,并将转换提升出循环. (MSVC inline asm doesn’t let you specify that an asm block is a pure function,and only needs to be run if the output value is needed,and that it doesn’t depend on a global. GNU C inline asm允许该部分,但它仍然是一个糟糕的选择,因为它对编译器不透明).
The GCC wiki even has a page on this subject,解释与前一段(以及更多)相同的内容,因此内联asm绝对应该是最后的手段.
在这种情况下,我们可以让编译器从纯C中发出好的代码,所以我们绝对应该这样做.
使用当前舍入模式的Float-> int只需要一条机器指令(见下文),但诀窍是让编译器发出它(只有它).将数学库函数设置为内联可能很棘手,因为在某些情况下,它们中的一些必须设置errno和/或引发不精确的异常. (-fno-math-errno可以提供帮助,如果你不能使用完整的-ffast-math或MSVC等价物)
但是,它并不理想:float-> long-> int与它们的大小不同时直接与int不同. x86-64 SystemV ABI(除了Windows之外的所有东西都使用)有64位长.
64位长度改变了lrint的溢出语义:而不是获得0x80000000(在带有SSE指令的x86上),你将获得长的低32位(如果值超出长的范围,则将为全零).
此lrintf不会自动向量化(除非编译器可以证明浮点数将在范围内),因为只有标量而非SIMD指令将浮点数或双精度转换为压缩的64位整数(until AVX512DQ). C数学库函数的IDK直接转换为int,但你可以使用(int)nearbyintf(x),它可以在64位代码中更容易地自动矢量化.请参阅下面的部分,了解gcc和clang如何做到这一点.
然而,除了击败自动矢量化之外,对于任何现代微体系结构的cvtss2si rax,xmm0都没有直接的速度惩罚(见Agner Fog’s insn tables).它只需花费REX前缀的额外指令字节.
在AArch64(又名ARM64),gcc4.8 compiles lround
into a single fcvtas x0,s0
instruction,所以我猜ARM64在硬件中提供了时髦的舍入模式(但x86没有).奇怪的是,-ffast-math使内联函数更少,但这与笨重的旧gcc4.8有关.对于ARM(不是64),即使使用-mfloat-abi = hard -mhard-float -march = armv7-a,gcc4.8也不会内联任何内容.也许那些不是正确的选择; IDK ARM非常好:/
如果您要进行大量转换,可以使用SSE / AVX内在函数like _mm_cvtps_epi32
(cvtps2dq)手动为x86进行矢量化,甚至将生成的32位整数元素打包为16位或8位(使用packssdw.但是,使用纯C编译器可以自动矢量化是一个很好的计划,因为它是可移植的.
lrintf
#include <math.h> int round_to_nearest(float f) { // default mode is always nearest return lrintf(f); }
编译器输出the Godbolt Compiler explorer:
########### Without -ffast-math ############# cvtss2si eax,xmm0 # gcc 6.1 (-O3 -mx32,so long is 32bit) cvtss2si rax,xmm0 # gcc 4.4 through 6.1 (-O3). can't auto-vectorize,though. jmp lrintf # clang 3.8 (-O3 -msse4.1),still tail-calls the function :/ ###### With -ffast-math ######### jmp lrintf # clang 3.8 (-O3 -msse4.1 -ffast-math)
所以显然clang并不能很好地应对它,但即使是古老的gcc也很棒,即使没有-ffast-math也能做得很好.
Don’t use roundf
/lroundf
:它具有非标准的舍入语义(距离0的中间情况,而不是偶数). This leads to worse x86 asm,但实际上更好的ARM64 asm.那么也许可以将它用于ARM?但它确实具有固定的舍入行为,而不是使用当前的舍入模式.
如果你想将返回值作为一个浮点数而不是转换为int,那么use nearbyintf
可能会更好.当输出!=输入时,rint必须提高FP不精确异常. (但SSE4.1 roundss可以实现其立即控制字节的第3位的行为).
直接将nearbyint()截断为int.
#include <math.h> int round_to_nearest(float f) { return nearbyintf(f); }
编译器输出the Godbolt Compiler explorer.
######## With -ffast-math ############ cvtss2si eax,xmm0 # gcc 4.8 through 6.1 (-O3 -ffast-math) # clang is dumb and won't fold the roundss into the cvt. Without sse4.1,it's a function call roundss xmm0,xmm0,12 # clang 3.5 to 3.8 (-O3 -ffast-math -msse4.1) cvttss2si eax,xmm0 roundss xmm1,12 # ICC13 (-O3 -msse4.1 -ffast-math) cvtss2si eax,xmm1 ######## WITHOUT -ffast-math ############ sub rsp,8 call nearbyintf # gcc 6.1 (-O3 -msse4.1) add rsp,8 # and clang without -msse4.1 cvttss2si eax,xmm0 roundss xmm0,12 # clang3.2 and later (-O3 -msse4.1) cvttss2si eax,12 # ICC13 (-O3 -msse4.1) cvtss2si eax,xmm1
Gcc 4.7及更早版本:只是没有-msse4.1的cvttss2si,但如果SSE4.1可用,则会发出一个回合.它的附近定义必须使用inline-asm,因为asm语法在intel-Syntax输出中被破坏.可能这是它插入的方式,然后当它意识到它转换为int时没有被优化掉.
它在asm中是如何工作的
Now,quite obvIoUsly casting the float to int is not the fastest way to do things,as for that the rounding mode has to be switched to truncate,as per the standard,and back afterwards.
如果您的目标是没有SSE的20年前的cpu,那就是这样. (你说浮动,而不是双倍,所以我们只需要SSE,而不是SSE2.没有SSE2的最老的cpu是Athlon XP).
现代系统在xmm寄存器中执行浮点运算. SSE有转换scalar float to signed int with truncation (cvttss2si
)或with the current counting mode (cvtss2si
)的指令.(注意第一个中Truncate的额外t.其余的助记符是Convert Scalar Single-precision to Signed Integer.)有类似的double指令,x86-64允许目标是64位整数寄存器.
cvtss2si基本上存在是因为C的浮点数为int的默认行为.更改舍入模式的速度很慢,因此英特尔提供了一种不吸引人的方法.
我认为即使32位版本的现代Windows也需要足够新的硬件才能拥有SSE2,以防万一对任何人都很重要. (SSE2是AMD64 ISA的一部分,64位调用约定甚至在xmm寄存器中传递float / double args).