计算 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)
所以这两个应该 运行 在“双通道”模式下(自动如此?)。
降低测量噪声
- 禁用频率缩放
- 禁用地址Space布局随机化
- 将
scaling_governor
设置为 performance
- 使用
cpuset
在自己的内核上隔离基准测试
- 设置 -20 的友好度
- 使用进程最少的无头系统 运行ning
然后我开始使用 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)
至vmovntps
Store 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 时)就无法实现 - 即使有多个线程也无法实现。
亲爱的 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)
所以这两个应该 运行 在“双通道”模式下(自动如此?)。
降低测量噪声- 禁用频率缩放
- 禁用地址Space布局随机化
- 将
scaling_governor
设置为performance
- 使用
cpuset
在自己的内核上隔离基准测试 - 设置 -20 的友好度
- 使用进程最少的无头系统 运行ning
然后我开始使用 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)
至vmovntps
Store 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 时)就无法实现 - 即使有多个线程也无法实现。