我正在做一个将RGB转换为亮度的项目,并且-mno-sse2标志存在一些舍入问题:
这是测试代码:
#include <stdio.h>
#include <stdint.h>
static double rec709_luma_coeff[3] = {0.2126, 0.7152, 0.0722};
int main()
{
uint16_t n = 242 * rec709_luma_coeff[0] + 242 * rec709_luma_coeff[1] + 242 * rec709_luma_coeff[2];
printf("%u\n", n);
return 0;
}
这就是我得到的:
user@gentoo>gcc -mno-sse2 test.c -o test && ./test
241
user@gentoo> gcc test.c -o test && ./test
242
我想gcc对
double
乘法使用sse2优化,但是我不明白的是为什么优化的版本是正确的。另外,您建议我使用什么来获得更一致的结果,
ceil()
或floor()
? 最佳答案
TL:DR使用lrint(x)
或(int)rint(x)
通过舍入到最近(而不是截断)从float到int转换。不幸的是,尽管如此,并非所有编译器都能有效地内联相同的数学函数。参见round() for float in C++gcc -mno-sse2
必须将x87用作double
,即使在64位代码中也是如此。 x87寄存器的内部精度为80位,但是SSE2在XMM寄存器中 native 使用IEEE binary64 (aka double
)格式,因此所有临时变量在每一步都四舍五入为64位double
。
问题并不像the double rounding problem(80位-> 64位,然后是整数)那么有趣。在将临时变量存储到内存时,它也不来自gcc -O0
(默认值:没有额外的优化)四舍五入,因为您在一个C语句中完成了全部操作,因此只对整个表达式使用x87寄存器。
只是 80位精度导致结果低于242.0,并被C的float-> int语义截断为241 ,而SSE2产生的结果恰好高于242.0,截断为242。对于x87,四舍五入为对于从1到65535的任何输入,下一个较低的整数始终出现,而不仅仅是242。(我使用atoi(argv[1])
创建了您的程序版本,以便可以使用-O3
测试其他值)。
请记住int foo = 123.99999
为123,因为C使用“截断”舍入模式(向零)。对于非负数,这与floor
(四舍五入为-Infinity)相同。 https://en.wikipedia.org/wiki/Floating-point_arithmetic#Rounding_modes。double
不能完全代表系数:我用gdb
打印它们并得到:{0.21260000000000001, 0.71519999999999995, 0.0722}
。这些十进制表示形式可能不是以2为基数的浮点值的精确表示形式。但是它们足够接近,可以看到这些系数加起来等于0.99999999999999996
(使用任意精度计算器)。
因为x87的内部精度高于系数的精度,所以我们进行了舍入舍入,因此n * rec709_luma_coeff[0]
等的总舍入误差以及对结果求和的〜2^11
小于系数和与之和之间的差。 1.0。 (有效位为64位,而有效位为53位)。
真正的问题是SSE2版本如何工作!在足够多的情况下,至少在242种情况下,临时临时值可能会向上舍入到偶数。在更多情况下,它会产生原始输入,但对于5、7、10、13,它会产生输入1。 14,20 ...(从1..1000开始的前1000个数字中有252个被SSE2版本“删减”了,因此也不总是可以使用。)
使用-O3
作为您的源代码,它可以在编译时以更高的精度进行计算并产生准确的结果。即它的编译方式与printf("%u\n", n);
相同。
顺便说一句,您应该为常量使用static
const
,以便gcc可以更好地进行优化。 static
比普通全局更好,因为编译器可以看到编译单元中什么也没有写入值或将它们的地址传递到任何地方,因此它可以将它们视为const
。
关于c - gcc -mno-sse2取整,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/35069186/