【问题标题】:Why does using mod with int64_t operand makes this function 150% slower?为什么使用带有 int64_t 操作数的 mod 会使这个函数慢 150%?
【发布时间】:2016-04-10 16:03:26
【问题描述】:

max_rem 函数计算(a+1)^n + (a-1)^n 除以 得到的n = 1, 2, 3... 的最大余数。 main 在从 3999 的每个 a 上调用 max_rem。完整代码:

#include <inttypes.h>
#include <stdio.h>

int max_rem(int a) {
    int max_r = 0;
    int m = a * a; // <-------- offending line
    int r1 = a+1, r2 = a-1;
    for(int n = 1; n <= a*a; n++) {
        r1 = (r1 * (a + 1)) % m;
        r2 = (r2 * (a - 1)) % m;
        int r = (r1 + r2) % m;
        if(max_r < r) 
            max_r = r;
    }
    return max_r;
}

int main() {
    int64_t sum = 0;
    for(int a = 3; a < 1000; a++)
        sum += max_rem(a);

    printf("%ld\n", sum);
}

如果我将第 6 行更改为:

int m = a * a;

int64_t m = a * a;

整个计算速度变慢了大约 150%。我尝试了gcc 5.3clang 3.6

int:

$ gcc -std=c99 -O3 -Wall -o 120 120.c
$ time(./120)

real    0m3.823s
user    0m3.816s
sys     0m0.000s

int64_t:

$ time(./120)

real    0m9.861s
user    0m9.836s
sys     0m0.000s

是的,我使用的是 64 位系统。为什么会这样?

我一直认为使用 int64_t 更安全、更便携,并且是“编写 C 的现代方式”®,并且不会损害 64 位系统上数字代码的性能。这个假设是错误的吗?

编辑:明确一点:即使您将 every 变量更改为 int64_t,减速仍然存在。所以这不是混合intint64_t的问题。

【问题讨论】:

  • 尝试将所有整数更改为 (u)int64_ts。
  • 您将int64_tint 混合在一起。在整个函数中使用相同的类型。
  • 如果他已经在使用 64 位,那又有什么关系呢?归根结底,不应该生成相同的程序集吗?我认为这是一个非常好的问题。
  • 即使您将所有内容都更改为 int64_t,减速也是一样的。我编辑澄清。
  • 你知道你可以做time ./120,对吧?你不需要分叉(subshell)

标签: c performance


【解决方案1】:

我一直认为使用 int64_t 更安全、更便携,并且是“编写 C 的现代方式”®,并且不会损害 64 位系统上的数字代码性能。这个假设是错误的吗?

在我看来是这样。您可以在Intel's Software Optimization Reference manual(附录 C,第 645 页的表 C-17 通用指令)中找到指令时序:

IDIV r64 吞吐量 每条指令 85-100 个周期 IDIV r32 吞吐量 每条指令 20-26 个周期

【讨论】:

  • 问题不一定是类型,当您混合类型并强制从一种类型转换为另一种类型时所需的类型转换会增加额外的处理要求。
  • @DavidC.Rankin 但在我看来,如果英特尔说 64 位 div/mod 比 32 位 div/mod 慢,那么无论你做多快,它都会保持这种状态转换。尽管确实避免转化可能能够将减速降低到 150% 以下。
  • 加法和减法速度很快,但乘法和除法速度不快。除法更糟糕,因为你不能像其他 3 一样并行计算
  • 这个假设适用于除除以外的一切。顺便说一句,compiler output 确认即使是内联到main 的版本也使用idiv rcx,而max_rem 对其本地人使用int64_t。每次迭代的三个idivs 的缓慢速度将完全隐藏任何转换开销。 movsx 非常便宜。循环携带的依赖链只是结果的添加,所以我们只是吞吐量限制,而不是延迟。
  • 英特尔指南中的这些数字类似于 Agner Fog's insn tables with numbers from experimental testing show:英特尔 Haswell:idiv r32每 8-11c 一个吞吐量(22-29c 延迟,9 微秒) . idiv r64每 24-81c 一个吞吐量(39-103c 延迟,59 微指令)。与大多数指令不同,除法具有与数据相关的性能,并且仅部分流水线化(div 单元每个时钟不能接受一个输入)。 Skylake:32b:每 6c 一个,64b:每 24-90c 一个。
【解决方案2】:

TL;DR:随着类型的变化,您会看到不同的性能,因为您测量的是不同的计算 - 一个是所有 32 位数据,另一个是部分或全部 64 位数据。

我一直认为使用 int64_t 更安全、更便携,并且是“编写 C 的现代方式”®

int64_t 是最安全和最便携(在符合 C99 和 C11 编译器中)的方式来引用没有填充位和二进制补码表示的 64 位有符号整数类型,如果实现实际上提供了这种类型.使用这种类型是否真的让你的代码更容易移植取决于代码是否依赖于整数表示的任何特定特征,以及你是否关心到不提供这种类型的环境的可移植性。

并且不会损害数字代码在 64 位系统上的性能。这个假设是错误的吗?

int64_t 被指定为typedef。在任何给定的系统上,使用 int64_t 在语义上与直接使用该系统上 typedef 基础的类型相同。您不会看到这些替代方案之间的性能差异。

但是,您的推理和问题似乎掩盖了一个假设:在您执行测试的系统上,int64_t 的基本类型是int,或者 64 位算术将执行与该系统上的 32 位算术。这些假设都没有道理。绝不保证 64 位系统的 C 实现将使 int 成为 64 位类型,特别是 GCC 和 x86_64 的 Clang 都不会这样做。此外,C 对不同类型的算术的相对性能无话可说,正如其他人指出的那样,本机 x86_64 整数除法指令实际上对于 64 位操作数比对 32 位操作数要慢。其他平台可能会表现出其他差异。

【讨论】:

  • RE:最后一段:我认为 OP 的印象是 int64_tint32_t 的算术速度相同,除了除法之外,这是真的。 (或在前silvermont Atom 上相乘)。当然,int 在任何一个测试中都不是 64 位的; AMD64 SysV ABI specifies that it's 32b,并且两个编译器都针对相同的 ABI。
  • @PeterCordes,很公平。我已经更新了我的答案以适应对 OP 问题的解释。底线仍然是,一般来说,从不同的计算中期望相同的行为是不合理的。
【解决方案3】:

与任何其他运算相比,整数除法/取模非常慢。 (并且取决于数据大小,与现代硬件上的大多数操作不同,请参阅此答案的末尾)

对于重复使用相同的模数,您将通过找到整数除数的乘法逆元获得更多更好的性能。编译器会为编译时常量执行此操作,但在运行时执行此操作在时间和代码大小上相当昂贵,因此对于当前的编译器,您必须自己决定何时值得这样做。

这需要一些 CPU 周期,但它们在每次迭代中分摊到 3 个部门。

这个想法的参考论文是Granlund and Montgomery's 1994 paper,当时除法的成本仅为 P5 Pentium 硬件上的乘法的 4 倍。那篇论文谈到了在 gcc 2.6 中实现这个想法,以及它有效的数学证明。

Compiler output 显示了除以一个小常数变成的那种代码:

## clang 3.8 -O3 -mtune=haswell  for x86-64 SysV ABI: first arg in rdi
int mod13 (int a) { return a%13; }
    movsxd  rax, edi               # sign-extend 32bit a into 64bit rax
    imul    rcx, rax, 1321528399   # gcc uses one-operand 32bit imul (32x32 => 64b), which is faster on Atom but slower on almost everything else.  I'm showing clang's output because it's simpler
    mov     rdx, rcx
    shr     rdx, 63                # 0 or 1: extract the sign bit with a logical right shift
    sar     rcx, 34                # only use the high half of the 32x32 => 64b multiply
    add     ecx, edx               # ecx = a/13.   # adding the sign bit accounts for the rounding semantics of C integer division with negative numbers
    imul    ecx, ecx, 13           # do the remainder as  a - (a/13)*13
    sub     eax, ecx
    ret

是的,就吞吐量和延迟而言,所有这些都比 div 指令便宜。

我试图用谷歌搜索更简单的描述或计算器,并找到了 like this page 的东西。


在现代 Intel CPU 上,32 和 64b 乘法具有每个周期的吞吐量和 3 个周期的延迟。 (即它是完全流水线的)。

除法只是部分流水线化(div 单元每个时钟不能接受一个输入),并且与大多数指令不同,具有数据相关的性能:

来自Agner Fog's insn tables(另见 标签wiki):

  • Intel Core2:idiv r32:每 12-36c 吞吐量一个(18-42c 延迟,4 uops)。
    idiv r64:每 28-40c 吞吐量一个(39-72c 延迟,56 uops)。 (未签名的div 明显更快:32 uop,每 18-37c 吞吐量一个)
  • Intel Haswell:div/idiv r32:每 8-11c 吞吐量一个(22-29c 延迟,9 uops)。
    idiv r64:每 24-81c 吞吐量一个(39-103c 延迟,59 uops)。 (无符号div:每 21-74c 吞吐量一个,36 微指令)
  • Skylake:div/idiv r32:每 6c 吞吐量一个(26c 延迟,10 微秒)。
    64b:每 24-90c 吞吐量一个(42-95c 延迟,57 微指令)。 (无符号div:每 21-83c 吞吐量一个,36 微指令)

所以在 Intel 硬件上,无符号除法对于 64 位操作数来说更便宜,对于 32b 操作数也是如此。

32b 和 64b idiv 之间的吞吐量差异很容易占性能的 150%。 Your code 完全受吞吐量限制,因为您有大量独立操作,尤其是在循环迭代之间。循环携带的依赖只是最大操作的cmov

【讨论】:

    【解决方案4】:

    这个问题的答案只能来自查看程序集。出于好奇,我会在我的盒子上运行它,但它在 3000 英里之外:(所以我不得不猜测,你在这里查看并发布你的发现...... 只需将 -S 添加到编译器命令行即可。

    我相信使用 int64 编译器所做的事情与使用 int32 不同。也就是说,他们无法使用 int32 提供的某些优化。

    也许 gcc 只用 int32 用乘法代替除法?应该有一个'if(x

    我不相信如果他们都做简单的'idiv',性能会如此不同

    【讨论】:

    • gcc 将除法替换为乘法逆运算(完全乘以魔法常数 + 移位和加法)仅用于除以常数。在运行时计算乘法逆(如果存在)将是性能上的胜利,因为 OP 的循环重复使用相同的模数,但 gcc 不会为您执行此操作。它会造成代码大小的巨大增加,即使 gcc 可以做到这一点,也可能并不总是合理或理想的。 (乘法逆只给你商,但你可以再次乘法并减去得到余数。)
    猜你喜欢
    • 2017-10-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-15
    • 1970-01-01
    • 2011-06-02
    • 1970-01-01
    • 2017-07-25
    相关资源
    最近更新 更多