不同CPU的FMA指令的中间精度是否不同?如果是,那么编译器如何均衡浮点行为?

Do FMA instructions of different CPUs have different intermediate accuracy? If yes, then how does a compiler equalize the floating-point behavior?

提问人:huseyin tugrul buyukisik 提问时间:5/7/2022 最后编辑:huseyin tugrul buyukisik 更新时间:5/7/2022 访问量:305

问:

当我运行 fma 优化的 horner 方案多项式计算(用于余弦近似)时,尽管缺少 -ffast-math (GCC),但它在 FX8150 上产生 0.161 ulps 误差,但在 godbolt.org 服务器上产生 0.154 ulps 误差。

如果这是由硬件引起的,并且每个硬件的精度不同,那么 C++ 编译器如何保持不同计算机之间的浮点精度?

编程语言规范是否只有最低精度要求,以便任何 CPU 供应商都可以根据需要提高精度?

最小可重复样品:

#include<iostream>
        // only optimized for [-1,1] input range
        template<typename Type, int Simd>
        inline
        void cosFast(Type * const __restrict__ data, Type * const __restrict__ result) noexcept
        {
            alignas(64)
            Type xSqr[Simd];
            
            for(int i=0;i<Simd;i++)
            {
                xSqr[i] =   data[i]*data[i];
            }   
            for(int i=0;i<Simd;i++)
            {
                result[i] =     Type(2.425144155360214881511638e-05);
            }
            for(int i=0;i<Simd;i++)
            {
                result[i] =     result[i]*xSqr[i] + Type(-0.001388599083010255696990498);
            }
            for(int i=0;i<Simd;i++)
            {
                result[i] =     result[i]*xSqr[i] + Type(0.04166657759826541962411284);
            }       
            for(int i=0;i<Simd;i++)
            {
                result[i] =     result[i]*xSqr[i] + Type(-0.4999999436679569697616898);
            }       
            for(int i=0;i<Simd;i++)
            {
                result[i] =     result[i]*xSqr[i] + Type(0.9999999821855363180134191);
            }


        }


#include<cstring>
template<typename T>
uint32_t GetUlpDifference(T a, T b)
{
    uint32_t aBitValue;
    uint32_t bBitValue;
    std::memcpy(&aBitValue,&a,sizeof(T));
    std::memcpy(&bBitValue,&b,sizeof(T));
    return (aBitValue > bBitValue) ?
           (aBitValue - bBitValue) :
           (bBitValue - aBitValue);
}
#include<vector>
template<typename Type>
float computeULP(std::vector<Type> real, std::vector<Type> approximation)
{
    int ctr = 0;
    Type diffSum = 0;
    for(auto r:real)
    {
        Type diff = GetUlpDifference(r,approximation[ctr++]);
        diffSum += diff;
    }
    return diffSum/ctr;
}

template<typename Type>
float computeMaxULP(std::vector<Type> real, std::vector<Type> approximation)
{
    int ctr = 0;
    Type mx = 0;
    int index = -1;
    Type rr = 0;
    Type aa = 0;
    for(auto r:real)
    {
        Type diff = GetUlpDifference(r,approximation[ctr++]);
        if(mx<diff)
        {
            mx = diff;
            rr=r;
            aa=approximation[ctr-1];
            index = ctr-1;
        }
    }
    std::cout<<"("<<index<<":"<<rr<<"<-->"<<aa<<")";
    return mx;
}
#include<cmath>
void test()
{
    constexpr int n = 8192*64;
    std::vector<float> a(n),b(n),c(n);
    for(int i=0;i<n;i++)
        a[i]=(i-(n/2))/(float)(n/2);

    // approximation
    for(int i=0;i<n;i+=16)
        cosFast<float,16>(a.data()+i,b.data()+i);

    // exact
    for(int i=0;i<n;i++)
        c[i] = std::cos(a[i]);
    
    std::cout<<"avg. ulps: "<<computeULP(b,c)<<std::endl;
    std::cout<<"max. ulps: "<<computeMaxULP(b,c)<<std::endl;
}

int main()
{
    test();
    return 0;
}

证明它使用了 FMA:

https://godbolt.org/z/Y4qYMoxcn

.L23:
    vmovups ymm3, YMMWORD PTR [r12+rax]
    vmovups ymm2, YMMWORD PTR [r12+32+rax]
    vmulps  ymm3, ymm3, ymm3
    vmulps  ymm2, ymm2, ymm2
    vmovaps ymm1, ymm3
    vmovaps ymm0, ymm2
    vfmadd132ps     ymm1, ymm7, ymm8
    vfmadd132ps     ymm0, ymm7, ymm8
    vfmadd132ps     ymm1, ymm6, ymm3
    vfmadd132ps     ymm0, ymm6, ymm2
    vfmadd132ps     ymm1, ymm5, ymm3
    vfmadd132ps     ymm0, ymm5, ymm2
    vfmadd132ps     ymm1, ymm4, ymm3
    vfmadd132ps     ymm0, ymm4, ymm2
    vmovups YMMWORD PTR [r13+0+rax], ymm1
    vmovups YMMWORD PTR [r13+32+rax], ymm0
    add     rax, 64
    cmp     rax, 2097152
    jne     .L23

这个实例(我不知道是至强还是霄龙)进一步将其提高到平均 0.152 ulps。

C++ GCC 浮点 精度

评论

1赞 Maxpm 5/7/2022
您的问题是否由 stackoverflow.com/questions/34294938/ 回答...
0赞 huseyin tugrul buyukisik 5/7/2022
However, implementing this annex is optional; the core standard specifically avoids saying anything about the representation of floating point numbers因此,他们对硬件供应商没有任何要求。
1赞 njuffa 5/7/2022
代码,否则它没有发生:-)在 IEEE-754 上运行的 FMA 和不同处理器上的数据极有可能为非特殊操作数提供完全相同的结果。但是,很有可能 (1) 软件以不同的方式配置了 FPU(例如,齐平到零或舍入模式),(2) 编译器以不同的顺序应用了 FMA 操作,或者根本没有应用(例如,由于编译器开关、优化设置)。在调查硬件之前,您需要在机器代码级别仔细检查这些软件因素。binary32binary64
0赞 huseyin tugrul buyukisik 5/7/2022
godbolt.org/z/59978TzEf,这是完整的代码: godbolt.org/z/9Tcjq4br5 我在精确地将范围缩小到 [-pi,pi] 时遇到了问题,所以这个版本仅适用于 [-1,1] 范围,这很容易产生更少的错误(甚至可能是 0 ulps),但有趣的是看到不同的 CPU 使用相同的代码提供不同的精度。(这些是平均 ULP,而不是最大值 1)
1赞 Eric Postpischil 5/7/2022
@huseyintugrulbuyukisik:你是说FCMLA指令吗?这只是两个真正的FMA。如上所述,每个都会产生其结果。

答:

1赞 Jérôme Richard 5/7/2022 #1

关于C++语言,没有很强的要求,它主要是实现定义的,如@Maxpm在评论中指出的先前答案所述。

浮点精度的主要标准是 IEEE-754。它通常被现在的大多数厂商正确实现(至少几乎所有最近的主流 x86-64 CPU 和大多数主流 GPU)。C++ 标准不需要它,但您可以使用 检查这一点。std::numeric_limits<T>::is_iec559

IEEE-754 标准要求使用正确的舍入方法正确计算运算(即误差小于 1 ULP)。规范支持不同的舍入方法,但最常见的是舍入到最接近的舍入。该标准还要求在相同的要求下实现一些操作,例如 FMA。因此,您不能指望使用此标准每次操作的计算结果精度优于 1 ULP(四舍五入可能有助于平均达到 0.5 ULP,甚至对于使用的实际算法更好)。

在实践中,符合 IEEE-754 标准的硬件供应商的计算单元在内部使用更高的精度,以便无论提供何种输入来满足要求。尽管如此,当结果存储在内存中时,它们需要像 IEEE-754 那样正确舍入。在 x86-64 处理器上,SIMD 寄存器(如 SSE、AVX 和 AVX-512 寄存器)具有众所周知的固定大小。每个通道都是 16 位(半浮点)、32 位(浮点)或 64 位(双通道),用于浮点运算。每个指令都应应用符合 IEEE-754 标准的舍入。虽然处理器理论上可以实现巧妙的优化,例如将两个 FP 指令融合为一个(只要精度为 <1 ULP),但 AFAIK 还没有做到这一点(尽管对某些指令(如条件分支)进行了融合)。

IEEE-754 平台之间的差异可能是由于硬件供应商的编译器或 FP 单元的配置造成的。

关于编译器,优化可以提高精度,同时符合 IEEE-754 标准。例如,在代码中使用 FMA 指令是一种优化,可以提高结果的精度,但在 x86-64 平台上,编译器并不强制这样做(事实上,并非所有 x86-64 处理器都支持它)。出于某些原因,编译器可能会使用单独的乘法+加法指令(Clang 有时会这样做)。编译器可以使用比目标处理器更好的精度来预计算某些常量(例如,GCC 以更高的精度对 FP 数字进行操作以生成编译时常量)。此外,可以使用不同的舍入方法来计算常量。

关于硬件供应商,因为默认的舍入模式可以从一个平台更改为另一个平台。就您而言,非常小的差异可能是由于这个原因。在一个平台上,舍入模式可能是“舍入到最近,平局到偶数”,在另一个平台上可能是“四舍五入到最近,平局远离零”,从而产生非常小但可见的差异。您可以使用此答案中提供的 C 代码设置舍入模式。另请注意,由于非常高的开销,非正态数字有时会在某些平台上被禁用(有关详细信息,请参阅此处),尽管它使结果不符合 IEEE-754 标准。您应该检查是否是这种情况。

简而言之,<1 ULP 的差异在两个符合 IEEE-754 标准的平台之间是完全正常的,实际上在非常不同的平台之间非常频繁(例如。在 POWER 上与 gcc 在 x86-64 上)。

评论

1赞 huseyin tugrul buyukisik 5/7/2022
在两个 cpu 上使用固定 ulp 为 0.3。 在 FX8150 上给出 0.161,在 godbolt.org 上给出 0.154。更精确的硬件四舍五入到真正接近的一侧,或者另一舍入到错误的一侧。由于两者的零是同一边,因此它的工作原理相同,但 ulps 几乎翻了一番,我猜这是由于四舍五入 1.99 到 1 吗?FE_TOWARDZEROFE_TONEAREST