【发布时间】:2016-11-11 17:02:18
【问题描述】:
在运行我的代码的优化版本(在g++ 4.8.2 和4.9.3 中编译)时,我发现了一个导致所有内容都变成NaNs 的错误,我发现问题出在-Ofast 选项上,具体来说,它包含的 -ffinite-math-only 标志。
代码的一部分涉及使用fscanf 从FILE* 读取浮点数,然后将所有NaNs 替换为数值。然而,正如所料,-ffinite-math-only 启动并删除了这些检查,从而留下了NaNs。
在尝试解决这个问题时,我偶然发现了this,它建议将-fno-finite-math-only 添加为方法属性以禁用对特定方法的优化。下面说明了问题和尝试的修复(实际上并没有修复它):
#include <cstdio>
#include <cmath>
__attribute__((optimize("-fno-finite-math-only")))
void replaceNaN(float * arr, int size, float newValue){
for(int i = 0; i < size; i++) if (std::isnan(arr[i])) arr[i] = newValue;
}
int main(void){
const size_t cnt = 10;
float val[cnt];
for(int i = 0; i < cnt; i++) scanf("%f", val + i);
replaceNaN(val, cnt, -1.0f);
for(int i = 0; i < cnt; i++) printf("%f ", val[i]);
return 0;
}
如果使用echo 1 2 3 4 5 6 7 8 nan 10 | (g++ -ffinite-math-only test.cpp -o test && ./test) 编译/运行,代码不会按预期运行,具体来说,它会输出nan(应该被-1.0f 替换)——如果-ffinite-math-only 标志它表现得很好被省略。这不应该工作吗?我是否遗漏了 gcc 中属性的语法,或者这是上述“与此相关的某些 GCC 版本存在一些问题”之一(来自链接的 SO 问题)
我知道的一些解决方案,但宁愿更清洁/更便携:
- 用
-fno-finite-math-only编译代码(我的临时解决方案):我怀疑这种优化在我的上下文中可能会在程序的其余部分中相当有用; - 在输入流中手动查找字符串
"nan",然后替换那里的值(输入读取器位于库的不相关部分中,导致在其中包含此测试的设计不佳)。 - 假设一个特定的浮点架构并创建我自己的
isNaN:我可以这样做,但它有点hackish且不可移植。 - 使用不带
-ffinite-math-only标志的单独编译程序对数据进行预过滤,然后将其输入主程序:维护两个二进制文件并让它们相互通信所增加的复杂性是不值得的。
编辑:正如已接受的答案所示,这似乎是 g++ 的旧版本中的编译器“错误”,例如 4.82 和 4.9.3,在较新的版本中已修复,例如 @ 987654346@ 和6.1.1。
如果由于某种原因更新编译器不是一个相当容易的选项(例如:没有 root 访问权限),或者将此属性添加到单个函数仍不能完全解决 NaN 检查问题,则另一种解决方案,如果您可以确定代码将始终在IEEE754 浮点环境中运行,即手动检查浮点数的位以获取NaN 签名。
公认的答案建议使用位域来执行此操作,但是,编译器将元素放置在位域中的顺序是非标准的,事实上,g++ 的旧版本和新版本之间会发生变化,甚至拒绝遵守旧版本中所需的定位(4.8.2 和 4.9.3,始终将尾数放在首位),无论它们在代码中出现的顺序如何。
但是,使用位操作的解决方案保证适用于所有IEEE754 兼容的编译器。下面是我的这种实现,我最终用它来解决我的问题。它检查IEEE754 的合规性,并且我已经对其进行了扩展以允许双精度数以及其他更常规的浮点位操作。
#include <limits> // IEEE754 compliance test
#include <type_traits> // enable_if
template<
typename T,
typename = typename std::enable_if<std::is_floating_point<T>::value>::type,
typename = typename std::enable_if<std::numeric_limits<T>::is_iec559>::type,
typename u_t = typename std::conditional<std::is_same<T, float>::value, uint32_t, uint64_t>::type
>
struct IEEE754 {
enum class WIDTH : size_t {
SIGN = 1,
EXPONENT = std::is_same<T, float>::value ? 8 : 11,
MANTISSA = std::is_same<T, float>::value ? 23 : 52
};
enum class MASK : u_t {
SIGN = (u_t)1 << (sizeof(u_t) * 8 - 1),
EXPONENT = ((~(u_t)0) << (size_t)WIDTH::MANTISSA) ^ (u_t)MASK::SIGN,
MANTISSA = (~(u_t)0) >> ((size_t)WIDTH::SIGN + (size_t)WIDTH::EXPONENT)
};
union {
T f;
u_t u;
};
IEEE754(T f) : f(f) {}
inline u_t sign() const { return u & (u_t)MASK::SIGN >> ((size_t)WIDTH::EXPONENT + (size_t)WIDTH::MANTISSA); }
inline u_t exponent() const { return u & (u_t)MASK::EXPONENT >> (size_t)WIDTH::MANTISSA; }
inline u_t mantissa() const { return u & (u_t)MASK::MANTISSA; }
inline bool isNan() const {
return (mantissa() != 0) && ((u & ((u_t)MASK::EXPONENT)) == (u_t)MASK::EXPONENT);
}
};
template<typename T>
inline IEEE754<T> toIEEE754(T val) { return IEEE754<T>(val); }
replaceNaN 函数现在变为:
void replaceNaN(float * arr, int size, float newValue){
for(int i = 0; i < size; i++)
if (toIEEE754(arr[i]).isNan()) arr[i] = newValue;
}
对这些函数的组装的检查表明,正如预期的那样,所有掩码都变成了编译时常量,从而产生了以下(看似)高效的代码:
# In loop of replaceNaN
movl (%rcx), %eax # eax = arr[i]
testl $8388607, %eax # Check if mantissa is empty
je .L3 # If it is, it's not a nan (it's inf), continue loop
andl $2139095040, %eax # Mask leaves only exponent
cmpl $2139095040, %eax # Test if exponent is all 1s
jne .L3 # If it isn't, it's not a nan, so continue loop
这比使用工作位域解决方案(无移位)少一条指令,并且使用了相同数量的寄存器(尽管很容易说这单独使它更有效,但还有其他问题,例如流水线,可能使一种解决方案的效率高于或低于另一种解决方案)。
【问题讨论】:
-
gcc.gnu.org/onlinedocs/gcc/… "此属性仅用于调试目的,不适合生产代码。"
标签: c++ gcc nan compiler-optimization