最近的question次会议,是否允许编译器用浮点乘法代替浮点除法,启发了我提出这个问题.

在严格要求代码转换后的结果必须与实际除法运算逐位相同的情况下, 很容易看出,对于二进制IEEE-754算术,这对于2的幂的除数是可能的.只要是互惠的 除数的值是可表示的,乘以除数的倒数可以得到与除法相同的结果.例如,乘以0.5可以替换除以2.0.

假设我们允许任何替换除法的短指令序列,但运行速度要快得多,同时提供位相同的结果,那么人们会想知道这样的替换会对其他除数起什么作用.具体地说,除了普通乘法之外,还允许融合乘法-加法运算. 我在 comments 中指出了以下有关文件:

Nicolas Brisebarre, Jean-Michel Muller, and Saurabh Kumar Raina. Accelerating correctly rounded floating-point division when the divisor is known in advance. IEEE Transactions on Computers, Vol. 53, No. 8, August 2004, pp. 1069-1072.

论文作者倡导的技术将除数y的倒数预计算为归一化头尾对zh:zl,如下所示:zh = 1 / y, zl = fma (-y, zh, 1) / y.稍后,除法q = x / y然后被计算为q = fma (zh, x, zl * x).文中推导了除数y为使该算法工作所必须满足的各种条件.正如人们很容易观察到的那样,当头部和尾部的符号不同时,该算法存在无穷大和零的问题.更重要的是,它将不能为数量非常小的红利x提供正确的结果,因为商尾zl * x的计算遭受下溢.

本文还顺便提到了另一种基于FMA的除法算法,该算法由彼得·马克斯坦(Peter Markstein)在IBM时首创.相关参考资料如下:

P. W. Markstein. Computation of elementary functions on the IBM RISC System/6000 processor. IBM Journal of Research & Development, Vol. 34, No. 1, January 1990, pp. 111-119

在马克斯坦的算法中,人们首先计算倒数rc,由此形成初始商q = x * rc.然后,使用FMA AS r = fma (-y, q, x)精确计算除法的剩余部分,并最终将改进的、更精确的商计算为q = fma (r, rc, q).

该算法也存在x是零或无穷大的问题(通过适当的条件执行很容易解决),但是使用IEEE-754单精度float数据的详尽测试表明,它在许多小整数中的许多除数y的所有可能的被除数x上提供了正确的商.此C代码实现它:

/* precompute reciprocal */
rc = 1.0f / y;

/* compute quotient q=x/y */
q = x * rc;
if ((x != 0) && (!isinf(x))) {
    r = fmaf (-y, q, x);
    q = fmaf (r, rc, q);
}

在大多数处理器体系 struct 上,这应该转换为使用谓词、条件移动或 Select 类型指令的无分支指令序列.举一个具体的例子:对于除以3.0f,CUDA 7.5的nvcc编译器为开普勒类GPU生成以下机器码:

    LDG.E R5, [R2];                        // load x
    FSETP.NEU.AND P0, PT, |R5|, +INF , PT; // pred0 = fabsf(x) != INF
    FMUL32I R2, R5, 0.3333333432674408;    // q = x * (1.0f/3.0f)
    FSETP.NEU.AND P0, PT, R5, RZ, P0;      // pred0 = (x != 0.0f) && (fabsf(x) != INF)
    FMA R5, R2, -3, R5;                    // r = fmaf (q, -3.0f, x);
    MOV R4, R2                             // q
@P0 FFMA R4, R5, c[0x2][0x0], R2;          // if (pred0) q = fmaf (r, (1.0f/3.0f), q)
    ST.E [R6], R4;                         // store q

在我的实验中,我编写了如下所示的微型C测试程序,该程序以递增的顺序遍历整数除数,并针对每个整数除数详尽地测试上面的代码序列与正确除法的关系.它打印通过此详尽测试的除数列表.部分输出如下所示:

PASS: 1, 2, 3, 4, 5, 7, 8, 9, 11, 13, 15, 16, 17, 19, 21, 23, 25, 27, 29, 31, 32, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 64, 65, 67, 69,

要将替换算法作为优化合并到编译器中,可以安全地应用上述代码转换的因子白名单是不切实际的.到目前为止程序的输出(以大约每分钟一个结果的速率)表明,对于那些为奇数整数或为2的幂的除数y,快速码在所有可能的x编码上正确工作.坊间证据,当然不是证据.

What set of mathematical conditions can determine a-priori whether the transformation of division into the above code sequence is safe?个答案可以假设所有浮点运算都是在默认舍入模式"舍入到最近或偶数"下执行的.

#include <stdlib.h>
#include <stdio.h>
#include <math.h>

int main (void)
{
    float r, q, x, y, rc;
    volatile union {
        float f;
        unsigned int i;
    } arg, res, ref;
    int err;

    y = 1.0f;
    printf ("PASS: ");
    while (1) {
        /* precompute reciprocal */
        rc = 1.0f / y;

        arg.i = 0x80000000;
        err = 0;
        do {
            /* do the division, fast */
            x = arg.f;
            q = x * rc;
            if ((x != 0) && (!isinf(x))) {
                r = fmaf (-y, q, x);
                q = fmaf (r, rc, q);
            }
            res.f = q;
            /* compute the reference, slowly */
            ref.f = x / y;

            if (res.i != ref.i) {
                err = 1;
                break;
            }
            arg.i--;
        } while (arg.i != 0x80000000);

        if (!err) printf ("%g, ", y);
        y += 1.0f;
    }
    return EXIT_SUCCESS;
}

推荐答案

让我第三次重新开始.我们正在努力加速

    q = x / y

其中y是整数常量,qxy都是IEEE 754-2008 binary32个浮点值.下面,fmaf(a,b,c)表示使用二进制32值的融合乘加a * b + c.

朴素算法通过预先计算的倒数,

    C = 1.0f / y

所以在运行时,一个(快得多的)乘法就足够了:

    q = x * C

Brisebarre-Muller-Raina加速度使用两个预先计算的常数,

    zh = 1.0f / y
    zl = -fmaf(zh, y, -1.0f) / y

因此,在运行时,一个乘法和一个融合的乘法-加法就足够了:

    q = fmaf(x, zh, x * zl)

Markstein算法将朴素的方法与两个融合的乘加相结合,如果朴素的方法通过预计算在最不重要的位置产生1个单位内的结果,则会产生正确的结果

    C1 = 1.0f / y
    C2 = -y

这样除法就可以用以下公式近似

    t1 = x * C1
    t2 = fmaf(C1, t1, x)
    q  = fmaf(C2, t2, t1)

这种幼稚的方法适用于两个y的所有幂,但在其他方面则相当糟糕.例如,对于除数7、14、15、28和30,它会对所有可能的x中的一半以上产生错误的结果.

Brisebarre-Muller-Raina方法对于几乎所有的2y的非幂都同样失败,但产生错误结果的x要少得多(在所有可能的x中,不到0.5%,具体取决于y).

Brisebarre-Muller-Raina的文章表明,朴素方法的最大误差为±1.5 ULPs.

Markstein方法对幂为2 y和奇数整数y的情况给出了正确的结果.(对于Markstein方法,我没有找到一个失败的奇数整数除数.)


对于马克斯坦方法,我已经分析了因子1-19700(raw data here).

绘制失效 case 的数量(横轴上的除数,Markstein方法对所述除数失效时的x个值的数量),我们可以看到一个简单的模式:

Markstein failure cases
(source: nominal-animal.net)

请注意,这些图的水平轴和垂直轴都是对数轴.奇数除数没有点,因为这种方法可以为我测试过的所有奇数除数产生正确的结果.

If we change the x axis to the bit reverse (binary digits in reverse order, i.e. 0b11101101 → 0b10110111, data) of the divisors, we have a very clear pattern: Markstein failure cases, bit reverse divisor
(source: nominal-animal.net)

如果我们画一条穿过点集中心的直线,我们就会得到曲线4194304/x.(请记住,绘图只考虑可能的浮点数的一半,因此在考虑所有可能的浮点数时,请将其加倍.) 8388608/x2097152/x完全包围了整个错误模式.

因此,如果我们使用rev(y)来计算除数y的位反转,那么8388608/rev(y)是一个很好的一阶近似值,它表示的情况(在所有可能的浮点数中)的数量,其中Markstein方法对两个除数y的偶数、非幂产生了错误的结果.(或者,上限为16777216/rev(x).)

增加了2016-02-28:在给定任意整数(二进制32)除数的情况下,我使用Markstein方法找到了错误 case 数量的近似值.下面是伪代码:

function markstein_failure_estimate(divisor):
    if (divisor is zero)
        return no estimate
    if (divisor is not an integer)
        return no estimate

    if (divisor is negative)
        negate divisor

    # Consider, for avoiding underflow cases,
    if (divisor is very large, say 1e+30 or larger)
        return no estimate - do as division

    while (divisor > 16777216)
        divisor = divisor / 2

    if (divisor is a power of two)
        return 0

    if (divisor is odd)
        return 0

    while (divisor is not odd)
        divisor = divisor / 2

    # Use return (1 + 83833608 / divisor) / 2
    # if only nonnegative finite float divisors are counted!
    return 1 + 8388608 / divisor

对于我测试过的Markstein故障 case (但我尚未充分测试大于8388608的除数),这将产生一个正确的误差估计,误差在±1范围内.最后一个除法应该是这样的,它不会报告错误的零,但我不能保证(现在).它没有考虑到非常大的因子(比如0x1p100,或者1e+30,以及更大的量级),这些因子有下溢问题——无论如何,我肯定会从加速中排除这些因子.

在初步测试中,这一估计似乎出奇地准确.我没有画出比较因子1到20000的估计误差和实际误差的曲线图,因为曲线图中的点都完全重合.(在此范围内,估计值是精确的,或者太大.)从本质上讲,这些估计准确地再现了这个答案中的第一个情节.


Markstein方法的失败模式是有规律的,而且非常有趣.该方法适用于两个除数的所有幂,以及所有奇数整数除数.

对于大于16777216的除数,我总是看到与除数相同的错误,除数被二的最小幂除,得到小于16777216的值.例如,0x1.3cdfa4p+23和0x1.3cdfa4p+41,0x1.d8874p+23和0x1.d8874p+32,0x1.cf84f8p+23和0x1.cf84f8p+34,0x1.e4a7fp+23和0x1.e4a7fp+37.(在每一对中,尾数是相同的,只有两个的幂不同.)

假设我的测试平台没有错误,这意味着马克斯坦方法也适用于大于16777216量级(但小于,比方说,1e+30)的除数,如果除数除以2的最小幂时,得出的量级商数小于16777216,并且商数是奇数.

C++相关问答推荐

当包含头文件时,gcc会发出隐式函数声明警告

是否有任何情况(特定类型/值),类型双关在所有符合标准的C实现中产生相同的行为?

在C++中头文件中声明外部 struct

在C23中使用_GENERIC实现带有右值的IS_POINTER(P)?

自定义变参数函数的C预处置宏和警告 suppress ?

Flose()在Docker容器中抛出段错误

如何确保在C程序中将包含uft8字符的字符串正确写入MySQL?

如何使用_newindex数组我总是得到错误的参数

在for循环中指向数组开头之前

在C++中父进程和子进程中的TAILQ队列同步问题

在编写代码时,Clion比vscode有更多的问题指示器

通过描述符查找文件路径时出现问题

不使用任何预定义的C函数进行逐位运算

正在try 理解C++中的`正在释放的指针未被分配‘错误

如何使用WRITE()以指针地址的十六进制形式写入标准输出

在C中打印指针本身

为什么 int32_t 和 int16_t 在 printf 输出中具有相同的位数?

System V 消息队列由于某种原因定期重置

为什么写入关闭管道会返回成功

如何确保 gcc + libc 对于多字节字符串使用 UTF-8,对于 wchar_t 使用 UTF-32?