我有一颗最近的12核英特尔CPU(Haswell架构),它有4个内存通道。这台机器可以同时执行多少DRAM内存访问?
例如,如果我有一个程序使用12个线程,在一个紧密循环中读取随机内存地址的单个字节,而地址范围太大无法放入缓存中。我预计所有12个线程将几乎全部时间都在等待内存获取。
这些线程必须轮流使用DRAM总线吗?
注意:假设我使用1GB VM页面大小,因此没有TLB缓存未命中。
我有一颗最近的12核英特尔CPU(Haswell架构),它有4个内存通道。这台机器可以同时执行多少DRAM内存访问?
例如,如果我有一个程序使用12个线程,在一个紧密循环中读取随机内存地址的单个字节,而地址范围太大无法放入缓存中。我预计所有12个线程将几乎全部时间都在等待内存获取。
这些线程必须轮流使用DRAM总线吗?
注意:假设我使用1GB VM页面大小,因此没有TLB缓存未命中。
英特尔的数据手册几乎回答了这个问题。
我的第一个线索来自于英特尔论坛上的一个问题: 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/323341.pdf 上面的链接,“内存控制器可以处理最多32个同时请求(读和写操作)”,我想知道Skylake和Kabylake CPU支持多少个同时请求。我已经查看了英特尔CPU数据手册的第6和第7代,但没有找到任何信息。
该链接已失效。但他的“32”数字听起来很有道理。
一位英特尔员工回复了他,并引用了第1卷:针对S平台的第6代英特尔®处理器系列中的内容:
内存控制器具有先进的命令调度程序,它同时检查所有挂起的请求以确定下一个发出的最有效请求。从所有挂起的请求中选出最有效的请求,并即时发出到系统内存,以充分利用命令重叠。因此,不必让所有内存访问请求通过仲裁机制单独进行,强制请求一次执行一次,而是可以在不干扰当前请求的情况下开始执行,从而实现并发请求的发出。这样可以实现优化带宽和减少延迟,同时保持适当的命令间隔,以满足系统内存协议要求。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纳秒。这是因为我的CPU中的乱序执行和预测执行功能能够将该循环并行化6次。即10纳秒=60纳秒/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);
}
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纳秒的摊销,意味着可以并行读取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
// 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
性能没有发生变化,无论是单进程还是多进程情况。
rand32()
函数有多慢,以及它是否访问内存(这将命中缓存,但CPU必须与cache-miss负载进行推测性重排序)。此外,如果速度太慢,ROB大小限制可能会防止OOO执行查看那么大的指令窗口。 - Peter Cordesgcc -masm = intel
和objdump -drwC -Mintel
),所以是的,g_seed(%rip)
是一个静态内存位置,使用RIP相对寻址(对于x86-64来说很正常)。请使用static g_seed = ...
(最好作为函数范围变量,但文件范围也可以,只要它是static
而不是全局的),这样编译器就可以将其优化为寄存器,而不是让它保留给外部函数查看。 - Peter Cordesfastrand()
线性同余生成器很棒。它甚至比xorshift
更简单,但仍然足够随机。更好的是,它不会重复相同的地址,直到看到每个其他地址,因为周期与范围相同。在这种情况下,这是一个特点。 - Peter Cordesaddb $1, (%rdi,%rax)
正在写入表格,就像 table_of_random_numbers[offset]++
一样。这可能解释了每个核心少于10个未完成的负载,因为某些 LFB 忙于存储,当需要驱逐脏缓存行时。还要注意,char*
可以与任何东西别名,因此如果编译器不确定表格存储是否别名 g_seed
,它将不得不在循环内部存储/重新加载它。这是将其作为本地变量并通过引用传递给 PRNG 的另一个原因,而不是使用全局变量(甚至是 static
)。 - Peter Cordes