现代处理器上的并行内存访问

And*_*dge 9 memory parallel-processing x86 multicore bus

我有一个最近的12核Intel CPU(Haswell架构),它有4个内存通道.机器可以并行执行多少次DRAM内存访问?

例如,如果我有一个使用12个线程的程序,这些线程位于紧密循环中,从一个范围太大而无法容纳缓存的随机存储器地址读取单个字节.我希望所有12个线程将花费几乎所有时间等待内存提取.

线程是否必须轮流使用DRAM总线?

注意:假设我使用的是1 GB的VM页面大小,因此没有TLB缓存未命中.

And*_*dge 12

英特尔数据表几乎可以回答这个问题.

我的第一个线索是英特尔论坛上的一个问题:https: //communities.intel.com/thread/110798

Jaehyuk.Lee,2017年2月1日09:27问我几乎和我一样的问题:

第二个问题是关于IMC的同时请求及其对全新CPU模型的支持,例如skylake和kaby-lake http://www.intel.com/Assets/PDF/datasheet/323​​341.pdf根据上述链接,"内存控制器最多可以同时运行32个请求(读取和写入)"我想知道skylake和kabylake CPU支持多少个同时请求.我已经检查了第6代和第7代英特尔CPU数据表,但我找不到任何信息.

链接已经死了.但他的"32"人物听起来似乎有道理.

一位英特尔工作人员回应,引用第六代英特尔®处理器系列S-Platforms,第1卷:

存储器控制器具有高级命令调度器,其中同时检查所有未决请求以确定下一个要发布的最有效请求.从所有待处理的请求中挑选出最有效的请求,并将其发布到系统内存即时,以便最佳地使用命令重叠.因此,不是让所有存储器访问请求单独通过强制请求一次一个地执行的仲裁机制,而是可以在不干扰允许并发发出请求的当前请求的情况下启动它们.这允许优化带宽和减少延迟,同时保持适当的命令间隔以满足系统存储器协议.

令人讨厌的是,我的Xeon E5-2670 v3的数据表不包含等效的部分.

另一部分答案是E5-2670有4个DDR通道.内存以256字节的粒度交错,以优化带宽.换句话说,如果从地址0读取1024字节块,则从D​​IMM 0读取前256个字节.字节256到511来自DIMM 1等.

将两者结合在一起,我怀疑内存控制器可以并行执行4次读取,并且足够智能,如果4个或更多线程正在等待映射到4个不同DIMM的读取,则它将并行执行.它有足够的硬件可以在其调度表中保持大约32个读/写.

我可以想到另一种实现并行性的可能方法.每个DDR通道都有自己的数据和地址总线.当存储器控制器请求读取时,它使用地址线+一些控制线来请求读取,然后等待响应.对于随机读取,通常有两个等待 - RAS到CAS延迟和CAS延迟 - 每个约15个周期.您可以想象内存控制器在这些等待期间从另一个DIMM(*)开始另一次读取,而不是让地址线空闲.我不知道这是否已经完成.

*实际上,根据Anandtech的这篇文章,DRAM硬件的并行性要高于每个通道只有多个DIMM.每个DIMM可能有多个排名,每个排名都有多个银行.我认为您可以切换到DIMM中的任何其他级别和存储区以并行执行另一个访问.

编辑

尽管只有4个内存控制器,我测量了我的机器可以并行进行至少6次随机访问.因此,单个存储器通道可以并行执行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];
}
Run Code Online (Sandbox Code Playgroud)

循环的每次迭代平均需要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);
}
Run Code Online (Sandbox Code Playgroud)

然后每次迭代需要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;
}
Run Code Online (Sandbox Code Playgroud)

// *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));
}
Run Code Online (Sandbox Code Playgroud)

他们都表现得差不多.我不记得确切的数字了.我看到的峰值单线程性能是使用更简单的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
Run Code Online (Sandbox Code Playgroud)

我不明白"g_seed(%rip)".这是内存访问吗?为什么编译器会这样做?

编辑4 - 从随机数生成器中删除全局变量

我按照Peter的建议从随机数生成器中删除了全局变量.生成的代码确实更清晰.我还为反汇编切换到Intel语法(感谢提示).

// 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
Run Code Online (Sandbox Code Playgroud)

但是,单个流程和多流程案例的性能都没有改变.