【问题标题】:Successfully enabling -fno-finite-math-only on NaN removal method在 NaN 去除方法上成功启用 -fno-finite-math-only
【发布时间】:2016-11-11 17:02:18
【问题描述】:

在运行我的代码的优化版本(在g++ 4.8.24.9.3 中编译)时,我发现了一个导致所有内容都变成NaNs 的错误,我发现问题出在-Ofast 选项上,具体来说,它包含的 -ffinite-math-only 标志。

代码的一部分涉及使用fscanfFILE* 读取浮点数,然后将所有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 &amp;&amp; ./test) 编译/运行,代码不会按预期运行,具体来说,它会输出nan(应该被-1.0f 替换)——如果-ffinite-math-only 标志它表现得很好被省略。这不应该工作吗?我是否遗漏了 gcc 中属性的语法,或者这是上述“与此相关的某些 GCC 版本存在一些问题”之一(来自链接的 SO 问题)

我知道的一些解决方案,但宁愿更清洁/更便携:

  • -fno-finite-math-only 编译代码(我的临时解决方案):我怀疑这种优化在我的上下文中可能会在程序的其余部分中相当有用;
  • 在输入流中手动查找字符串 "nan",然后替换那里的值(输入读取器位于库的不相关部分中,导致在其中包含此测试的设计不佳)。
  • 假设一个特定的浮点架构并创建我自己的isNaN:我可以这样做,但它有点hackish且不可移植。
  • 使用不带-ffinite-math-only 标志的单独编译程序对数据进行预过滤,然后将其输入主程序:维护两个二进制文件并让它们相互通信所增加的复杂性是不值得的。

编辑:正如已接受的答案所示,这似乎是 g++ 的旧版本中的编译器“错误”,例如 4.824.9.3,在较新的版本中已修复,例如 @ 987654346@ 和6.1.1

如果由于某种原因更新编译器不是一个相当容易的选项(例如:没有 root 访问权限),或者将此属性添加到单个函数仍不能完全解决 NaN 检查问题,则另一种解决方案,如果您可以确定代码将始终在IEEE754 浮点环境中运行,即手动检查浮点数的位以获取NaN 签名。

公认的答案建议使用位域来执行此操作,但是,编译器将元素放置在位域中的顺序是非标准的,事实上,g++ 的旧版本和新版本之间会发生变化,甚至拒绝遵守旧版本中所需的定位(4.8.24.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

这比使用工作位域解决方案(无移位)少一条指令,并且使用了相同数量的寄存器(尽管很容易说这单独使它更有效,但还有其他问题,例如流水线,可能使一种解决方案的效率高于或低于另一种解决方案)。

【问题讨论】:

标签: c++ gcc nan compiler-optimization


【解决方案1】:

对我来说看起来像是一个编译器错误。在 GCC 4.9.2 之前,该属性被完全忽略。 GCC 5.1 及更高版本注意它。也许是时候升级你的编译器了?

__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;
}

在 GCC 4.9.2 上使用 -ffinite-math-only 编译:

replaceNaN(float*, int, float):
        rep ret

但在 GCC 5.1 上使用完全相同的设置:

replaceNaN(float*, int, float):
        test    esi, esi
        jle     .L26
        sub     rsp, 8
        call    std::isnan(float) [clone .isra.0]
        test    al, al
        je      .L2
        mov     rax, rdi
        and     eax, 15
        shr     rax, 2
        neg     rax
        and     eax, 3
        cmp     eax, esi
        cmova   eax, esi
        cmp     esi, 6
        jg      .L28
        mov     eax, esi
.L5:
        cmp     eax, 1
        movss   DWORD PTR [rdi], xmm0
        je      .L16
        cmp     eax, 2
        movss   DWORD PTR [rdi+4], xmm0
        je      .L17
        cmp     eax, 3
        movss   DWORD PTR [rdi+8], xmm0
        je      .L18
        cmp     eax, 4
        movss   DWORD PTR [rdi+12], xmm0
        je      .L19
        cmp     eax, 5
        movss   DWORD PTR [rdi+16], xmm0
        je      .L20
        movss   DWORD PTR [rdi+20], xmm0
        mov     edx, 6
.L7:
        cmp     esi, eax
        je      .L2
.L6:
        mov     r9d, esi
        lea     r8d, [rsi-1]
        mov     r11d, eax
        sub     r9d, eax
        lea     ecx, [r9-4]
        sub     r8d, eax
        shr     ecx, 2
        add     ecx, 1
        cmp     r8d, 2
        lea     r10d, [0+rcx*4]
        jbe     .L9
        movaps  xmm1, xmm0
        lea     r8, [rdi+r11*4]
        xor     eax, eax
        shufps  xmm1, xmm1, 0
.L11:
        add     eax, 1
        add     r8, 16
        movaps  XMMWORD PTR [r8-16], xmm1
        cmp     ecx, eax
        ja      .L11
        add     edx, r10d
        cmp     r9d, r10d
        je      .L2
.L9:
        movsx   rax, edx
        movss   DWORD PTR [rdi+rax*4], xmm0
        lea     eax, [rdx+1]
        cmp     eax, esi
        jge     .L2
        add     edx, 2
        cdqe
        cmp     esi, edx
        movss   DWORD PTR [rdi+rax*4], xmm0
        jle     .L2
        movsx   rdx, edx
        movss   DWORD PTR [rdi+rdx*4], xmm0
.L2:
        add     rsp, 8
.L26:
        rep ret
.L28:
        test    eax, eax
        jne     .L5
        xor     edx, edx
        jmp     .L6
.L20:
        mov     edx, 5
        jmp     .L7
.L19:
        mov     edx, 4
        jmp     .L7
.L18:
        mov     edx, 3
        jmp     .L7
.L17:
        mov     edx, 2
        jmp     .L7
.L16:
        mov     edx, 1
        jmp     .L7

在 GCC 6.1 上的输出相似,但并不完全相同。

将属性替换为

#pragma GCC push_options
#pragma GCC 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;
}
#pragma GCC pop_options

完全没有区别,所以这不仅仅是属性被忽略的问题。这些旧版本的编译器显然不支持在函数级粒度上控制浮点优化行为。

但是,请注意,在 GCC 5.1 及更高版本上生成的代码仍然明显比不使用 -ffinite-math-only 开关编译函数差:

replaceNaN(float*, int, float):
        test    esi, esi
        jle     .L1
        lea     eax, [rsi-1]
        lea     rax, [rdi+4+rax*4]
.L5:
        movss   xmm1, DWORD PTR [rdi]
        ucomiss xmm1, xmm1
        jnp     .L6
        movss   DWORD PTR [rdi], xmm0
.L6:
        add     rdi, 4
        cmp     rdi, rax
        jne     .L5
        rep ret
.L1:
        rep ret

我不知道为什么会有这样的差异。有些东西严重地让编译器脱离了它的游戏;这比完全禁用优化得到的代码还要糟糕。如果我不得不猜测,我会推测它是std::isnan 的实现。如果这个replaceNaN 方法对速度不是很重要,那么它可能并不重要。如果您需要重复解析文件中的值,您可能更希望有一个相当有效的实现。

就我个人而言,我会编写自己的std::isnan 的非便携式实现。 IEEE 754 格式都有很好的文档记录,假设您对代码进行了彻底的测试和注释,我看不出这有什么危害,除非您绝对需要将代码移植到所有不同的体系结构中。它将把纯粹主义者逼上绝路,但也应该使用像-ffinite-math-only 这样的非标准选项。对于single-precision float,类似:

bool my_isnan(float value)
{
  union IEEE754_Single
  {
    float f;
    struct
    {
    #if BIG_ENDIAN
        uint32_t sign     : 1;
        uint32_t exponent : 8;
        uint32_t mantissa : 23;
    #else
        uint32_t mantissa : 23;
        uint32_t exponent : 8;
        uint32_t sign     : 1;
    #endif
    } bits;
  } u = { value };

  // In the IEEE 754 representation, a float is NaN when
  // the mantissa is non-zero, and the exponent is all ones
  // (2^8 - 1 == 255).
  return (u.bits.mantissa != 0) && (u.bits.exponent == 255);
}

现在,不需要注释,只需使用my_isnan 而不是std::isnan。在启用-ffinite-math-only 的情况下编译时会生成以下目标代码:

replaceNaN(float*, int, float):
        test    esi, esi
        jle     .L6
        lea     eax, [rsi-1]
        lea     rdx, [rdi+4+rax*4]
.L13:
        mov     eax, DWORD PTR [rdi]     ; get original floating-point value
        test    eax, 8388607             ; test if mantissa != 0
        je      .L9
        shr     eax, 16                  ; test if exponent has all bits set
        and     ax, 32640
        cmp     ax, 32640
        jne     .L9
        movss   DWORD PTR [rdi], xmm0    ; set newValue if original was NaN
.L9:
        add     rdi, 4
        cmp     rdx, rdi
        jne     .L13
        rep ret
.L6:
        rep ret

NaN 检查比简单的ucomiss 稍复杂一些,然后是奇偶校验标志测试,但只要您的编译器符合 IEEE 754 标准,就可以保证它是正确的。这适用于所有版本的 GCC,以及任何其他编译器。

【讨论】:

  • 非常彻底!但是,我应该注意,位字段方法不能保证适用于所有符合 IEEE754 的编译器,因为该标准没有为联合成员定义特定的顺序,实际上它没有,例如我的 @987654339 @ 和 4.9.3 安装,它决定将 bits.mantissa 放在字段的开头(无论它们在代码中的顺序如何)。然而,位域方法在 MSVC 和g++ 6.1.1 中运行良好。但是,对位掩码执行相同操作应该在所有符合 IEEE754 的编译器中都可以正常工作。解决这个问题,这将是正确的答案!
猜你喜欢
  • 1970-01-01
  • 2018-10-23
  • 1970-01-01
  • 2021-11-02
  • 2019-12-31
  • 1970-01-01
  • 2012-04-26
  • 1970-01-01
  • 2017-08-12
相关资源
最近更新 更多