繁体   English   中英

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

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

当我运行 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

这个实例(我不知道它是 xeon 还是 epyc)进一步将它提高到 0.152 ulps 平均值。

关于 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 标准的硬件供应商的计算单元在内部使用更高的精度,以便无论提供什么输入都能满足要求。 尽管如此,当结果存储在 memory 中时,它们需要按照 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 标准。 您应该检查是否是这种情况。

简而言之,两个 IEEE-754 兼容平台之间的差异 <1 ULP 是完全正常的,并且实际上在非常不同的平台之间非常频繁(例如,POWER 上的 Clang 与 x86-64 上的 GCC)。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM