我正在编写一个创建32个线程的代码,并将它们与我的多核多处理器系统中的32个核心中的每个核心设置密切关系。
线程只是执行RDTSCP指令,并且值存储在非重叠位置的共享数组中,这是共享数组:
uint64_t rdtscp_values[32];
因此,每个线程都将根据其核心编号写入特定的阵列位置。
最重要的是,一切都正常,但我知道我可能没有使用正确的数据结构来避免 缓存行反弹 。
P.S: 我已经检查过我的处理器缓存行是64字节宽。
因为我使用的是简单的 uint64_t 数组,所以它意味着由于预读,单个缓存行将存储此数组的8个位置。
由于这个简单的数组,虽然线程写入不同的索引,但我的理解是,每次写入此数组都会导致所有其他线程的缓存失效?
如何创建与缓存行对齐的结构?
我的系统是: 2x Intel Xeon E5-2670 2.30GHz(8核,16线程)
答案 0 :(得分:3)
是的,你绝对想避免"虚假分享"和缓存行乒乓。 但这可能没有意义:如果这些内存位置比其他线程收集的线程更私密,它们应该与其他每线程数据一起存储,这样你就不会浪费缓存占用56个字节的填充。另见Cache-friendly way to collect results from multiple threads。 (没有很好的答案;如果可以的话,避免设计一个需要真正细粒度收集结果的系统。)
但是,让我们假设一分钟,不同线程的插槽之间未使用的填充实际上就是你想要的。
是的,您需要将步幅设为64个字节(1个缓存行),但实际上您并不需要使用每个 start 的8B缓存行。因此,只要uint64_t
对象自然对齐(因此它们不会在缓存行边界上分割),您就不需要任何额外的对齐。
如果每个线程都写入其缓存行的第3个qword而不是第1个qword,那就没问题了。 OTOH,对齐到64B确保没有其他东西与第一个元素共享一个缓存行,这很容易,所以我们也可以。
静态存储:使用alignas()
或使用编译器特定的东西在ISO C11中对齐静态存储非常容易。
使用结构,隐式填充是为了使大小成为所需对齐的倍数。让一个成员具有对齐要求意味着整个结构需要至少那么多对齐。编译器通过静态和自动存储为您处理此问题,但您必须使用aligned_alloc
或替代方案来进行过度对齐的动态分配。
#include <stdalign.h> // for #define alignas _Alignas for C++ compat
#include <stdint.h> // for uint64_t
// compiler knows the padding is just padding
struct { alignas(64) uint64_t v; } rdtscp_values[32];
int foo(unsigned t) {
rdtscp_values[t].v = 1;
return sizeof(rdtscp_values[0]); // yes, this is 64
}
或使用数组as suggested by @ Eric Postpischil:
alignas(64) // optional, stride will still be 64B without this.
uint64_t rdtscp_values_2d[32][8]; // 8 uint64_t per cache line
void bar(unsigned t) {
rdtscp_values_2d[t][0] = 1;
}
alignas()
是可选的,如果您不关心64B对齐的整个事物,只需要在您使用的元素之间进行64B跨步。你也可以在GNU C或C ++中使用__attribute__((aligned(64)))
,或者在__declspec(align(64))
用于MSVC,使用#ifdef
来定义可在主要x86编译器中移植的ALIGN宏。
无论哪种方式都产生相同的asm。我们可以检查编译器输出以验证我们得到了我们想要的东西。 I put it up on the Godbolt compiler explorer。我们得到:
foo: # and same for bar
mov eax, edi # zero extend 32-bit to 64-bit
shl rax, 6 # *64 is the same as <<6
mov qword ptr [rax + rdtscp_values], 1 # store 1
mov eax, 64 # return value = 64 = sizeof(struct)
ret
两个数组的声明方式相同,编译器从汇编器/链接器with the 3rd arg to .comm
请求64B对齐:
.comm rdtscp_values_2d,2048,64
.comm rdtscp_values,2048,64
动态存储空间:
如果线程数不是编译时常量,那么您可以使用对齐的分配函数来获得对齐的动态分配内存(特别是如果您想支持非常高数量的线程)。请参阅How to solve the 32-byte-alignment issue for AVX load/store operations?,但实际上只使用C11 aligned_alloc
。它非常适合这种情况,并返回一个与free()
兼容的指针。
struct { alignas(64) uint64_t v; } *dynamic_rdtscp_values;
void init(unsigned nthreads) {
size_t sz = sizeof(dynamic_rdtscp_values[0]);
dynamic_rdtscp_values = aligned_alloc(nthreads*sz, sz);
}
void baz(unsigned t) {
dynamic_rdtscp_values[t].v = 1;
}
baz:
mov rax, qword ptr [rip + dynamic_rdtscp_values]
mov ecx, edi # same code as before to scale by 64 bytes
shl rcx, 6
mov qword ptr [rax + rcx], 1
ret
数组的地址不再是链接时间常量,因此有一个额外的间接级别来访问它。但是指针在初始化后是只读的,因此它将保持在每个核心的缓存中共享,并在需要时重新加载非常便宜。
脚注:在i386 System V ABI中,uint64_t
默认情况下只有4B对齐内部结构(没有alignas(8)
或__attribute__((aligned(8)))
),所以如果你放int
在uint64_t
之前并且没有对整个结构进行任何对齐,就有可能获得缓存行分割。但编译器尽可能将它与8B对齐,因此你的struct-with padding仍然没问题。
答案 1 :(得分:0)
所以,我遵循了这里的评论,我必须感谢所有的贡献。
最后我得到了我的预期:每个线程正确使用缓存行。
这是共享结构:
typedef struct align_st {
uint64_t v;
uint64_t padding[7];
} align_st_t __attribute__ ((aligned (64)));
当结构加载到L1缓存时,我在结构中使用填充uint64_t padding[7]
来填充缓存行中的剩余字节。尽管如此,我要求编译器在编译__attribute__ ((aligned (64)))
时使用64字节内存对齐。
因此,我根据核心数动态分配此结构,使用memalign()
:
align_st_t *al = (align_st_t*) memalign(64, n_cores * sizeof(align_st_t));
为了比较它,我编写了一个使用这些对齐机制的代码版本( V1 ),以及使用简单数组方法的其他代码版本( V2 )。 / p>
通过执行perf,我得到了这些数字:
P.S。:每个帖子都写了1千次到共享结构的同一地址只是为了增加数字