gcc -mno-sse2舍入

use*_*511 6 c gcc compilation rounding sse2

我正在做一个我做RGB转换为luma的项目,而且我对-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;
}
Run Code Online (Sandbox Code Playgroud)

而这就是我得到的:

user@gentoo>gcc -mno-sse2 test.c -o test && ./test
241
user@gentoo> gcc test.c -o test && ./test
242
Run Code Online (Sandbox Code Playgroud)

我想gcc使用sse2优化进行double乘法,但我没有得到的是为什么优化版本是正确的.

另外,您建议我使用什么来获得更一致的结果,ceil()或者floor()

Pet*_*des 0

TL:DR 使用lrint(x)or(int)rint(x)将 float 转换为 int,并舍入到最接近的值而不是截断。不幸的是,并非所有编译器都能有效地内联相同的数学函数。C++ 中的 float参见round()


gcc -mno-sse2double即使在 64 位代码中,也必须使用 x87 。x87 寄存器的内部精度为 80 位,但 SSE2 在 XMM 寄存器中本地使用IEEE binary64(又名double格式,因此所有临时值在每一步都会舍入为 64 位double

这个问题并不像双舍入问题(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.