与 GCC 的关联数学
associative-math with GCC
我在 C 中创建了一个 double-double 数据类型。我尝试使用 GCC -Ofast
并发现它快得多(例如,使用 -O3
1.5 秒,使用 [= 0.3 秒13=]) 但结果是假的。我追查到 -fassociative-math
。我很惊讶这不起作用,因为我在重要时明确定义了我的操作的关联性。例如,在下面的代码中,我在重要的地方加上了括号。
static inline doublefloat two_sum(const float a, const float b) {
float s = a + b;
float v = s - a;
float e = (a - (s - v)) + (b - v);
return (doublefloat){s, e};
}
所以我不希望 GCC 发生变化,例如(a - (s - v))
到 ((a + v) - s)
即使 -fassociative-math
。那么,为什么使用 -fassociative-math
结果如此错误(而且速度如此之快)?
我尝试 /fp:fast
使用 MSVC(在将我的代码转换为 C++ 之后),结果是正确的,但它并不比 /fp:precise
.
快
GCC 手册中关于 -fassociative-math
的说明
Allow re-association of operands in series of floating-point operations. This violates the ISO C and C++ language standard by possibly changing computation result. NOTE: re-ordering may change the sign of zero as well
as ignore NaNs and inhibit or create underflow or overflow (and thus cannot be used on code that relies on rounding behavior like "(x + 2^52) - 2^52". May also reorder floating-point comparisons and thus may not be
used when ordered comparisons are required. This option requires that both -fno-signed-zeros and -fno-trapping-math be in effect. Moreover, it doesn't make much sense with -frounding-math.
编辑:
我对整数(有符号和无符号)和浮点数进行了一些测试,以检查 GCC 是否简化了关联运算。这是我测试的代码
//test1.c
unsigned foosu(unsigned a, unsigned b, unsigned c) { return (a + c) - b; }
signed fooss(signed a, signed b, signed c) { return (a + c) - b; }
float foosf(float a, float b, float c) { return (a + c) - b; }
unsigned foomu(unsigned a, unsigned b, unsigned c) { return a*a*a*a*a*a; }
signed fooms(signed a, signed b, signed c) { return a*a*a*a*a*a; }
float foomf(float a, float b, float c) { return a*a*a*a*a*a; }
和
//test2.c
unsigned foosu(unsigned a, unsigned b, unsigned c) { return a - (b - c); }
signed fooss(signed a, signed b, signed c) { return a - (b - c); }
float foosf(float a, float b, float c) { return a - (b - c); }
unsigned foomu(unsigned a, unsigned b, unsigned c) { return (a*a*a)*(a*a*a); }
signed fooms(signed a, signed b, signed c) { return (a*a*a)*(a*a*a); }
float foomf(float a, float b, float c) { return (a*a*a)*(a*a*a); }
我遵守了 -O3
和 -Ofast
并查看了生成的程序集,这就是我观察到的结果
- 无符号:加法和乘法的代码相同(减少为三个乘法)
- 有符号:代码与加法不同,但与乘法相同(减少为三个乘法)
- float:代码与
-O3
的加法或乘法不同,但是与 -Ofast
的加法相同,乘法几乎相同,仅使用三个乘法。
由此我得出结论
- 如果一个操作是关联的,那么 GCC 将根据它的选择简化它,以便
a - (b - c)
可以变成 (a + c) - b
。
- 无符号加法和乘法是结合的
- 有符号加法不是结合的
- 带符号的乘法是结合的
a*a*a*a*a*a
在使用 -fassociative-math
. 时简化为只有整数和浮点数的三个乘法
-fassociative-math
使浮点加法和乘法结合。
换句话说,GCC 所做的正是我没有预料到的 -fassociative-math
。它将 (a - (s - v))
转换为 ((a + v) - s)
.
人们可能认为这在 -fassociative-math
中很明显,但在某些情况下,程序员可能希望浮点数在一种情况下是关联的,而在另一种情况下是非关联的。 For example auto-vectorization and reducing a floating point array requires -fassociative-math
但如果这样做,双浮点数就不能在同一个模块中使用。所以唯一的选择是将关联浮点函数放在一个模块中,将非关联浮点函数放在另一个模块中,然后将它们编译成单独的目标文件。
I'm surprised this does not work because I explicitly define the associativity of my operations when it matters. For example in the following code I but parentheses where it matters.
这正是 -fassociative-math
所做的:它忽略您的程序定义的顺序(与没有括号的定义一样),而是执行允许简化的操作。通常,对于 double-double 加法,误差项计算为 0,因为如果浮点运算是关联的,它就等于 0。 e = 0;
比e = (a - …;
快多了,当然,只是错了
在C99标准中,6.5.6:1中的以下语法规则暗示x + y + z
只能解析为(x + y) + z
:
additive-expression:
multiplicative-expression
additive-expression + multiplicative-expression
additive-expression - multiplicative-expression
对中间左值的显式括号和赋值不会阻止 -fassociative-math
执行其操作。即使没有它们也定义了顺序(在一系列加法和减法的情况下从左到右),并且您告诉编译器忽略定义的顺序。事实上,在应用优化的中间表示上,我怀疑该顺序是由中间赋值、括号或语法强加的信息是否仍然存在。
您可以尝试将所有您希望按照 C 标准强加的顺序编译的函数放在同一个编译单元中,您将在不使用 -fassociative-math
的情况下进行编译,或者在整个程序中完全避免使用此标志.如果你坚持在用 -fassociative-math
编译的编译单元中保留双双加法,你可以尝试使用 volatile
变量,但是 volatile
类型限定符只使对左值的访问成为可观察的事件,它不会强制进行正确的计算。
我在 C 中创建了一个 double-double 数据类型。我尝试使用 GCC -Ofast
并发现它快得多(例如,使用 -O3
1.5 秒,使用 [= 0.3 秒13=]) 但结果是假的。我追查到 -fassociative-math
。我很惊讶这不起作用,因为我在重要时明确定义了我的操作的关联性。例如,在下面的代码中,我在重要的地方加上了括号。
static inline doublefloat two_sum(const float a, const float b) {
float s = a + b;
float v = s - a;
float e = (a - (s - v)) + (b - v);
return (doublefloat){s, e};
}
所以我不希望 GCC 发生变化,例如(a - (s - v))
到 ((a + v) - s)
即使 -fassociative-math
。那么,为什么使用 -fassociative-math
结果如此错误(而且速度如此之快)?
我尝试 /fp:fast
使用 MSVC(在将我的代码转换为 C++ 之后),结果是正确的,但它并不比 /fp:precise
.
GCC 手册中关于 -fassociative-math
的说明
Allow re-association of operands in series of floating-point operations. This violates the ISO C and C++ language standard by possibly changing computation result. NOTE: re-ordering may change the sign of zero as well as ignore NaNs and inhibit or create underflow or overflow (and thus cannot be used on code that relies on rounding behavior like "(x + 2^52) - 2^52". May also reorder floating-point comparisons and thus may not be used when ordered comparisons are required. This option requires that both -fno-signed-zeros and -fno-trapping-math be in effect. Moreover, it doesn't make much sense with -frounding-math.
编辑:
我对整数(有符号和无符号)和浮点数进行了一些测试,以检查 GCC 是否简化了关联运算。这是我测试的代码
//test1.c
unsigned foosu(unsigned a, unsigned b, unsigned c) { return (a + c) - b; }
signed fooss(signed a, signed b, signed c) { return (a + c) - b; }
float foosf(float a, float b, float c) { return (a + c) - b; }
unsigned foomu(unsigned a, unsigned b, unsigned c) { return a*a*a*a*a*a; }
signed fooms(signed a, signed b, signed c) { return a*a*a*a*a*a; }
float foomf(float a, float b, float c) { return a*a*a*a*a*a; }
和
//test2.c
unsigned foosu(unsigned a, unsigned b, unsigned c) { return a - (b - c); }
signed fooss(signed a, signed b, signed c) { return a - (b - c); }
float foosf(float a, float b, float c) { return a - (b - c); }
unsigned foomu(unsigned a, unsigned b, unsigned c) { return (a*a*a)*(a*a*a); }
signed fooms(signed a, signed b, signed c) { return (a*a*a)*(a*a*a); }
float foomf(float a, float b, float c) { return (a*a*a)*(a*a*a); }
我遵守了 -O3
和 -Ofast
并查看了生成的程序集,这就是我观察到的结果
- 无符号:加法和乘法的代码相同(减少为三个乘法)
- 有符号:代码与加法不同,但与乘法相同(减少为三个乘法)
- float:代码与
-O3
的加法或乘法不同,但是与-Ofast
的加法相同,乘法几乎相同,仅使用三个乘法。
由此我得出结论
- 如果一个操作是关联的,那么 GCC 将根据它的选择简化它,以便
a - (b - c)
可以变成(a + c) - b
。 - 无符号加法和乘法是结合的
- 有符号加法不是结合的
- 带符号的乘法是结合的
a*a*a*a*a*a
在使用-fassociative-math
. 时简化为只有整数和浮点数的三个乘法
-fassociative-math
使浮点加法和乘法结合。
换句话说,GCC 所做的正是我没有预料到的 -fassociative-math
。它将 (a - (s - v))
转换为 ((a + v) - s)
.
人们可能认为这在 -fassociative-math
中很明显,但在某些情况下,程序员可能希望浮点数在一种情况下是关联的,而在另一种情况下是非关联的。 For example auto-vectorization and reducing a floating point array requires -fassociative-math
但如果这样做,双浮点数就不能在同一个模块中使用。所以唯一的选择是将关联浮点函数放在一个模块中,将非关联浮点函数放在另一个模块中,然后将它们编译成单独的目标文件。
I'm surprised this does not work because I explicitly define the associativity of my operations when it matters. For example in the following code I but parentheses where it matters.
这正是 -fassociative-math
所做的:它忽略您的程序定义的顺序(与没有括号的定义一样),而是执行允许简化的操作。通常,对于 double-double 加法,误差项计算为 0,因为如果浮点运算是关联的,它就等于 0。 e = 0;
比e = (a - …;
快多了,当然,只是错了
在C99标准中,6.5.6:1中的以下语法规则暗示x + y + z
只能解析为(x + y) + z
:
additive-expression: multiplicative-expression additive-expression + multiplicative-expression additive-expression - multiplicative-expression
对中间左值的显式括号和赋值不会阻止 -fassociative-math
执行其操作。即使没有它们也定义了顺序(在一系列加法和减法的情况下从左到右),并且您告诉编译器忽略定义的顺序。事实上,在应用优化的中间表示上,我怀疑该顺序是由中间赋值、括号或语法强加的信息是否仍然存在。
您可以尝试将所有您希望按照 C 标准强加的顺序编译的函数放在同一个编译单元中,您将在不使用 -fassociative-math
的情况下进行编译,或者在整个程序中完全避免使用此标志.如果你坚持在用 -fassociative-math
编译的编译单元中保留双双加法,你可以尝试使用 volatile
变量,但是 volatile
类型限定符只使对左值的访问成为可观察的事件,它不会强制进行正确的计算。