枚举 n 位所有值的时间

Time to enumerate all values in n bits

我正在寻找遍历可以用 n 位编码的所有可能值所需的理论时间,其中 n 很大(比如 80)。 我知道这需要一些指令,即测试、加法和跳转。但我无法弄清楚 CPU 将如何在这样的数字上管理这些操作,以及每次递增的循环总数。

感谢任何帮助,谢谢!

总结:每个值一个时钟周期,将结果存储到内存中,在乱序执行时非常容易CPU。除了存储结果之外的任何事情都会比递增更多的开销。

指令 throughput/latency 和 CPU 来自 Agner Fog 的执行管道信息。


80 位比普通 64 位 CPU 上的单个寄存器大,因此我们需要一对寄存器来保存这些值。即

uint64_t high, low;  // or a struct, or a 2-element array

无论是单独的变量、结构字段还是数组元素,任何体面的编译器都会将它们保存在寄存器中。 high 可以是 32 位变量。

有两种方法可以将这对数字从 0 增加到 2^80-1:

  • 慢速方式:low+=1, high = add-with-cary(high, 0).

  • 快捷方式:双嵌套循环:

low = 0;
for (high = 0 ; high < 1UL << (80-64) ; high++) {
    do {
        use_value(high,low);  // this point sees all 2^64 values of low
        low++;
    } while (low != 0);
    // low has wrapped to zero, so we don't even have to re-zero it
}

如果必须使用 32 位代码执行此操作,请使用三层嵌套循环。 (在 32 位代码中使用 uint64_t 可能会很慢,每次都添加进位。)

这应该编译成一个内部循环,每次迭代只需要一个周期。 (Intel Haswell 可以进行每周期一次的循环迭代,但 Intel Sandybridge 的分支吞吐量仅为每 2 个周期一次。)

我把这个 on godbolt 和 store(memory_order_relaxed) 放在原子全局变量中作为我的 use(high,low),以防止循环优化。存储比函数调用的开销更少,并且不需要 mov 指令将变量放入正确的寄存器中。它实际上编译成一个相当好的循环,应该 运行 在大多数 CPU 上每个时钟一个。

当我在原子存储之前有 low++ 时,它使用了一个额外的 test 指令,这是愚蠢的。它应该这样做:

loop80:
    xor edx, edx    # high
    xor eax, eax    # low
.Louter_loop:
    mov QWORD PTR global_h[rip], rdx    # or this could just be a 32bit store
.Linner_loop:
    mov QWORD PTR global_l[rip], rax    #,, low
    inc rax     # shorter encoding than add rax, 1
    jne .Linner_loop      # flags set by inc
 # inner loop is 2 fused-domain uops.  Or on AMD, 3 macro-ops since it can't fuse ALU ops with jcc

    inc edx     # only need to use the 32bit reg.
    cmp edx, 65536   # or count down from 65535 to zero, saving the cmp insn
    jne .Louter_loop

    ret

通过循环展开(gcc -O3 -funroll-loops),gcc 和 clang 可以使用 lea 指令并行执行 low+1、low+2、low+3 等,因此循环-携带依赖性仅为low+=8。这大大降低了所需的分支吞吐量,但现在瓶颈变成了存储吞吐量(每个时钟一个)。

如果我们不考虑存储,我们可以在寄存器中每个时钟生成 3 个 low 值。 (英特尔 SnB 和 Haswell 可以 运行 LEA 在两个执行端口上,并在第三个端口上融合算术+分支。)这使得没有执行资源可以自由地使用值做 任何事情 ,但您所要求的只是列举它们。 LEA 作为非破坏性的 3 操作数加法运算很有用。超过该吞吐量需要更多的 LEA 吞吐量,或不同的 CPU 架构。例如大多数 RISC CPUs(ARM、PowerPC 等)对大多数 ALU 操作都有 3 个操作数指令,而不是 dest = src 操作数之一。我没有查看 POWER 架构上的指令吞吐量(而且我发现指令助记符真的很难读......),但它可能能够并行执行 4 个加法。如果是这样,它可能会在此微基准测试中击败英特尔的 x86 实现。

注意我们是如何递增全局变量的。在 dep 链中具有 store->load 依赖性将使它至少有 6 个周期长。原子增量(lock inc [mem])需要相当长的时间。


因此,如果您需要对每个位模式任何事情,即使只是将其存储在内存中,也会占用 运行ning 时间。如果您不需要存储它,乱序执行CPU运行宁好的代码可以在每个时钟并行生成多个值。 (英特尔和 AMD 当前的实现每个时钟只能存储一次。)


如果你想开始变傻,可以使用向量指令递增 16(或 32,或 AVX512 为 64)8 位值,并对该向量进行一次 16B(或 32B 或 64B)存储每个时钟。但是,这不会在内存中产生连续的 80 位值,这就是为什么我称它为愚蠢的原因。