【问题标题】:Modulo operator slower than manual implementation?模运算符比手动执行慢?
【发布时间】:2021-04-06 22:20:55
【问题描述】:

我发现在__int128 上手动计算% 运算符比内置编译器运算符要快得多。我将向您展示如何计算模 9,但该方法可用于计算任何其他数的模。

首先,考虑内置的编译器操作符:

uint64_t mod9_v1(unsigned __int128 n)
{
    return n % 9;
}

现在考虑我的手动实现:

uint64_t mod9_v2(unsigned __int128 n)
{
    uint64_t r = 0;

    r += (uint32_t)(n);
    r += (uint32_t)(n >> 32) * (uint64_t)4;
    r += (uint32_t)(n >> 64) * (uint64_t)7;
    r += (uint32_t)(n >> 96);

    return r % 9;
}

测量超过 100,000,000 个随机数会得出以下结果:

mod9_v1 | 3.986052 secs
mod9_v2 | 1.814339 secs

在 AMD Ryzen Threadripper 2990WX 上使用了带有 -march=native -O3 的 GCC 9.3.0。 Here 是godbolt的链接。

我想问一下你的行为是否相同? (在向 GCC Bugzilla 报告错误之前)。

更新: 根据要求,我提供了一个生成的程序集:

mod9_v1:
        sub     rsp, 8
        mov     edx, 9
        xor     ecx, ecx
        call    __umodti3
        add     rsp, 8
        ret
mod9_v2:
        mov     rax, rdi
        shrd    rax, rsi, 32
        mov     rdx, rsi
        mov     r8d, eax
        shr     rdx, 32
        mov     eax, edi
        add     rax, rdx
        lea     rax, [rax+r8*4]
        mov     esi, esi
        lea     rcx, [rax+rsi*8]
        sub     rcx, rsi
        mov     rax, rcx
        movabs  rdx, -2049638230412172401
        mul     rdx
        mov     rax, rdx
        shr     rax, 3
        and     rdx, -8
        add     rdx, rax
        mov     rax, rcx
        sub     rax, rdx
        ret

【问题讨论】:

  • @stark 我在uint64_t 上做%,而不是unsigned __int128
  • 我想有趣的部分在__umodti3 函数中。但无论如何,您的实现是专门为% 9 编写的,而__umodti3 是通用的% n
  • __umodti3 是一个通用的除法函数,所以它不能像% 9 的优化版本一样快。至于为什么 GCC 或 Clang 都没有自动应用优化这一点,我们只能推测 - 很可能它只是不需要那么频繁并且不值得开发工作。值得注意的是,uint64_t % 9 确实针对乘法和移位进行了优化。
  • 解释很简单:编译器作者没有优化__int128 模。通常整数除法可以优化为乘法,乘法可以(通常)优化为移位和加法。尝试__int128 部门向自己证明它没有优化。然后与__int64 除法比较,你会发现区别。
  • @Jabberwocky:mov esi,esirsi 的最高 32 位设置为零(就像 movzx rsi,esi 一样)。

标签: c x86-64 compiler-optimization modular-arithmetic 128-bit


【解决方案1】:

同时(在等待 Bugzilla 时),您可以让预处理器为您进行优化。例如。定义一个名为 MOD_INT128(n,d) 的宏:

#define MODCALC0(n,d)   ((65536*n)%d)
#define MODCALC1(n,d)   MODCALC0(MODCALC0(n,d),d)
#define MODCALC2(n,d)   MODCALC1(MODCALC1(n,d),d)
#define MODCALC3(n,d)   MODCALC2(MODCALC1(n,d),d)
#define MODPARAM(n,d,a,b,c) \
    ((uint64_t)((uint32_t)(n) ) + \
    (uint64_t)((uint32_t)(n >> 32) * (uint64_t)a) + \
    (uint64_t)((uint32_t)(n >> 64) * (uint64_t)b) + \
    (uint64_t)((uint32_t)(n >> 96) * (uint64_t)c) ) % d
#define MOD_INT128(n,d) MODPARAM(n,d,MODCALC1(1,d),MODCALC2(1,d),MODCALC3(1,d))

现在,

uint64_t mod9_v3(unsigned __int128 n)
{
    return MOD_INT128( n, 9 );
}

将生成与 mod9_v2() 函数类似的汇编语言,并且

uint64_t mod8_v3(unsigned __int128 n)
{
    return MOD_INT128( n, 8 );
}

适用于现有优化 (GCC 10.2.0)

【讨论】:

    【解决方案2】:

    从汇编列表中可以清楚地看出这种差异的原因:应用于 128 位整数的 % 运算符是通过对无法利用除数值的编译时知识的通用函数的库调用来实现的,该函数可以将除法和模运算转换为更快的乘法。

    在我使用 clang 的旧 Macbook-pro 上,时间差异更加显着,我 mod_v2()mod_v1()x15 倍。

    但请注意以下备注:

    • 您应该在 for 循环结束后测量 CPU 时间,而不是在当前编码的第一个 printf 之后。
    • 假设RAND_MAX0x7fffffffrand_u128() 只会产生 124 位。
    • 大部分时间都花在计算随机数上。

    使用您的切片方法,我扩展了您的代码以减少使用 42、42 和 44 位切片的步骤数,这进一步改进了时序(因为 242 % 9 == 1) :

    #pragma GCC diagnostic ignored "-Wpedantic"
    #include <stddef.h>
    #include <stdint.h>
    #include <stdlib.h>
    #include <assert.h>
    #include <inttypes.h>
    #include <stdio.h>
    #include <time.h>
    
    static uint64_t mod9_v1(unsigned __int128 n) {
        return n % 9;
    }
    
    static uint64_t mod9_v2(unsigned __int128 n) {
        uint64_t r = 0;
    
        r += (uint32_t)(n);
        r += (uint32_t)(n >> 32) * (uint64_t)(((uint64_t)1ULL << 32) % 9);
        r += (uint32_t)(n >> 64) * (uint64_t)(((unsigned __int128)1 << 64) % 9);
        r += (uint32_t)(n >> 96);
    
        return r % 9;
    }
    
    static uint64_t mod9_v3(unsigned __int128 n) {
        return (((uint64_t)(n >>  0) & 0x3ffffffffff) +
                ((uint64_t)(n >> 42) & 0x3ffffffffff) +
                ((uint64_t)(n >> 84))) % 9;
    }
    
    unsigned __int128 rand_u128() {
        return ((unsigned __int128)rand() << 97 ^
                (unsigned __int128)rand() << 66 ^
                (unsigned __int128)rand() << 35 ^
                (unsigned __int128)rand() << 4 ^
                (unsigned __int128)rand());
    }
    
    #define N 100000000
    
    int main() {
        srand(42);
    
        unsigned __int128 *arr = malloc(sizeof(unsigned __int128) * N);
        if (arr == NULL) {
            return 1;
        }
    
        for (size_t n = 0; n < N; ++n) {
            arr[n] = rand_u128();
        }
    
    #if 1
        /* check that modulo 9 is calculated correctly */
        for (size_t n = 0; n < N; ++n) {
            uint64_t m = mod9_v1(arr[n]);
            assert(m == mod9_v2(arr[n]));
            assert(m == mod9_v3(arr[n]));
        }
    #endif
    
        clock_t clk1 = -clock();
        uint64_t sum1 = 0;
        for (size_t n = 0; n < N; ++n) {
            sum1 += mod9_v1(arr[n]);
        }
        clk1 += clock();
    
        clock_t clk2 = -clock();
        uint64_t sum2 = 0;
        for (size_t n = 0; n < N; ++n) {
            sum2 += mod9_v2(arr[n]);
        }
        clk2 += clock();
    
        clock_t clk3 = -clock();
        uint64_t sum3 = 0;
        for (size_t n = 0; n < N; ++n) {
            sum3 += mod9_v3(arr[n]);
        }
        clk3 += clock();
    
        printf("mod9_v1: sum=%"PRIu64", elapsed time: %.3f secs\n", sum1, clk1 / (double)CLOCKS_PER_SEC);
        printf("mod9_v2: sum=%"PRIu64", elapsed time: %.3f secs\n", sum2, clk2 / (double)CLOCKS_PER_SEC);
        printf("mod9_v3: sum=%"PRIu64", elapsed time: %.3f secs\n", sum3, clk3 / (double)CLOCKS_PER_SEC);
    
        free(arr);
        return 0;
    }
    

    这是我的 linux 服务器 (gcc) 上的时间安排:

    mod9_v1: sum=400041273, elapsed time: 7.992 secs
    mod9_v2: sum=400041273, elapsed time: 1.295 secs
    mod9_v3: sum=400041273, elapsed time: 1.131 secs
    

    我的 Macbook 上的相同代码(clang):

    mod9_v1: sum=399978071, elapsed time: 32.900 secs
    mod9_v2: sum=399978071, elapsed time: 0.204 secs
    mod9_v3: sum=399978071, elapsed time: 0.185 secs
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-05-31
      • 1970-01-01
      • 2016-06-19
      • 1970-01-01
      • 1970-01-01
      • 2022-03-20
      相关资源
      最近更新 更多