【问题标题】:Efficient Multiply/Divide of two 128-bit Integers on x86 (no 64-bit)x86(非 64 位)上两个 128 位整数的高效乘法/除法
【发布时间】:2012-01-08 07:32:16
【问题描述】:

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

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


当涉及到乘法和除法运算符时,与类中的其他所有操作相比,它们的速度慢得让人难以忍受。

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

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;
  //...
}

乘法目前是使用典型的 long-multiplication 方法完成的(在汇编中以便我可以捕获EDX 输出),同时忽略掉出来的单词范围(也就是说,我只做 10 个mull 与 16 个相比)。

Division 使用 shift-subtract 算法(速度取决于操作数的位数)。但是,它不是在组装中完成的。我发现这有点难以收集,并决定让编译器对其进行优化。


我在 Google 上搜索了几天,查看描述算法的页面,例如 Karatsuba Multiplication、高基数除法和 Newton-Rapson Division,但数学符号有点超出我的想象。我想使用其中一些高级方法来加速我的代码,但我必须先将“希腊语”翻译成可以理解的内容。

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

我想建议我应该使用哪种算法来改进乘法和除法(如果可能的话),并且对建议的算法如何工作的基本(希望很容易理解)解释将是高度 赞赏。


编辑:成倍改进

我能够通过将代码内联到 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));
    }
};

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

【问题讨论】:

  • 我在定点数学中使用它们,我需要大尺寸以便通过线-线交点等计算保持准确性。
  • 我实际上已经测试过浮点代码并且它运行得更快,但是我希望至少可以选择定点(据我估计,非常大的水平会导致双浮点失去显着的精度)。无论如何,我仍然想改进课程,因为我也在学习它是如何工作的,我想在课程准备好后免费提供。
  • 不可能,因为这是仅 32 位的代码,并且乘法/除法无法使用 SSE2 指令轻松优化,因为非常缺乏适用于整个 %xmm 寄存器的基于整数的指令。
  • (a) 阻止数学在引擎的最大限制下分崩离析。 (b) 它是固定点,因为它是我的引擎的技术要求(在其他地方涉及模拟对双打效率低下的行为)。 (c) 有一些库,但它们都过分了——实际上,由于函数调用开销,它们会比我的内联代码慢得多。 (d) 当然——它会被使用到足够重要的地方。越快越好。 (e) 是的。 (f) 我坚持当前的解决方案。只有除法比其相应的 FP 操作慢。我可以忍受。
  • @vonbrand 无论它是否适合 Simion32 的用例,对于希望进行 128 位运算的其他人来说,这仍然是一个有趣的问题。例如,他们可能希望编写一个函数来执行 64 位数字的高效 A*B+C 而不会溢出。

标签: c++ algorithm x86 bignum


【解决方案1】:

我不会太担心乘法。你正在做的似乎很有效。在 Karatsuba 乘法上,我并没有真正遵循希腊语,但我的感觉是,只有比你处理的数字大得多时,它才会更有效率。

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

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

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

关于除法,我不知道。我看到的大多数算法都在谈论渐近效率,这可能意味着它们仅对非常多的比特有效。

【讨论】:

  • 在乘法上,我想将程序集内联到 C++ operator*= 函数体中可以避免函数调用,但我认为除非有其他一些晦涩难懂的问题我没有读过的算法。
  • 我比 C++ 更了解 C,并且内联函数完全消除了函数调用开销。覆盖*= 不应该改变太多——它可能会使代码更好。我怀疑你是否会找到一种更适合 128 位数字的算法。
  • 取决于本机乘法的速度Karatsuba Multiplication 即使对于 128 位乘法也可能是一个重大胜利。
  • 我内联了 operator*= 的汇编代码,并获得了 2 倍以上的加速! (但是除法怎么办,嗯……)
【解决方案2】:

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

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

确保展开循环以节省开销。根据需要尽可能少地保存寄存器。如果您在此处发布 ASM 代码可能会有所帮助,以便我们对其进行审核。

Karatsuba 不会帮助您,因为它仅从大约 20-40 个 32 位字开始有效。

除法总是比乘法昂贵得多。如果您多次除以常数或相同的值,则预先计算倒数然后乘以它可能会有所帮助。

【讨论】:

  • “u”是由 QueryPerformanceFrequency() 定义的单位,它们是我 CPU 上的(大致)CPU 周期。但是请参阅我对 ugoren 的 awnser 的评论;通过内联代码,我能够将其加速 2 倍以上。现在它需要 ~86u,最大延迟为 ~256u(在这些之间波动很大)。内联 asm volatile 块通过指针传递参数,甚至推送/弹出 %ebp(%ecx:%edx:%edi:%esi = 结果累加器;%ebp = 数据指针;%eax:%edx = 乘以中间结果)。这是在没有额外指令开销​​的情况下运行的最快速度!对于除法,我可能必须处理高基数方法......
  • 我已按照您的建议编辑了代码,请随时查看... ;)
猜你喜欢
  • 2018-07-18
  • 2015-10-17
  • 2015-11-28
  • 1970-01-01
  • 2015-05-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多