我最近有12核Intel CPU(Haswell架构),它有4个内存通道。机器可以并行执行多少次DRAM内存访问?
例如,如果我有一个使用12个线程的程序,这个线程位于一个紧密的循环中,它在一个范围太大而无法容纳在缓存中的随机存储器地址中读取单个字节。我希望所有12个线程花费几乎所有时间等待内存提取。
线程是否必须轮流使用DRAM总线?
注意:假设我使用的是1 GB的VM页面大小,因此没有TLB缓存未命中。
答案 0 :(得分:9)
英特尔数据表几乎回答了这个问题。
我的第一个线索是英特尔论坛上的一个问题: https://communities.intel.com/thread/110798
Jaehyuk.Lee,01-Feb-2017 09:27问我几乎和我一样的问题:
第二个问题是关于IMC及其的同时请求 支持全新的CPU型号,如skylake和kaby-lake http://www.intel.com/Assets/PDF/datasheet/323341.pdf继续 以上链接,"内存控制器最多可以运行32个 同时请求(读写)"我想知道多少 skylake和kabylake CPU支持同时请求。我'已经 已经检查了第6代和第7代英特尔CPU数据表, 但我找不到任何信息。
链接已经死了。但他的" 32"这听起来似乎有道理。
英特尔工作人员回应,引自6th Generation Intel® Processor Families for S-Platforms, Vol 1:
内存控制器有一个高级命令调度程序 同时检查未决请求以确定最多 有效的要求下一步发布。最有效的要求是 从所有待处理的请求中挑选并发布到系统内存 及时使用Command Overlapping。因此,相反 让所有内存访问请求单独通过 仲裁机制强制要求一次执行一个请求, 它们可以在不干扰当前请求的情况下启动 允许并发发出请求。这允许优化 带宽和减少的延迟,同时保持适当的命令 间距以满足系统内存协议。
我的Xeon E5-2670 v3数据表令人讨厌地没有相应的部分。
答案的另一部分是E5-2670有4个DDR通道。内存以256字节的粒度交错,以优化带宽。换句话说,如果从地址0读取1024字节块,则从DIMM 0读取前256个字节。字节256到511来自DIMM 1等。
将两者放在一起,我怀疑内存控制器可以并行执行4次读取,并且足够聪明,如果4个或更多线程正在等待映射到4个不同DIMM的读取,它将并行执行。它有足够的硬件可以在其调度表中保持大约32个读/写。
我可以想到另一种实现并行性的可能方法。每个DDR通道都有自己的数据和地址总线。当存储器控制器请求读取时,它使用地址线+一些控制线来请求读取,然后等待响应。对于随机读取,通常有两个等待 - RAS到CAS延迟和CAS延迟 - 每个约15个周期。您可以想象内存控制器在这些等待期间从另一个DIMM(*)开始另一次读取,而不是让地址线空闲。我不知道这是否已经完成。
*实际上,根据this Anandtech article,DRAM硬件的并行性要高于每个通道只有多个DIMM。每个DIMM可能有多个排名,每个排名都有多个银行。我认为您可以切换到DIMM中的任何其他级别和存储区以并行执行另一个访问。
修改强>
我测量到我的机器至少可以进行6次并行随机访问,尽管只有4个内存控制器。因此,单个存储器通道可以并行执行2个或更多随机访问,可能使用上面段落中描述的方案。
要获取此信息,我使用tinymembench来衡量计算机上DRAM访问的延迟。结果是60 ns。然后我写了一个小的C程序,从1 GB的随机数表中执行32位读取,并使用结果递增校验和。伪代码:
uint32_t checksum = 0;
for (int i = 0; i < 256 * 1024 * 1024; i++) {
unsigned offset = rand32() & (TABLE_SIZE - 1);
checksum += table_of_random_numbers[offset];
}
循环的每次迭代平均花费10 ns。这是因为我的CPU中的乱序和推测执行功能能够将此循环并行化6次。即10 ns = 60 ns / 6.
如果我用以下代码替换了代码:
unsigned offset = rand32() & (TABLE_SIZE - 1);
for (int i = 0; i < 256 * 1024 * 1024; i++) {
offset = table_of_random_numbers[offset];
offset &= (TABLE_SIZE - 1);
}
然后每次迭代需要60 ns,因为循环不能并行化。它不能被平行化,因为每次访问的地址取决于先前读取的结果。
我还检查了编译器生成的程序集,以确保它没有完成并行化。
编辑2
我决定测试当我并行运行多个测试时会发生什么,每个测试都是一个单独的过程。我使用上面的程序片段,其中包括校验和(即看起来每次访问的延迟为10 ns)。通过并行运行6个实例,我的平均表观延迟为13.9 ns,这意味着大约26次访问必须并行发生。(60 ns / 13.9 ns)* 6 = 25.9。
6个实例是最佳的。不再导致整体吞吐量下降。
编辑3 - 回应Peter Cordes RNG问题
我尝试了两种不同的随机数生成器。
uint32_t g_seed = 12345;
uint32_t fastrand() {
g_seed = 214013 * g_seed + 2531011;
return g_seed;
}
和
// *Really* minimal PCG32 code / (c) 2014 M.E. O'Neill / pcg-random.org
// Licensed under Apache License 2.0 (NO WARRANTY, etc. see website)
typedef struct { uint64_t state; uint64_t inc; } pcg32_random_t;
uint32_t pcg32_random_r(pcg32_random_t* rng)
{
uint64_t oldstate = rng->state;
// Advance internal state
rng->state = oldstate * 6364136223846793005ULL + (rng->inc|1);
// Calculate output function (XSH RR), uses old state for max ILP
uint32_t xorshifted = ((oldstate >> 18u) ^ oldstate) >> 27u;
uint32_t rot = oldstate >> 59u;
return (xorshifted >> rot) | (xorshifted << ((-rot) & 31));
}
他们都表现得差不多。我无法记住确切的数字。我看到的峰值单线程性能是使用更简单的RNG,它给出了8.5 ns的分摊延迟,意味着7个并行读取。定时循环的程序集是:
// Pseudo random number is in edx
// table is in rdi
// loop counter is in rdx
// checksum is in rax
.L8:
imull $214013, %edx, %edx
addl $2531011, %edx
movl %edx, %esi
movl %edx, g_seed(%rip)
andl $1073741823, %esi
movzbl (%rdi,%rsi), %esi
addq %rsi, %rax
subq $1, %rcx
jne .L8
ret
我不明白&#34; g_seed(%rip)&#34;。这是内存访问吗?为什么编译器会这样做?
编辑4 - 从随机数生成器中删除全局变量
我从Peter建议的随机数生成器中删除了全局变量。生成的代码确实更清晰。我也转而使用英特尔语法进行反汇编(感谢提示)。
// Pseudo random number is in edx
// table is in rdi
// loop counter is in rdx
// checksum is in rax
.L8:
imul edx, edx, 214013
add edx, 2531011
mov esi, edx
and esi, 1073741823
movzx esi, BYTE PTR [rdi+rsi]
add rax, rsi
sub rcx, 1
jne .L8
ret
虽然单个流程和多流程案例的性能没有变化。