【问题标题】:How to compute 2⁶⁴/n in C?如何在 C 中计算 2⁶⁴/n?
【发布时间】:2019-08-29 03:17:27
【问题描述】:

如何计算整数除法,264/n?假设:

  • unsigned long 是 64 位
  • 我们使用 64 位 CPU
  • 1 64

如果我们执行18446744073709551616ul / n,我们会在编译时得到warning: integer constant is too large for its type。这是因为我们无法在 64 位 CPU 中表达 264。另一种方法如下:

#define IS_POWER_OF_TWO(x) ((x & (x - 1)) == 0)

unsigned long q = 18446744073709551615ul / n;
if (IS_POWER_OF_TWO(n))
    return q + 1;
else
    return q;

有更快(CPU 周期)或更简洁(编码)的实现吗?

【问题讨论】:

  • 128 位当然可以。或者,您可以尝试使用双除法...
  • 如果你的编译器有__int128,那么干净的方法是__int128 q = ((__int128)18446744073709551615ull + 1)/n
  • @mvp AFAIK,double 是 64 位的,所以我们需要 128 位四倍。
  • IEEE754 double 有 53 位
  • 好像把18446744073709551615ul写成-1ul会更好。

标签: c integer-division


【解决方案1】:

我将在此处使用uint64_t(需要包含<stdint.h>),以免您假设unsigned long 的大小。

phuclv 使用-n 的想法很聪明,但可以变得更简单。作为无符号 64 位整数,我们有 -n = 264-n,然后 (-n)/n = 264/n - 1,我们可以简单加回 1。

uint64_t divide_two_to_the_64(uint64_t n) {
  return (-n)/n + 1;
}

生成的代码正是您所期望的(x86-64 上的 gcc 8.3,通过godbolt):

    mov     rax, rdi
    xor     edx, edx
    neg     rax
    div     rdi
    add     rax, 1
    ret

【讨论】:

  • 相当精明的观察。它似乎与if (twoPow64divphuclv (v) != twoPow64divnate (v)) { /* throw error */ } 保持一致,因为我愿意通过:) 等待尽可能多的数字虽然我确实让你的uint64_t twoPow64divnate (uint64_t n)
  • @DavidC.Rankin 如果您每秒可以进行 10 亿次迭代,那么您将需要大约 585 年的时间来遍历所有可能的 int64_t 值,所以不幸的是,您必须测试 32 位版本,然后检查它是否适用于某些 64 位值
  • 很好,但使用标准 uint64_t 而不是 unsigned long 不是更好吗? unsigned long 不保证是 64 位,即使在 64 位编译器上也是如此
  • @Jean-FrançoisFabre:当然,如果你愿意的话。这个问题明确说明了他们的假设,即unsigned long 在感兴趣的系统上是 64 位,所以我只是保持一致。但我现在改了。
【解决方案2】:

我想出了另一个受this question 启发的解决方案。从那里我们知道

(a1 + a2 + a3 + ... + an)/n =

(a1/n + a2/n + a3/n + ... + an/n) + (a1 % n + a2 % n + a3 % n + ... + an % n)/n

通过选择a1 = a2 = a3 = ... = an -1 = 1an = 264 - n em> 我们会有

(a1 + a2 + a3 + ... + an)/n = (1 + 1 + 1 + ... + (264 - n))/n = 264/n

= [(n - 1)*1/n + (264 - n)/n] + [(n - 1)*0 + (264 - n) % n]/n

= (264 - n)/n + ((264 - n) % n)/n

264 - n是n的2的补码,即-n,也可以写成@987654333 @。所以最终的解决方案是

uint64_t twoPow64div(uint64_t n)
{
    return (-n)/n + (n + (-n) % n)/n + (n > 1ULL << 63);
}

最后一部分是更正结果,因为我们处理的是无符号整数,而不是像另一个问题中的有符号整数。在我的电脑上检查了 32 位和 64 位版本,结果与您的解决方案匹配

但是在 MSVC 上有一个 intrinsic for 128-bit division,所以你可以像这样使用

uint64_t remainder;
return _udiv128(1, 0, n, &remainder);

这会产生最干净的输出

    mov     edx, 1
    xor     eax, eax
    div     rcx
    ret     0

这是demo

在大多数 x86 编译器上(一个值得注意的例外是 MSVC)long double 也具有 64 位精度,因此您可以使用其中任何一个

(uint64_t)(powl(2, 64)/n)
(uint64_t)(((long double)~0ULL)/n)
(uint64_t)(18446744073709551616.0L/n)

虽然性能可能会更差。这也可以应用于 long double 具有超过 63 位有效位的任何实现,例如 PowerPC 及其 double-double implementation

有一个关于计算((UINT_MAX + 1)/x)*x - 1: Integer arithmetic: Add 1 to UINT_MAX and divide by n without overflow 的相关问题也很聪明。基于此,我们有

264/n = (264 - n + n)/n = (264 - n)/n + 1 = (-n)/n + 1

这本质上只是获得Nate Eldredge's answer的另一种方式

这是godbolt上其他编译器的一些演示

另见:

【讨论】:

  • 既然(-n) /n 等于 (2^64-n)/n = 2^64/n - 1,我们不能只说(-n)/n + 1 吗?它似乎在我所有的测试中都给出了正确的答案。我错过了什么吗?
  • @NateEldredge 好点。我没有想到,但这似乎是合理的
【解决方案3】:

我们使用 64 位 CPU

哪个 64 位 CPU?

一般来说,如果你将一个有 N 位的数乘以另一个有 M 位的数,结果将有多达 N+M 位。对于整数除法,它是类似的——如果一个 N 位的数字除以一个 M 位的数字,结果将有 N-M+1 位。

因为乘法自然是“变宽”(结果的位数比任何一个源数都多),而整数除法自然是“变窄”的(结果位数少);一些 CPU 支持“扩大乘法”和“缩小除法”。

换句话说,一些 64 位 CPU 支持将 128 位数字除以 64 位数字以获得 64 位结果。例如,在 80x86 上,它是一条 DIV 指令。

不幸的是,C 不支持“扩大乘法”或“缩小除法”。它只支持“结果与源操作数大小相同”。

具有讽刺意味的是(对于 64 位 80x86 上的无符号 64 位除数)没有其他选择,编译器必须使用 DIV 指令将 128 位数字除以 64 位数字。这意味着 C 语言强制你使用 64 位分子,然后编译器生成的代码将你的 64 位分子扩展为 128 位,然后将其除以 64 位数字得到 64 位结果;然后你编写额外的代码来解决这个语言阻止你使用 128 位分子的事实。

希望您能看到这种情况如何被视为“不太理想”。

我想要的是一种诱使编译器支持“缩小除法”的方法。例如,可能通过滥用强制转换并希望优化器足够聪明,像这样:

  __uint128_t numerator = (__uint128_t)1 << 64;
  if(n > 1) {
      return (uint64_t)(numerator/n);
  }

我针对最新版本的 GCC、CLANG 和 ICC(使用 https://godbolt.org/)对此进行了测试,发现(对于 64 位 80x86)没有一个编译器足够聪明,无法意识到单个 DIV 指令就是全部这是必需的(他们都生成了执行call __udivti3 的代码,这是获得128 位结果的昂贵函数)。只有当(128 位)分子为 64 位时,编译器才会使用 DIV(并且前面会加上 XOR RDX,RDX 以将 128 位分子的最高半部分设置为零)。

换句话说,获得理想代码(64 位 80x86 上的 DIV 指令本身)的唯一方法可能是求助于内联汇编。

例如,没有内联汇编的最佳代码(来自 Nate Eldredge 的回答)将是:

    mov     rax, rdi
    xor     edx, edx
    neg     rax
    div     rdi
    add     rax, 1
    ret

...最好的代码是:

    mov     edx, 1
    xor     rax, rax
    div     rdi
    ret

【讨论】:

  • “最好的”代码的一个小缺点(或者可能是优点?)是,如果你不小心用n=1 尝试它,你会得到一个除法错误异常。其他建议都返回 0。
  • @NateEldredge:是的 - 这就是为什么我需要在 Godbolt 中测试时输入 if(n &gt; 1)(以确保编译器可以知道 n 不能为 1)。
  • 当前的编译器也不够聪明,无法将__(u)int128_t 除以常数转换为乘法,而是调用除法函数。而不是xor rax, rax,你应该使用xor eax, eax
【解决方案4】:

你的方法很不错。 可能这样写会更好:

return 18446744073709551615ul / n + ((n&(n-1)) ? 0:1);

希望确保编译器注意到它可以执行条件移动而不是分支。

编译和反汇编。

【讨论】:

  • 有趣的是,gcc 7 和 8 都使用条件集编译您的代码,并添加其结果。对于原始代码,gcc 7 进行了条件移动。 gcc 8 做了一个非常聪明的cmp rdi, 1 ; adc rax, 0 (!)。
  • 哇,太聪明了。我会对这些结果中的任何一个感到满意,但是您的 -n/n+1 当然要好得多——我很失望我没有想到它。
猜你喜欢
  • 2023-03-15
  • 1970-01-01
  • 2014-07-13
  • 2021-01-19
  • 2012-07-30
  • 2015-06-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多