gpt4 book ai didi

c++ - 成功启用NaN移除方法上的-fno-finite-math-only

转载 作者:太空狗 更新时间:2023-10-29 20:36:28 26 4
gpt4 key购买 nike

在运行优化版代码(以NaNg++ 4.8.2编译)时发现一个使一切变成4.9.3的错误时,我确定问题出在-Ofast选项,特别是它包括的-ffinite-math-only标志。

该代码的一部分包括使用FILE*fscanf读取浮点数,然后将所有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(我的边缘解决方案)编译代码:我怀疑这种优化在程序的其余部分中对我的上下文可能非常有用。
  • 在输入流中手动查找字符串"nan",然后在其中替换值(输入阅读器在库的无关部分中,因此在该测试中包含了较差的设计)。
  • 假定一个特定的浮点架构,并创建自己的isNaN:我可以这样做,但这有点hackerable,不可移植。
  • 使用不带-ffinite-math-only标志的单独编译的程序对数据进行预过滤,然后将其输入到主程序中:维护两个二进制文件并使它们相互通信增加了复杂性,这是不值得的。


  • 编辑:按照接受的答案,这似乎是 g++较旧版本(例如 4.824.9.3)中的编译器“错误”,而在较新版本(例如 5.16.1.1)中已修复。

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

    公认的答案建议使用bit字段执行此操作,但是,编译器将元素放置在bit字段中的顺序是非标准的,并且实际上,在较旧版本的 g++之间进行了更改,甚至拒绝遵守较早版本中的所需位置( 4.8.24.9.3,始终将尾数放在首位),无论它们在代码中出现的顺序如何。

    但是,可以保证使用位操作的解决方案可在所有符合 IEEE754的编译器上使用。下面是我的这种实现,我最终将其用于解决我的问题。它检查 IEEE754是否符合要求,并且我对其进行了扩展,以允许使用double以及其他更多常规的浮点位操作。
    #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 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

    绝对没有区别,因此,这不仅仅是忽略属性的问题。这些较旧的编译器版本显然不支持以函数级粒度控制浮点优化行为。

    但是请注意,在没有 -ffinite-math-only开关的情况下,在GCC 5.1及更高版本上生成的代码仍然比编译函数差很多:

    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标准,就可以保证NaN检查是正确的。这适用于所有版本的GCC和任何其他编译器。

    关于c++ - 成功启用NaN移除方法上的-fno-finite-math-only,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38278300/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com