为什么 MOVNTI 在循环中重复存储到同一地址的速度不慢?

Why isn't MOVNTI slower, in a loop storing repeatedly to the same address?

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 个周期。为什么?毕竟还是没有loop-carried-dependency

结果是合理的。您的循环代码由以下指令组成。根据Agner Fog's instruction tables,这些时间安排如下:

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

所以

  • MOVNTI 消耗 2 个 uOps,1 个在端口 2 或 3,1 个在端口 4
  • ADD 在端口 0 或 1 或 5 或 6 中消耗 1 uOps
  • CMP 和 Jcc 宏融合到 table 中的最后一行,导致消耗 1 uOp

因为 ADDCMP+Jcc 都不依赖于 MOVNTI 的结果,所以它们可以在最近的架构上(几乎)并行执行,例如使用端口 1,2, 4,6。最坏的情况是 ADDCMP+Jcc 之间的延迟为 1。

这很可能是您代码中的设计错误:您实质上是在向同一地址写入 [array] 100000 次,因为您没有调整地址。

重复写入甚至可以进入the condition that

下的L1-cache

The memory type of the region being written to can override the non-temporal hint, if the memory address specified for the non-temporal store is in an uncacheable (UC) or write protected (WP) memory region.

但它看起来不像这样,无论如何也不会有太大的区别,因为即使写入内存,内存速度也会成为限制因素。

例如,如果您有 3GHz CPU 和 1600MHz DDR3-RAM,这将导致每个内存周期有 3/1.6 = 1.875 CPU 个周期。这似乎是合理的。

movnti 当重复写入同一地址时,显然可以维持每个时钟一个的吞吐量。

我认为 movnti 一直写入同一个 fill buffer,并且它不会经常刷新,因为没有其他加载或存储发生。 (那个link是关于用SSE4.1 NT加载从WC视频内存复制,以及用NT存储存储到普通内存。)

所以 NT 写入组合填充缓冲区就像一个缓存,用于将多个重叠的 NT 存储到同一地址,并且写入实际上是在填充缓冲区中命中,而不是每次都进入 DRAM。

DDR DRAM 仅支持突发传输命令。如果每个 movnti 产生一个 4B 的写入,内存芯片实际上是可见的,那么它不可能 运行 这么快。内存控制器要么必须 read/modify/write,要么进行中断的突发传输,因为 there is no non-burst write command. See also Ulrich Drepper's What Every Programmer Should Know About Memory

我们可以通过 运行一次在多个内核上进行测试来进一步证明是这种情况。 由于它们根本不会互相减慢速度,我们可以确定写入只是偶尔从 CPU 核心中取出并竞争内存周期。


你的实验没有显示你的循环 运行ning 在每时钟 4 条指令(每次迭代一个周期)的原因是你使用了如此小的重复计数。 100k 周期几乎不占启动开销(perf 的时间包括在内)。

例如,在具有双通道 DDR2 533MHz 的 Core2 E6600 (Merom/Conroe) 上,包括所有进程启动/退出内容在内的总时间为 0.113846 毫秒。那只有 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 个副本可以同时 运行(在不同的物理内核上),并且每个副本都将同时完成,就好像它是单独 运行ning 一样。 (不过,如果您测量挂钟时间而不是周期计数,那么单核 Turbo 与多核 Turbo 也将是一个因素。)


zx485 的 uop 计数很好地解释了为什么循环没有受到前端或未融合域执行资源的瓶颈。

然而,这反驳了他关于 CPU 与内存时钟的比率与它有任何关系的理论。不过,有趣的巧合是,OP 选择了一个恰好使最终总 IPC 以这种方式计算的计数。


P.S Is there any loop-carried-dependency?

是的,循环计数器。 (1 个周期)。顺便说一句,你可以通过使用 dec / jg 倒数到零来保存一个 insn 而不是向上计数并且必须使用 cmp.


写后写内存依赖性不是正常意义上的 "true" 依赖性,但它是 CPU 必须跟踪的东西。 CPU 不会 "notice" 重复写入相同的值,因此必须确保最后写入的是 "counts".

这叫做architectural hazard。我认为这个术语在谈论内存时仍然适用,而不是寄存器。