我正在寻找通过所有可能以n位编码的可能值所需的理论时间,其中n很大(例如,80)。 我知道这需要一些指令,即测试,添加和跳转。但我无法弄清楚CPU如何在这样的数字上管理这些操作,以及每次增量的总周期数。
感谢任何帮助,谢谢!
答案 0 :(得分:0)
总结:每个值一个时钟周期,将结果存储到内存中,在无序执行CPU上非常容易。除了存储结果之外的任何东西都比增量更多。
来自Agner Fog的指令吞吐量/延迟和CPU执行管道信息。
80位比普通64位CPU上的单个寄存器大,因此我们需要一对寄存器来保存这些值。即。
uint64_t high, low; // or a struct, or a 2-element array
无论是单独的变量,结构域还是数组元素,任何体面的编译器都会将它们保存在寄存器中。 high
可以是32位变量。
有两种方法可以将此对从0增加到2 ^ 80-1:
缓慢的方式:低+ = 1,高=添加加密(高,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可能会很慢,每次都有一个add-carry。)
这应编译为内循环,每次迭代只需一个循环。 (英特尔Haswell可以进行每周期一次的循环迭代,但英特尔Sandybridge的每分钟循环吞吐量只有一个。)
我把这个on godbolt和store(memory_order_relaxed)作为我的use(high,low)
放在原子全局中,以防止循环优化。存储比函数调用具有更少的开销,并且不需要mov指令来将变量放入正确的寄存器中。事实上它确实编译成一个相当好的循环,应该在大多数CPU上每个时钟运行一次。
当我在原子商店之前使用低++时,它使用了额外的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
指令并行执行低+ 1,低+ 2,低+ 3等,因此循环执行依赖只是低+ = 8。这大大降低了所需的分支吞吐量,但现在瓶颈变成了存储吞吐量(每个时钟一个)。
如果我们遗漏商店,我们可以在寄存器中为每个时钟生成3 low
个值。 (英特尔SnB和Haswell可以在两个执行端口上运行LEA,并在第三个端口上运行融合算术+分支。)这样就不会有任何执行资源可以自由地使用值来执行任何,但你要求的只是枚举他们。 LEA可用作非破坏性3操作数添加操作。击败吞吐量需要更多的LEA吞吐量或不同的CPU架构。例如对于大多数ALU操作,大多数RISC CPU(ARM,PowerPC等)具有3操作数指令,而不是具有dest = src操作数之一。我还没有看过POWER架构上的指令吞吐量(我发现指令助记符真的难以阅读......),但它可能能够并行执行4次添加。如果是这样,它可以在这个微基准测试中击败英特尔的x86实现。
注意我们如何不递增全局变量。在dep链中具有store-gt; load依赖性将使其至少长达6个周期。原子增量(lock inc [mem]
)需要更长的时间。
因此,如果您需要对每个位模式执行任何操作,即使只将其存储在内存中,也会占用运行时间。如果您不需要存储它,那么运行良好代码的无序执行CPU可以每个时钟并行生成多个值。 (英特尔和AMD目前的实现每个时钟只能存储一次。)
如果你想开始变得愚蠢,可以使用向量指令递增16(或32或64与AVX512)8位值,并为每个时钟执行一次16B(或32B或64B)存储。但是,这不会在内存中产生连续的80位值,这就是我称之为愚蠢的原因。