section .text
%define n 100000
_start:
xor rcx, rcx
jmp .cond
.begin:
movnti [array], eax
.cond:
add rcx, 1
cmp rcx, n
jl .begin
section .data
array times 81920 db "A"
根据perf
,每个周期运行1.82条指令。我不明白为什么它如此之快。毕竟,它必须存储在内存(RAM)中,因此它应该很慢。
P.S是否存在循环携带依赖性?
section .text
%define n 100000
_start:
xor rcx, rcx
jmp .cond
.begin:
movnti [array+rcx], eax
.cond:
add rcx, 1
cmp rcx, n
jl .begin
section .data
array times n dq 0
现在,迭代每次迭代需要5个周期。为什么?毕竟,仍然没有循环携带依赖。
答案 0 :(得分:2)
movnti
显然可以维持每个时钟一个的吞吐量。
我认为movnti
一直在写fill buffer,并且没有经常刷新,因为没有其他负载或商店发生。 (该链接是关于使用SSE4.1 NT加载从 WC视频内存中复制,以及使用NT存储区存储到普通内存。)
因此,NT写入组合填充缓冲区就像一个缓存,用于多个重叠的NT存储到同一地址,并且写入实际上是在填充缓冲区中而不是每次都进入DRAM。
DDR DRAM仅支持突发传输命令。如果每个movnti
产生了一个实际上对内存芯片可见的4B写入,那么它就无法以那么快的速度运行。由于there is no non-burst write command,存储器控制器必须读取/修改/写入或执行中断的突发传输。另见Ulrich Drepper's What Every Programmer Should Know About Memory。
我们可以通过一次在多个核心上运行测试来进一步证明这种情况。 由于它们根本不会相互减慢速度,因此我们可以确定写入很少会使其从CPU内核中脱离并竞争内存周期。
你的实验没有显示你的循环以每个时钟4个指令运行(每次迭代一个循环)的原因是你使用了如此微小的重复计数。 100k周期几乎不考虑启动开销(perf
的时间包括)。
例如,在具有双通道DDR2 533MHz的Core2 E6600(Merom / Conroe)上,包括所有进程启动/退出内容的总时间为0.113846 ms。那只有266,007个周期。
更合理的微基准标记显示每个周期一次迭代(一个movnti
):
global _start
_start:
xor ecx,ecx
.begin:
movnti [array], eax
dec ecx
jnz .begin ; 2^32 iterations
mov eax, 60 ; __NR_exit
xor edi,edi
syscall ; exit(0)
section .bss
array resb 81920
(asm-link
is a script I wrote)
$ asm-link movnti-same-address.asm
+ yasm -felf64 -Worphan-labels -gdwarf2 movnti-same-address.asm
+ ld -o movnti-same-address movnti-same-address.o
$ perf stat -e task-clock,cycles,instructions ./movnti-same-address
Performance counter stats for './movnti-same-address':
1835.056710 task-clock (msec) # 0.995 CPUs utilized
4,398,731,563 cycles # 2.397 GHz
12,891,491,495 instructions # 2.93 insns per cycle
1.843642514 seconds time elapsed
$ time ./movnti-same-address; time ./movnti-same-address & time ./movnti-same-address &
real 0m1.844s / user 0m1.828s # running alone
[1] 12523
[2] 12524
peter@tesla:~/src/SO$
real 0m1.855s / user 0m1.824s # running together
real 0m1.984s / user 0m1.808s
# output compacted by hand to save space
我希望完美的SMP扩展(超线程除外),可达到任意数量的内核。例如在一个10核的Xeon上,这个测试的10个拷贝可以同时运行(在不同的物理内核上),每个拷贝都可以在同一时间内完成,就像它独自运行一样。 (但是,如果你测量的是挂钟时间而不是周期数,那么单核涡轮增压和多核涡轮增压也是一个因素。)
zx485的uop计数很好地解释了为什么循环不会受到前端或未融合域执行资源的瓶颈。
然而,这反驳了他关于CPU与内存时钟之比与其有关的理论。然而,有趣的巧合是,OP选择了一个计数,恰好使IPC的最终总体工作成功。
P.S是否存在循环携带依赖性?
是的,循环计数器。 (1个周期)。顺便说一句,您可以通过使用dec
/ jg
向下计数而不是计算并且必须使用cmp
来保存insn。
写后写内存依赖性不是" true"在正常意义上的依赖,但它是CPU必须跟踪的东西。 CPU没有注意到"重复写入相同的值,因此必须确保最后一次写入是"计算"。
这称为architectural hazard。我认为这个术语在谈论记忆而不是寄存器时仍然适用。
答案 1 :(得分:1)
结果似乎是合理的。你的循环代码包含以下的指令。根据{{3}},这些具有以下时间:
Instruction regs fused unfused ports Latency Reciprocal Throughput
---------------------------------------------------------------------------------------------------------------------------
MOVNTI m,r 2 2 p23 p4 ~400 1
ADD r,r/i 1 1 p0156 1 0.25
CMP r,r/i 1 1 p0156 1 0.25
Jcc short 1 1 p6 1 1-2 if predicted that the jump is taken
Fused CMP+Jcc short 1 1 p6 1 1-2 if predicted that the jump is taken
所以
因为ADD
和CMP+Jcc
都不依赖于MOVNTI
的结果,所以它们可以在最近的体系结构上并行执行(例如),例如使用端口1,2,4, 6。最糟糕的情况是ADD
和CMP+Jcc
之间的延迟为1。
这很可能是代码中的设计错误:您实际上是在100000次写入同一地址[array]
,因为您没有调整地址。
重复写入甚至可以转到Agner Fog's instruction tables
下的L1缓存如果为非临时存储指定的内存地址位于不可缓存(UC)或写保护(WP)内存区域中,则写入的区域的内存类型可以覆盖非时间提示。
但它看起来并不像这样,无论如何也不会产生很大的不同,因为即使写入内存,内存速度也将成为限制因素。
例如,如果你有一个3GHz CPU和1600MHz DDR3-RAM,这将导致每个内存周期3 / 1.6 = 1.875个CPU周期。这似乎是合理的。