浮点定向舍入和优化
Floating-point directed roundings and optimization
有以下代码,其中相同的表达式在不同的舍入模式下求值:
#include <iostream>
#include <fenv.h>
#pragma STDC FENV_ACCESS ON
#define SIZE 8
double foo(double * a, double * b){
double sum = 0.0;
for(unsigned int i = 0; i < SIZE; i++) {
sum+= b[i] / a[i];
}
return sum;
}
int main() {
double a[]={127, 131, 137, 139, 149, 151, 157, 163};
double b[SIZE];
for(unsigned int i = 0; i < SIZE; i++){
b[i] = i+1;
}
printf("to nearest: %.18f \n", foo(a, b));
fesetround(FE_TOWARDZERO);
printf("toward zero: %.18f \n", foo(a, b));
fesetround(FE_UPWARD);
printf("to +infinity: %.18f \n", foo(a, b));
fesetround(FE_DOWNWARD);
printf("to -infinity: %.18f \n", foo(a, b));
return 0;
}
用带-O0
选项的g++编译时,输出如下:
to nearest: 0.240773868136782450
toward zero: 0.240773868136782420
to +infinity: 0.240773868136782560
to -infinity: 0.240773868136782420
但是当使用 -O3
选项编译它时,我们有:
to nearest: 0.240773868136782480
toward zero: 0.240773868136782480
to +infinity: 0.240773868136782480
to -infinity: 0.240773868136782480
编译器: g++ (MinGW.org GCC-6.3.0-1) 6.3.0
为什么舍入模式没有改变?如何解决?
(如果 fesetround
在 for
循环(在 foo
函数内)的每次迭代中被调用,那么结果对于任何编译标志都是正确的。)
UPD: 我认为问题是编译器在编译类型中计算了 fesetround
的值,正如@haneefmubarak 在 中指出的那样。问题是如何预防。 (只针对一个命令,fesetround
,不针对整个函数)。
我用 __attribute__ ((noinline))
编写了舍入例程的包装器,并在 main
函数中调用它们:
void __attribute__ ((noinline)) rounddown(){
fesetround(FE_DOWNWARD);
}
void __attribute__ ((noinline)) roundup(){
fesetround(FE_UPWARD);
}
int main() {
...
roundup();
printf("to +infinity: %.18f \n", foo(a, b));
rounddown();
printf("to -infinity: %.18f \n", foo(a, b));
...
}
但它不起作用。有什么想法吗?
UPD2:更清楚的例子:
很容易看出确切的结果:
2/3 + 2/5 + 4/7 + 4/11 = 2.0017316017316017316...
根据问题作者的评论,他们使用的编译器不支持 #pragma STDC FENV_ACCESS ON
并打印一条警告。
代码可能在未优化的版本中“有效”,因为 fesetround
确实改变了硬件中的舍入模式,并且编译器发出直接代码以源代码表示的标称顺序执行操作。
优化代码不起作用的原因可能包括:
- 编译器在编译时执行一些运算,忽略
fesetround
调用。
- 在优化期间,编译器会重新排序操作,可能会以与源代码所示不同的顺序执行算术运算和
fesetround
调用。 fesetround
调用甚至可以完全删除。
C 中可能没有解决此问题的方法。如果编译器不支持访问浮点环境,则可能无法强制它生成必要的代码。声明某些对象 volatile
可能会强制某些操作在执行时按所需顺序执行,但编译器可能仍会根据有关 [=11= 的信息对这些操作重新排序 fesetround
]内置于其中。
可能需要使用汇编语言来执行具有所需舍入模式的浮点运算。
有以下代码,其中相同的表达式在不同的舍入模式下求值:
#include <iostream>
#include <fenv.h>
#pragma STDC FENV_ACCESS ON
#define SIZE 8
double foo(double * a, double * b){
double sum = 0.0;
for(unsigned int i = 0; i < SIZE; i++) {
sum+= b[i] / a[i];
}
return sum;
}
int main() {
double a[]={127, 131, 137, 139, 149, 151, 157, 163};
double b[SIZE];
for(unsigned int i = 0; i < SIZE; i++){
b[i] = i+1;
}
printf("to nearest: %.18f \n", foo(a, b));
fesetround(FE_TOWARDZERO);
printf("toward zero: %.18f \n", foo(a, b));
fesetround(FE_UPWARD);
printf("to +infinity: %.18f \n", foo(a, b));
fesetround(FE_DOWNWARD);
printf("to -infinity: %.18f \n", foo(a, b));
return 0;
}
用带-O0
选项的g++编译时,输出如下:
to nearest: 0.240773868136782450
toward zero: 0.240773868136782420
to +infinity: 0.240773868136782560
to -infinity: 0.240773868136782420
但是当使用 -O3
选项编译它时,我们有:
to nearest: 0.240773868136782480
toward zero: 0.240773868136782480
to +infinity: 0.240773868136782480
to -infinity: 0.240773868136782480
编译器: g++ (MinGW.org GCC-6.3.0-1) 6.3.0
为什么舍入模式没有改变?如何解决?
(如果 fesetround
在 for
循环(在 foo
函数内)的每次迭代中被调用,那么结果对于任何编译标志都是正确的。)
UPD: 我认为问题是编译器在编译类型中计算了 fesetround
的值,正如@haneefmubarak 在 中指出的那样。问题是如何预防。 (只针对一个命令,fesetround
,不针对整个函数)。
我用 __attribute__ ((noinline))
编写了舍入例程的包装器,并在 main
函数中调用它们:
void __attribute__ ((noinline)) rounddown(){
fesetround(FE_DOWNWARD);
}
void __attribute__ ((noinline)) roundup(){
fesetround(FE_UPWARD);
}
int main() {
...
roundup();
printf("to +infinity: %.18f \n", foo(a, b));
rounddown();
printf("to -infinity: %.18f \n", foo(a, b));
...
}
但它不起作用。有什么想法吗?
UPD2:更清楚的例子:
很容易看出确切的结果:
2/3 + 2/5 + 4/7 + 4/11 = 2.0017316017316017316...
根据问题作者的评论,他们使用的编译器不支持 #pragma STDC FENV_ACCESS ON
并打印一条警告。
代码可能在未优化的版本中“有效”,因为 fesetround
确实改变了硬件中的舍入模式,并且编译器发出直接代码以源代码表示的标称顺序执行操作。
优化代码不起作用的原因可能包括:
- 编译器在编译时执行一些运算,忽略
fesetround
调用。 - 在优化期间,编译器会重新排序操作,可能会以与源代码所示不同的顺序执行算术运算和
fesetround
调用。fesetround
调用甚至可以完全删除。
C 中可能没有解决此问题的方法。如果编译器不支持访问浮点环境,则可能无法强制它生成必要的代码。声明某些对象 volatile
可能会强制某些操作在执行时按所需顺序执行,但编译器可能仍会根据有关 [=11= 的信息对这些操作重新排序 fesetround
]内置于其中。
可能需要使用汇编语言来执行具有所需舍入模式的浮点运算。