哪一种最有效的多精度乘法算法?

时间:2019-03-25 11:26:46

标签: algorithm optimization c++-cli integer-arithmetic

我正在研究本机C ++ / CLI类,该类执行具有多精度值的整数运算。单个整数由64位无符号整数数组表示。该符号由布尔值表示,负值以其绝对值存储,而不是二进制补码。这使得处理标志问题变得更加容易。目前,我正在优化乘法运算。我已经进行了几次优化,但是我的函数仍然需要两倍于两个.NET BigInteger值的*运算符的时间,这表明进一步的优化仍有很大的潜力。

在寻求帮助之前,让我向您展示我已经尝试过的内容。我的第一次尝试是一种幼稚的方法:使用基本的64位到128位乘法将所有64位项对相乘,然后对结果进行移位/相加。我在这里没有显示代码,因为它非常慢。下一个尝试是递归分治算法,事实证明它要好得多。在我的实现中,两个操作数都在中间递归拆分,直到剩下两个64位值。将这些乘以一个128位结果。所收集的基本结果一直在递归层中向上移位/相加,以产生最终结果。该算法可能受益于以下事实:需要计算的64至128位基本积要少得多,这似乎是主要瓶颈。

这是我的代码。第一个片段显示了顶级入口点:

// ----------------------------------------------------------------------------
// Multi-precision multiplication, using a recursive divide-and-conquer plan:
// Left  split: (a*2^k + b)i = ai*2^k + bi
// Right split: a(i*2^k + j) = ai*2^k + aj

public: static UINT64* Mul (UINT64* pu8Factor1,
                            UINT64* pu8Factor2,
                            UINT64  u8Length1,
                            UINT64  u8Length2,
                            UINT64& u8Product)
    {
    UINT64* pu8Product;

    if ((u8Length1 > 0) && (u8Length2 > 0))
        {
        pu8Product = _SnlMemory::Unsigned ((u8Length1 * u8Length2) << 1);

        u8Product  = Mul (pu8Product, pu8Factor1, 0, u8Length1,
                                      pu8Factor2, 0, u8Length2);
        }
    else
        {
        pu8Product = _SnlMemory::Unsigned (0);
        u8Product  = 0;
        }
    return pu8Product;
    }

因子作为UINT64*数组指针传递,长度分别指定为相应数组中UINT64项的数量。该函数分配一个足以容纳最大预期长度的值的内存块,该内存块也用作临时下级结果的暂存器。该函数调用另一个Mul函数,该函数执行递归求值并返回最终结果实际使用的UINT64个项目的数量。

这是分而治之算法的递归“除法”部分:

// ----------------------------------------------------------------------------
// Recursively expand the arbitrary-precision multiplication to the sum of a
// series of elementary 64-to-128-bit multiplications.

private: static UINT64 Mul (UINT64* pu8Product,
                            UINT64* pu8Factor1,
                            UINT64  u8Offset1,
                            UINT64  u8Length1,
                            UINT64* pu8Factor2,
                            UINT64  u8Offset2,
                            UINT64  u8Length2)
    {
    UINT64 *pu8Lower, u8Lower, *pu8Upper, u8Upper, u8Split;
    UINT64 u8Product = 0;

    if (u8Length1 > 1)
        {
        // left split: (a*2^k + b)i = ai*2^k + bi
        u8Split = u8Length1 >> 1;

        u8Lower = Mul (pu8Lower = pu8Product,
                       pu8Factor1, u8Offset1, u8Split,  // bi
                       pu8Factor2, u8Offset2, u8Length2);

        u8Upper = Mul (pu8Upper = pu8Product + ((u8Split * u8Length2) << 1),
                       pu8Factor1, u8Offset1 + u8Split, // ai
                                   u8Length1 - u8Split,
                       pu8Factor2, u8Offset2, u8Length2);

        u8Product = Mul (u8Split, pu8Lower, u8Lower, pu8Upper, u8Upper);
        }
    else if (u8Length2 > 1)
        {
        // right split: a(i*2^k + j) = ai*2^k + aj
        u8Split = u8Length2 >> 1;

        u8Lower = Mul (pu8Lower = pu8Product,
                       pu8Factor1, u8Offset1, u8Length1, // aj
                       pu8Factor2, u8Offset2, u8Split);

        u8Upper = Mul (pu8Upper = pu8Product + ((u8Length1 * u8Split) << 1),
                       pu8Factor1, u8Offset1, u8Length1, // ai
                       pu8Factor2, u8Offset2 + u8Split,
                                   u8Length2 - u8Split);

        u8Product = Mul (u8Split, pu8Lower, u8Lower, pu8Upper, u8Upper);
        }
    else // recursion base: 64-to-128-bit multiplication
        {
        AsmMul1 (pu8Factor1 [u8Offset1],
                 pu8Factor2 [u8Offset2],
                 u8Lower, u8Upper);

        if (u8Upper > 0)
            {
            pu8Product [u8Product++] = u8Lower;
            pu8Product [u8Product++] = u8Upper;
            }
        else if (u8Lower > 0)
            {
            pu8Product [u8Product++] = u8Lower;
            }
        }
    return u8Product;
    }

在第一个条件分支中,左操作数被拆分。在第二个中,右操作数被拆分。第三分支是递归基础,它调用基本乘法例程:

; -----------------------------------------------------------------------------
; 64-bit to 128-bit multiplication, using the x64 MUL instruction

AsmMul1 proc ; ?AsmMul1@@$$FYAX_K0AEA_K1@Z

; ecx  : Factor1
; edx  : Factor2
; [r8] : ProductL
; [r9] : ProductH

mov  rax, rcx            ; rax = Factor1
mul  rdx                 ; rdx:rax = Factor1 * Factor2
mov  qword ptr [r8], rax ; [r8] = ProductL
mov  qword ptr [r9], rdx ; [r9] = ProductH
ret

AsmMul1 endp

这是一个简单的ASM PROC,它使用CPU MUL指令进行64到128位乘法。我在ASM和C ++中尝试了其他几种候选人,而这是最有效的候选人。

最后一部分是分而治之算法的“征服”部分:

// ----------------------------------------------------------------------------
// Shift-add recombination of the results of two partial multiplications.

private: static UINT64 Mul (UINT64  u8Split,
                            UINT64* pu8Lower,
                            UINT64  u8Lower,
                            UINT64* pu8Upper,
                            UINT64  u8Upper)
    {
    FLAG   fCarry;
    UINT64 u8Count, u8Lower1, u8Upper1;
    UINT64 u8Product = u8Lower;

    if (u8Upper > 0)
        {
        u8Count = u8Split + u8Upper;
        fCarry  = false;

        for (u8Product = u8Split; u8Product < u8Count; u8Product++)
            {
            u8Lower1 = u8Product < u8Lower ? pu8Lower [u8Product] : 0;
            u8Upper1 = pu8Upper [u8Product - u8Split];

            if (fCarry)
                {
                pu8Lower [u8Product] = u8Lower1 + u8Upper1 + 1;
                fCarry = u8Lower1 >= MAX_UINT64 - u8Upper1;
                }
            else
                {
                pu8Lower [u8Product] = u8Lower1 + u8Upper1;
                fCarry = u8Lower1 > MAX_UINT64 - u8Upper1;
                }
            }
        if (fCarry)
            {
            pu8Lower [u8Product++] = 1;
            }
        }
    return u8Product;
    }

这里添加了两个部分结果,第二个操作数向左移动了相应递归步骤的“分割因子”。

我花了很多时间来优化代码以提高速度,但取得了一些成功,但是现在我已经达到了一个点,除了使用完全不同的算法之外,我看不到其他任何可能性。但是,由于我不是数字技巧的专家,所以我被困在这里。

希望获得一些有关如何改进此计算的好主意...

编辑2019-03-26:好吧,有时候似乎最好不要尝试变得聪明...经过几次额外的优化尝试,其中有些甚至取得了一定的成功,我试图编写乘法的真实笨拙版本,该版本仅利用_umul128_addcarry_u64编译器内在函数的计算能力。代码非常简单:

public: static UINT64* Mul (UINT64* pu8Factor1,
                            UINT64* pu8Factor2,
                            UINT64  u8Length1,
                            UINT64  u8Length2,
                            UINT64& u8Product)
    {
    u8Product = u8Length1 + u8Length2;

    CHAR    c1Carry1, c1Carry2;
    UINT64  u8Offset, u8Offset1, u8Offset2, u8Item1, u8Item2, u8Lower, u8Upper;
    UINT64* pu8Product = _SnlMemory::Unsigned (u8Product);

    if (u8Product > 0)
        {
        for (u8Offset1 = 0; u8Offset1 < u8Length1; u8Offset1++)
            {
            u8Offset = u8Offset1;
            u8Item1  = pu8Factor1 [u8Offset1];
            u8Item2  = 0;
            c1Carry1 = 0;
            c1Carry2 = 0;

            for (u8Offset2 = 0; u8Offset2 < u8Length2; u8Offset2++)
                {
                u8Lower = _umul128 (u8Item1, pu8Factor2 [u8Offset2], &u8Upper);

                c1Carry1 = _addcarry_u64 (c1Carry1, u8Item2, u8Lower,
                                          &u8Item2);

                c1Carry2 = _addcarry_u64 (c1Carry2, u8Item2,
                                          pu8Product  [u8Offset],
                                          pu8Product + u8Offset);
                u8Item2 = u8Upper;
                u8Offset++;
                }
            if (c1Carry1 != 0)
                {
                c1Carry2 = _addcarry_u64 (c1Carry2, u8Item2 + 1,
                                          pu8Product  [u8Offset],
                                          pu8Product + u8Offset);
                }
            else if (u8Item2 != 0)
                {
                c1Carry2 = _addcarry_u64 (c1Carry2, u8Item2,
                                          pu8Product  [u8Offset],
                                          pu8Product + u8Offset);
                }
            }
        if (pu8Product [u8Product - 1] == 0)
            {
            u8Product--;
            }
        }
    return pu8Product;
    }

它在堆上创建一个结果缓冲区,该缓冲区足够大以容纳乘积的最大大小,并在两个嵌套循环中结合两个波纹流将基本64-zo-128位_umul128乘法-使用_addcarry_u64进行添加。到目前为止,此版本的性能是我尝试过的所有功能中最好的!它比等效的.NET BigInteger乘法快大约10倍,因此最终我实现了20倍的加速。

1 个答案:

答案 0 :(得分:1)

正如我们在reference source中看到的那样,.NET中的BigInteger使用相当慢的乘法算法,这是通常的使用32x32-> 64乘法的二次时间算法。但是,它的编写开销很低:迭代,分配很少,并且没有对不可植入的ASM过程的调用。部分产品会立即添加到结果中,而不是单独实现。

不可植入的ASM过程可以用_umul128内在函数代替。手动进位计算(包括条件+1和确定输出进位)都可以由_addcarry_u64内在函数代替。

诸如Karatsuba乘法和Toom-Cook乘法之类的更完善的算法可能是有效的,但是当递归一直进行到单肢级别时,效果却不明显-这远远超出了开销超过保存的基本乘法的程度。举一个具体的例子,Java的this implementation切换到Karatsuba以获得80条肢体(2560位,因为它们使用32位肢体),并切换到3路Toom-Cook以获得240条肢体。鉴于该阈值为80,只有64条肢体,我希望那里不会有太大的收获。