对齐结构,同时最大限度地减少缓存行浪费
Align struct while minimizing cache line waste
关于如何将结构与缓存行对齐(例如,Aligning to cache line and knowing the cache line size)有很多很棒的话题。
假设您有一个缓存行大小为 256B 的系统和一个大小为 17B 的结构(例如,一个包含两个 uint64_t
和一个 uint8_t
的紧凑结构)。如果将结构与缓存行大小对齐,则每个结构实例将恰好加载一个缓存行。
对于缓存行大小为 32B 甚至 64B 的机器,这将有利于性能,因为我们避免了必须获取 2 个缓存行,因为我们绝对不会跨越 CL 边界。
但是,在 256B 机器上,这会浪费大量内存,并在遍历此结构的 array/vector 时导致不必要的加载。事实上,您可以在单个缓存行中存储该结构的 15 个实例。
我的问题有两个:
- 在 C++17 及更高版本中,使用
alignas
,我可以对齐缓存行大小。但是,我不清楚如何以类似于“在缓存行中放置尽可能多的实例而不跨越缓存行边界,然后从下一个缓存行开始”的方式强制对齐。所以像这样:
上面的框是缓存行,其他框是我们的小结构的实例。
- 我真的想要这个吗?我真的不能全神贯注于此。通常,我们说如果我们将我们的结构与缓存行大小对齐,访问会更快,因为我们只需要加载一个缓存行。但是,看到我的示例,我想知道这是否真的如此。不对齐不是更快,而是将更多实例存储在单个缓存行中吗?
非常感谢您在这里的投入。非常感谢。
你 可以 通过将结构本身声明为 under-aligned 来打包到缓存行中,使用 GNU C __attribute__((packed))
或其他东西,所以它有 sizeof( struct) = 17 而不是通常的填充,您可以使结构大小成为 8 字节的倍数(由于具有 uint64_t 成员,它通常具有的对齐方式,假设 alignof(uint64_t) == 8).
然后放在一个alignas(256) T array[]
中,所以只对齐数组的开头。
对齐到比结构对象本身更宽的边界只有在包含多个结构的更大对象的情况下才有可能; ISO C++ 的对齐系统不能指定一个对象只能放在从某个对齐边界开始的容器中;这会导致无意义的情况,例如 T *arr
是有效数组的开始,但 arr + 1
不是有效数组,即使它具有相同的类型。
除非你担心两个线程之间的错误共享,否则你最多应该 naturally-align 你的结构,例如到 32 字节。 naturally-aligned 对象 (alignof=sizeof) 不能跨越比其自身大的对齐边界。但这浪费了很多 space,所以最好让编译器将它对齐 8,继承 alignof(struct)
= max (alignof(members))
.
另请参阅 以了解有关 space 内部 单个结构如何工作的更多详细信息。
根据访问数据的方式,一种选择是将成对的 uint64_t
存储在一个数组中,并将相应的字节存储在单独的数组中。你没有填充字节的方式,一切都是 2 大小和对齐的幂。但是随机访问所有 3 个成员可能会导致 2 次缓存未命中而不是 1 次。
但是对于按顺序迭代数据,这非常好。
针对 (2),尚不清楚使用打包结构的额外开销(例如,未对齐的 64 位访问)和访问数组元素的额外数学运算是否值得。但是如果你想尝试一下,你可以创建一个新的结构来适当地打包你的结构元素,然后创建一个小包装器 class 来像访问数组一样访问元素:
#include <array>
#include <iostream>
using namespace std;
template <typename T, size_t BlockAlignment>
struct __attribute__((packed)) Packer
{
static constexpr size_t NUM_ELEMS = BlockAlignment / sizeof(T);
static_assert( NUM_ELEMS > 0, "BlockAlignment too small for one object." );
T &operator[]( size_t index ) { return packed[index]; }
T packed[ NUM_ELEMS ];
uint8_t padding[ BlockAlignment - sizeof(T)*NUM_ELEMS ];
};
template <typename T, size_t NumElements, size_t BlockAlignment>
struct alignas(BlockAlignment) PackedAlignedArray
{
typedef Packer<T, BlockAlignment> PackerType;
std::array< PackerType, NumElements / PackerType::NUM_ELEMS + 1 > packers;
T &operator[]( size_t index ) {
return packers[ index / PackerType::NUM_ELEMS ][ index % PackerType::NUM_ELEMS ];
}
};
struct __attribute__((packed)) Foo
{
uint64_t a;
uint64_t b;
uint8_t c;
};
int main()
{
static_assert( sizeof(Foo) == 17, "Struct not packed for test" );
constexpr size_t NUM_ELEMENTS = 10;
constexpr size_t BLOCK_ALIGNMENT = 64;
PackedAlignedArray<Foo, NUM_ELEMENTS, BLOCK_ALIGNMENT> theArray;
for ( size_t i=0; i<NUM_ELEMENTS; ++i )
{
// Display the memory offset between the current
// element and the start of the array
cout << reinterpret_cast<std::ptrdiff_t>(&theArray[i]) -
reinterpret_cast<std::ptrdiff_t>(&theArray[0]) << std::endl;
}
return 0;
}
程序的输出显示了 17 字节元素在内存中地址的字节偏移量,每四个元素自动重置为 64 的倍数:
0
17
34
64
81
98
128
145
162
192
关于如何将结构与缓存行对齐(例如,Aligning to cache line and knowing the cache line size)有很多很棒的话题。
假设您有一个缓存行大小为 256B 的系统和一个大小为 17B 的结构(例如,一个包含两个 uint64_t
和一个 uint8_t
的紧凑结构)。如果将结构与缓存行大小对齐,则每个结构实例将恰好加载一个缓存行。
对于缓存行大小为 32B 甚至 64B 的机器,这将有利于性能,因为我们避免了必须获取 2 个缓存行,因为我们绝对不会跨越 CL 边界。
但是,在 256B 机器上,这会浪费大量内存,并在遍历此结构的 array/vector 时导致不必要的加载。事实上,您可以在单个缓存行中存储该结构的 15 个实例。
我的问题有两个:
- 在 C++17 及更高版本中,使用
alignas
,我可以对齐缓存行大小。但是,我不清楚如何以类似于“在缓存行中放置尽可能多的实例而不跨越缓存行边界,然后从下一个缓存行开始”的方式强制对齐。所以像这样:
上面的框是缓存行,其他框是我们的小结构的实例。
- 我真的想要这个吗?我真的不能全神贯注于此。通常,我们说如果我们将我们的结构与缓存行大小对齐,访问会更快,因为我们只需要加载一个缓存行。但是,看到我的示例,我想知道这是否真的如此。不对齐不是更快,而是将更多实例存储在单个缓存行中吗?
非常感谢您在这里的投入。非常感谢。
你 可以 通过将结构本身声明为 under-aligned 来打包到缓存行中,使用 GNU C __attribute__((packed))
或其他东西,所以它有 sizeof( struct) = 17 而不是通常的填充,您可以使结构大小成为 8 字节的倍数(由于具有 uint64_t 成员,它通常具有的对齐方式,假设 alignof(uint64_t) == 8).
然后放在一个alignas(256) T array[]
中,所以只对齐数组的开头。
对齐到比结构对象本身更宽的边界只有在包含多个结构的更大对象的情况下才有可能; ISO C++ 的对齐系统不能指定一个对象只能放在从某个对齐边界开始的容器中;这会导致无意义的情况,例如 T *arr
是有效数组的开始,但 arr + 1
不是有效数组,即使它具有相同的类型。
除非你担心两个线程之间的错误共享,否则你最多应该 naturally-align 你的结构,例如到 32 字节。 naturally-aligned 对象 (alignof=sizeof) 不能跨越比其自身大的对齐边界。但这浪费了很多 space,所以最好让编译器将它对齐 8,继承 alignof(struct)
= max (alignof(members))
.
另请参阅
根据访问数据的方式,一种选择是将成对的 uint64_t
存储在一个数组中,并将相应的字节存储在单独的数组中。你没有填充字节的方式,一切都是 2 大小和对齐的幂。但是随机访问所有 3 个成员可能会导致 2 次缓存未命中而不是 1 次。
但是对于按顺序迭代数据,这非常好。
针对 (2),尚不清楚使用打包结构的额外开销(例如,未对齐的 64 位访问)和访问数组元素的额外数学运算是否值得。但是如果你想尝试一下,你可以创建一个新的结构来适当地打包你的结构元素,然后创建一个小包装器 class 来像访问数组一样访问元素:
#include <array>
#include <iostream>
using namespace std;
template <typename T, size_t BlockAlignment>
struct __attribute__((packed)) Packer
{
static constexpr size_t NUM_ELEMS = BlockAlignment / sizeof(T);
static_assert( NUM_ELEMS > 0, "BlockAlignment too small for one object." );
T &operator[]( size_t index ) { return packed[index]; }
T packed[ NUM_ELEMS ];
uint8_t padding[ BlockAlignment - sizeof(T)*NUM_ELEMS ];
};
template <typename T, size_t NumElements, size_t BlockAlignment>
struct alignas(BlockAlignment) PackedAlignedArray
{
typedef Packer<T, BlockAlignment> PackerType;
std::array< PackerType, NumElements / PackerType::NUM_ELEMS + 1 > packers;
T &operator[]( size_t index ) {
return packers[ index / PackerType::NUM_ELEMS ][ index % PackerType::NUM_ELEMS ];
}
};
struct __attribute__((packed)) Foo
{
uint64_t a;
uint64_t b;
uint8_t c;
};
int main()
{
static_assert( sizeof(Foo) == 17, "Struct not packed for test" );
constexpr size_t NUM_ELEMENTS = 10;
constexpr size_t BLOCK_ALIGNMENT = 64;
PackedAlignedArray<Foo, NUM_ELEMENTS, BLOCK_ALIGNMENT> theArray;
for ( size_t i=0; i<NUM_ELEMENTS; ++i )
{
// Display the memory offset between the current
// element and the start of the array
cout << reinterpret_cast<std::ptrdiff_t>(&theArray[i]) -
reinterpret_cast<std::ptrdiff_t>(&theArray[0]) << std::endl;
}
return 0;
}
程序的输出显示了 17 字节元素在内存中地址的字节偏移量,每四个元素自动重置为 64 的倍数:
0
17
34
64
81
98
128
145
162
192