计算 DRAM 峰值性能

calculating DRAM peak performance

亲爱的 Whosebug 社区,

我试图了解 DRAM 访问性能限制的计算,但我实现这些限制的基准与规范中的数字不太接近。当然,人们不会期望达到理论上的极限,但可能会有一些解释为什么这离得太远。

例如我的系统 DRAM 访问量约为 11 GB/s,但 WikiChip or the JEDEC spec 列出双通道 DDR4-2400 系统的峰值性能为 38.4 GB/s。

是我的测量有缺陷还是这些不是计算峰值内存性能的正确数字?

测量

在我的系统上 core i7 8550u at 1.8GHz from the (Kaby Lake Microarchitecture)

在这种情况下,lshw 显示了两个 memory 条目

     *-memory
        ...
        *-bank:0
             ...
             slot: ChannelA-DIMM0
             width: 64 bits
             clock: 2400MHz (0.4ns)
        *-bank:1
             ...
             slot: ChannelB-DIMM0
             width: 64 bits
             clock: 2400MHz (0.4ns)

所以这两个应该 运行 在“双通道”模式下(自动如此?)。

set up the system

降低测量噪声

然后我开始使用 pmbw - Parallel Memory Bandwidth Benchmark / Measurement 程序的 ScanWrite256PtrUnrollLoop 基准:

pmbw -f ScanWrite256PtrUnrollLoop -p 1 -P 1

内部循环可以用

检查
gdb -batch -ex "disassemble/rs ScanWrite256PtrUnrollLoop" `which pmbw` | c++filt

似乎这个基准创建了 vmovdqa Move Aligned Packed Integer Values AVX256-instructions to saturate the CPU's memory subsystem

的“流”
<+44>:  
  vmovdqa %ymm0,(%rax)
  vmovdqa %ymm0,0x20(%rax)
  vmovdqa %ymm0,0x40(%rax)
  vmovdqa %ymm0,0x60(%rax)
  vmovdqa %ymm0,0x80(%rax)
  vmovdqa %ymm0,0xa0(%rax)
  vmovdqa %ymm0,0xc0(%rax)
  vmovdqa %ymm0,0xe0(%rax)
  vmovdqa %ymm0,0x100(%rax)
  vmovdqa %ymm0,0x120(%rax)
  vmovdqa %ymm0,0x140(%rax)
  vmovdqa %ymm0,0x160(%rax)
  vmovdqa %ymm0,0x180(%rax)
  vmovdqa %ymm0,0x1a0(%rax)
  vmovdqa %ymm0,0x1c0(%rax)
  vmovdqa %ymm0,0x1e0(%rax)
  add    [=13=]x200,%rax
  cmp    %rsi,%rax
  jb     0x37dc <ScanWrite256PtrUnrollLoop(char*, unsigned long, unsigned long)+44>

作为 Julia 中的类似基准,我提出了以下内容:

const C = NTuple{K,VecElement{Float64}} where K
@inline function Base.fill!(dst::Vector{C{K}},x::C{K},::Val{NT} = Val(8)) where {NT,K}
  NB = div(length(dst),NT)
  k = 0
  @inbounds for i in Base.OneTo(NB)
    @simd   for j in Base.OneTo(NT)
      dst[k += 1] = x
    end
  end
end

调查这个fill!函数的内部循环时

code_native(fill!,(Vector{C{4}},C{4},Val{16}),debuginfo=:none)

我们可以看到,这也创建了一个类似的 vmovups Move Unaligned Packed Single-Precision Floating-Point Values 指令“流”:

L32:
  vmovups %ymm0, -480(%rcx)
  vmovups %ymm0, -448(%rcx)
  vmovups %ymm0, -416(%rcx)
  vmovups %ymm0, -384(%rcx)
  vmovups %ymm0, -352(%rcx)
  vmovups %ymm0, -320(%rcx)
  vmovups %ymm0, -288(%rcx)
  vmovups %ymm0, -256(%rcx)
  vmovups %ymm0, -224(%rcx)
  vmovups %ymm0, -192(%rcx)
  vmovups %ymm0, -160(%rcx)
  vmovups %ymm0, -128(%rcx)
  vmovups %ymm0, -96(%rcx)
  vmovups %ymm0, -64(%rcx)
  vmovups %ymm0, -32(%rcx)
  vmovups %ymm0, (%rcx)
  leaq    1(%rdx), %rsi
  addq    2, %rcx
  cmpq    %rax, %rdx
  movq    %rsi, %rdx
  jne     L32

现在,所有这些基准测试都以某种方式显示了三个缓存和主内存的不同“性能高原”:

但有趣的是,对于更大的测试规模,它们都绑定在 11 GB/s 左右:

使用多线程和(重新)激活频率缩放(使 CPU 的频率加倍)对较小的测试大小有影响,但并没有真正改变较大测试的这些发现。

经过一些调查,我发现 a blogpost describing the same problem with the recommendation to use so-called non-temporal write operations. There are multiple other resources, including an LWN article by Ulrich Drepper 有更多细节需要从这里开始研究。

在 Julia 中,这可以通过 SIMD.jl 包中的 vstorent 实现:

function Base.fill!(p::Ptr{T},len::Int64,y::Y,::Val{K},::Val{NT} = Val(16)) where
  {K,NT,T,Y <: Union{NTuple{K,T},T,Vec{K,T}}}
  # @assert Int64(p) % K*S == 0
  x  = Vec{K,T}(y)
  nb = max(div(len ,K*NT),0)
  S  = sizeof(T)
  p0 = p + nb*NT*K*S
  while p < p0
    for j in Base.OneTo(NT)
      vstorent(x,p) # non-temporal, `K*S` aligned store
      p += K*S
    end
  end
  Threads.atomic_fence()
  return nothing
end

对于 4 × Float64 的矢量宽度和 16 的展开因子,向下编译

code_native(fill!,(Ptr{Float64},Int64,Float64,Val{4},Val{16}),debuginfo=:none)

vmovntpsStore Packed Single-Precision Floating-Point Values Using Non-Temporal Hint说明:

...                                            # y is register-passed via %xmm0
        vbroadcastsd    %xmm0, %ymm0           # x = Vec{4,Float64}(y)
        nop
L48:
        vmovntps        %ymm0, (%rdi)
        vmovntps        %ymm0, 32(%rdi)
        vmovntps        %ymm0, 64(%rdi)
        vmovntps        %ymm0, 96(%rdi)
        vmovntps        %ymm0, 128(%rdi)
        vmovntps        %ymm0, 160(%rdi)
        vmovntps        %ymm0, 192(%rdi)
        vmovntps        %ymm0, 224(%rdi)
        vmovntps        %ymm0, 256(%rdi)
        vmovntps        %ymm0, 288(%rdi)
        vmovntps        %ymm0, 320(%rdi)
        vmovntps        %ymm0, 352(%rdi)
        vmovntps        %ymm0, 384(%rdi)
        vmovntps        %ymm0, 416(%rdi)
        vmovntps        %ymm0, 448(%rdi)
        vmovntps        %ymm0, 480(%rdi)
        addq    2, %rdi                      # imm = 0x200
        cmpq    %rax, %rdi
        jb      L48
L175:
        mfence                                  # Threads.atomic_fence()
        vzeroupper
        retq
        nopw    %cs:(%rax,%rax)

最多达到 34 GB/s 几乎是理论最大值 38.4 GB/s 的 90% GB/s。

实现的带宽似乎取决于各种因素:例如线程数以及是否启用频率缩放:

当频率达到最大值(4 GHz “turbo” 而不是 1.8 GHz ""no_turbo") 但如果没有频率缩放(即在 1.8 GHz 时)就无法实现 - 即使有多个线程也无法实现。