x86上的两个128位整数的高效乘法/除法(无64位)

时间:2012-01-08 07:32:17

标签: c++ algorithm x86 bignum

编译器: MinGW / GCC
问题:不允许使用GPL / LGPL代码(GMP或任何bignum库,因为我已经实现了这个问题,因为这个问题已经过时了。)

我构建了自己的 128位固定大小的大整数类(用于游戏引擎,但可以推广到任何使用情况)并且我发现当前乘法的性能并且将操作划分得相当糟糕(是的,我已将它们计时,见下文),并且我想改进(或更改)执行低级数字运算的算法。 < / p>


当涉及乘法和除法运算符时,与几乎所有其他类似的运算符相比,它们都是无法忍受的缓慢。

这些是相对于我自己的计算机的近似测量值:

Raw times as defined by QueryPerformanceFrequency:
1/60sec          31080833u
Addition:              ~8u
Subtraction:           ~8u
Multiplication:      ~546u
Division:           ~4760u (with maximum bit count)

正如您所看到的,只是进行乘法比加或减慢很多倍。除法比乘法慢10倍。

我想提高这两个运算符的速度,因为每帧可能会进行大量的计算(点积,各种碰撞检测方法等)。


结构(方法省略)看起来有点像:

class uint128_t
{
    public:
        unsigned long int dw3, dw2, dw1, dw0;
  //...
}

乘法目前使用典型的长乘法方法完成(在汇编中,以便我可以捕获EDX输出)而忽略了单词超出范围(也就是说,与16相比,我只做了10 mull

分区使用 shift-subtract 算法(速度取决于操作数的位数)。但是,它不是在装配中完成的。我发现有点太难以集合,并决定让编译器优化它。


我已经谷歌了几天,查看描述Karatsuba Multiplication,高基数除法和Newton-Rapson Division等算法的页面,但数学符号有点太过分了。我想使用其中一些高级方法来加速我的代码,但我必须首先将“希腊语”翻译成可以理解的东西。

对于那些可能认为我的努力“过早优化”的人;我认为这个代码是一个瓶颈,因为非常基本的数学运算本身变得很慢。我可以在更高级别的代码上忽略这种类型的优化,但是这个代码将被调用/使用到足够重要。

我想建议我应该使用哪种算法来提高乘法和除法(如果可能的话),以及关于建议的算法如何工作的基本(希望很容易理解)解释高< / em>赞赏。


编辑:乘以改进

我能够通过将代码内联到operator * =来改进乘法运算,并且它似乎尽可能快。

Updated raw times:
1/60sec          31080833u
Addition:              ~8u
Subtraction:           ~8u
Multiplication:      ~100u (lowest ~86u, highest around ~256u)
Division:           ~4760u (with maximum bit count)

这里有一些简单的代码供你检查(注意我的类型名称实际上是不同的,为简单起见,这是编辑的):

//File: "int128_t.h"
class int128_t
{
    uint32_t dw3, dw2, dw1, dw0;

    // Various constrctors, operators, etc...

    int128_t& operator*=(const int128_t&  rhs) __attribute__((always_inline))
    {
        int128_t Urhs(rhs);
        uint32_t lhs_xor_mask = (int32_t(dw3) >> 31);
        uint32_t rhs_xor_mask = (int32_t(Urhs.dw3) >> 31);
        uint32_t result_xor_mask = (lhs_xor_mask ^ rhs_xor_mask);
        dw0 ^= lhs_xor_mask;
        dw1 ^= lhs_xor_mask;
        dw2 ^= lhs_xor_mask;
        dw3 ^= lhs_xor_mask;
        Urhs.dw0 ^= rhs_xor_mask;
        Urhs.dw1 ^= rhs_xor_mask;
        Urhs.dw2 ^= rhs_xor_mask;
        Urhs.dw3 ^= rhs_xor_mask;
        *this += (lhs_xor_mask & 1);
        Urhs += (rhs_xor_mask & 1);

        struct mul128_t
        {
            int128_t dqw1, dqw0;
            mul128_t(const int128_t& dqw1, const int128_t& dqw0): dqw1(dqw1), dqw0(dqw0){}
        };

        mul128_t data(Urhs,*this);
        asm volatile(
        "push      %%ebp                            \n\
        movl       %%eax,   %%ebp                   \n\
        movl       $0x00,   %%ebx                   \n\
        movl       $0x00,   %%ecx                   \n\
        movl       $0x00,   %%esi                   \n\
        movl       $0x00,   %%edi                   \n\
        movl   28(%%ebp),   %%eax #Calc: (dw0*dw0)  \n\
        mull             12(%%ebp)                  \n\
        addl       %%eax,   %%ebx                   \n\
        adcl       %%edx,   %%ecx                   \n\
        adcl       $0x00,   %%esi                   \n\
        adcl       $0x00,   %%edi                   \n\
        movl   24(%%ebp),   %%eax #Calc: (dw1*dw0)  \n\
        mull             12(%%ebp)                  \n\
        addl       %%eax,   %%ecx                   \n\
        adcl       %%edx,   %%esi                   \n\
        adcl       $0x00,   %%edi                   \n\
        movl   20(%%ebp),   %%eax #Calc: (dw2*dw0)  \n\
        mull             12(%%ebp)                  \n\
        addl       %%eax,   %%esi                   \n\
        adcl       %%edx,   %%edi                   \n\
        movl   16(%%ebp),   %%eax #Calc: (dw3*dw0)  \n\
        mull             12(%%ebp)                  \n\
        addl       %%eax,   %%edi                   \n\
        movl   28(%%ebp),   %%eax #Calc: (dw0*dw1)  \n\
        mull              8(%%ebp)                  \n\
        addl       %%eax,   %%ecx                   \n\
        adcl       %%edx,   %%esi                   \n\
        adcl       $0x00,   %%edi                   \n\
        movl   24(%%ebp),   %%eax #Calc: (dw1*dw1)  \n\
        mull              8(%%ebp)                  \n\
        addl       %%eax,   %%esi                   \n\
        adcl       %%edx,   %%edi                   \n\
        movl   20(%%ebp),   %%eax #Calc: (dw2*dw1)  \n\
        mull              8(%%ebp)                  \n\
        addl       %%eax,   %%edi                   \n\
        movl   28(%%ebp),   %%eax #Calc: (dw0*dw2)  \n\
        mull              4(%%ebp)                  \n\
        addl       %%eax,   %%esi                   \n\
        adcl       %%edx,   %%edi                   \n\
        movl   24(%%ebp),  %%eax #Calc: (dw1*dw2)   \n\
        mull              4(%%ebp)                  \n\
        addl       %%eax,   %%edi                   \n\
        movl   28(%%ebp),   %%eax #Calc: (dw0*dw3)  \n\
        mull               (%%ebp)                  \n\
        addl       %%eax,   %%edi                   \n\
        pop        %%ebp                            \n"
        :"=b"(this->dw0),"=c"(this->dw1),"=S"(this->dw2),"=D"(this->dw3)
        :"a"(&data):"%ebp");

        dw0 ^= result_xor_mask;
        dw1 ^= result_xor_mask;
        dw2 ^= result_xor_mask;
        dw3 ^= result_xor_mask;
        return (*this += (result_xor_mask & 1));
    }
};

至于除法,检查代码是没有意义的,因为我需要改变数学算法以看到任何实质性的好处。唯一可行的选择似乎是高基数除法,但我还没有解决(在我看来) 它将如何工作。

2 个答案:

答案 0 :(得分:2)

我不会担心乘法。你正在做什么似乎非常有效。我并没有真正遵循Karatsuba乘法中的希腊语,但我的感觉是,只有比你处理的数字更大的数字才能更有效率。

我的一个建议是尝试使用最小的内联汇编块,而不是在汇编中编写逻辑。你可以写一个函数:

struct div_result { u_int x[2]; };
static inline void mul_add(int a, int b, struct div_result *res);

该函数将在内联汇编中实现,您将从C ++代码中调用它。它应该与纯装配一样高效,并且更容易编码。

关于师,我不知道。我看到的大多数算法都谈到了渐近效率,这可能意味着它们只对非常高的位数有效。

答案 1 :(得分:1)

我是否正确了解您在1.8 GHz计算机上运行测试的数据,并且您的计时中的“u”是处理器周期?

如果是这样,10个32x32位MUL的546个周期对我来说似乎有点慢。我在2GHz Core2 Duo上拥有自己的品牌bignums,在大约150个周期内运行128x128 = 256位MUL(我做了所有16个小型MUL),即大约快6倍。但这可能只是一个更快的CPU。

确保您展开循环以节省开销。尽可能少注册保存。如果你在这里发布ASM代码也许会有所帮助,所以我们可以查看它。

Karatsuba不会帮助你,因为它开始只有20-40个32位字的效率。

分区总是比乘法贵得多。如果你多次使用常数或相同的值,可能有助于预先计算倒数,然后乘以它。