在運行我的代碼的優化版本(編譯在g++ 4.8.2
和4.9.3
中)時,發現一個變爲NaN
的錯誤,我發現問題是-Ofast
選項,特別是它包含的-ffinite-math-only
標誌。僅在NaN去除方法中成功啓用-fno-finite-math-
代碼的一部分涉及使用fscanf
從FILE*
讀取浮動塊,然後用數字值替換所有NaN
。然而,如所預料的那樣,-ffinite-math-only
開始,並且移除這些檢查,從而留下NaN
。
爲了解決這個問題,我偶然發現了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
(我interrim解決方案)的代碼:我懷疑這種優化可能是我在程序的其餘方面相當有用; - 在輸入流中手動查找字符串
"nan"
,然後替換那裏的值(輸入讀取器位於庫的不相關部分,產生不良設計以在其中包含此測試)。 - 假設一個特定的浮點體系結構並製作我自己的
isNaN
:我可能會這樣做,但它有點冒險和不可移植。 - 使用不帶
-ffinite-math-only
標記的單獨編譯的程序預過濾數據,然後將其提供給主程序:維護兩個二進制文件並使它們彼此交談的額外複雜性不值得。
編輯:放於接受的答案,這似乎這是在舊版本的g++
編譯器「錯誤」,如4.82
和4.9.3
,即固定在新版本中,如5.1
和6.1.1
。
如果出於某種原因更新編譯器不是一個比較簡單的選項(例如:無根訪問),或者將此屬性添加到單個函數中仍然不能完全解決檢查問題NaN
,可以確定代碼將始終運行在浮點環境IEEE754
中,手動檢查float的位以獲得NaN
簽名。
接受的答案建議使用位字段來完成此操作,但是,編譯器將位元素放置在位字段中的順序是非標準的,實際上,舊版本和更新版本之間的更改拒絕遵守舊版本中的理想定位(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
這比用一個工作位字段溶液(無偏移),和相同數量的寄存器用於少一個指令(儘管說這樣做會讓它更有效率,但還有其他一些問題,例如流水線問題,這可能會導致一種解決方案比另一種解決方案效率更高或更低)。
https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#index-g_t_0040code_007boptimize_007d-function-attribute-3278「該屬性僅用於調試目的,不適用於生產代碼「。 –