Intel与AMD x86 CPU非对齐访问性能收益差异原因问询
问题背景
我实现了一款采用结构体数组内存布局的简单线性探测哈希表,结构体存储键(key)、值(value)以及标识条目是否有效的标志位。默认情况下编译器会为该结构体添加填充字节:key和value均为64位整数,有效标志仅占1字节,因此我尝试以非对齐访问为代价对结构体进行打包,期望更高的内存密度(无需传输填充字节浪费带宽)能为打包/非对齐版本带来更好的性能。
我在Intel Xeon Gold 5220S CPU上对该哈希表进行基准测试(单线程、gcc 11.2、编译选项-O3 -march=native),发现填充版本与非对齐版本无性能差异;但在相同测试环境的AMD EPYC 7742 CPU上,非对齐版本与填充版本存在明显性能差距。
哈希表负载因子为25%和50%时,横轴为不同成功查询率(0、25、50、75、100)的测试结果如下:
可以看到,Intel平台上代表两个版本的灰、蓝(圆形、方形)曲线几乎重合,结构体打包的收益极小;但AMD平台上代表非对齐/打包结构体的曲线始终更高,即吞吐量更高。
微基准测试实现
为排查原因,我编写了一个更精简的微基准测试,剔除了哈希表的查找逻辑,仅保留随机选取数组索引后顺序访问相邻少量元素的逻辑,测试代码如下:
#include <atomic> #include <chrono> #include <cstdint> #include <iostream> #include <random> #include <vector> void ClobberMemory() { std::atomic_signal_fence(std::memory_order_acq_rel); } template <typename T> void doNotOptimize(T const& val) { asm volatile("" : : "r,m"(val) : "memory"); } struct PaddedStruct { uint64_t key; uint64_t value; bool is_valid; PaddedStruct() { reset(); } void reset() { key = uint64_t{}; value = uint64_t{}; is_valid = 0; } }; struct PackedStruct { uint64_t key; uint64_t value; uint8_t is_valid; PackedStruct() { reset(); } void reset() { key = uint64_t{}; value = uint64_t{}; is_valid = 0; } } __attribute__((__packed__)); int main() { const uint64_t size = 134217728; uint16_t repetitions = 0; uint16_t advancement = 0; std::cin >> repetitions; std::cout << "Got " << repetitions << std::endl; std::cin >> advancement; std::cout << "Got " << advancement << std::endl; std::cout << "Initializing." << std::endl; std::vector<PaddedStruct> padded(size); std::vector<PackedStruct> unaligned(size); std::vector<uint64_t> queries(size); // 初始化结构体随机值并预读内存页 std::random_device rd; std::mt19937 gen{rd()}; std::uniform_int_distribution<uint64_t> dist{0, 0xDEADBEEF}; std::uniform_int_distribution<uint64_t> dist2{0, size - advancement - 1}; for (uint64_t i = 0; i < padded.size(); ++i) { padded[i].key = dist(gen); padded[i].value = dist(gen); padded[i].is_valid = 1; } for (uint64_t i = 0; i < unaligned.size(); ++i) { unaligned[i].key = padded[i].key; unaligned[i].value = padded[i].value; unaligned[i].is_valid = 1; } for (uint64_t i = 0; i < unaligned.size(); ++i) { queries[i] = dist2(gen); } std::cout << "Running benchmark." << std::endl; ClobberMemory(); auto start_padded = std::chrono::high_resolution_clock::now(); PaddedStruct* padded_ptr = nullptr; uint64_t sum = 0; for (uint16_t j = 0; j < repetitions; j++) { for (const uint64_t& query : queries) { for (uint16_t i = 0; i < advancement; i++) { padded_ptr = &padded[query + i]; if (padded_ptr->is_valid) [[likely]] { sum += padded_ptr->value; } } doNotOptimize(sum); } } ClobberMemory(); auto end_padded = std::chrono::high_resolution_clock::now(); uint64_t padded_runtime = static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(end_padded - start_padded).count()); std::cout << "Padded Runtime (ms): " << padded_runtime << " (sum = " << sum << ")" << std::endl; // 输出sum避免被编译器优化掉 ClobberMemory(); auto start_unaligned = std::chrono::high_resolution_clock::now(); uint64_t sum2 = 0; PackedStruct* packed_ptr = nullptr; for (uint16_t j = 0; j < repetitions; j++) { for (const uint64_t& query : queries) { for (uint16_t i = 0; i < advancement; i++) { packed_ptr = &unaligned[query + i]; if (packed_ptr->is_valid) [[likely]] { sum2 += packed_ptr->value; } } doNotOptimize(sum2); } } ClobberMemory(); auto end_unaligned = std::chrono::high_resolution_clock::now(); uint64_t unaligned_runtime = static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(end_unaligned - start_unaligned).count()); std::cout << "Unaligned Runtime (ms): " << unaligned_runtime << " (sum = " << sum2 << ")" << std::endl; }
运行基准测试时设置repetitions = 3、advancement = 5,即编译运行后依次输入3(按回车)、5(按回车)即可。我对源码做了两处调整:
- 将重复次数、访问步长改为运行时输入,避免编译器因常量硬编码进行循环展开
- 改用指向vector的指针访问元素,更贴近哈希表的实际访问行为
测试结果
Intel CPU上的测试结果为:
Padded Runtime (ms): 13204
Unaligned Runtime (ms): 12185
AMD CPU上的测试结果为:
Padded Runtime (ms): 28432
Unaligned Runtime (ms): 22926
微基准测试结果和哈希表测试表现一致:Intel平台仅能从非对齐访问中获得约8%的小幅收益,但AMD平台打包版本性能提升接近24%,收益幅度远高于Intel平台。
根据公开技术讨论的结论,只要单次非对齐访问不跨单个cache line,单个成员的非对齐访问开销与对齐访问一致;有资料提到缓存取指宽度(cache fetch width)可能与cache line大小不同,但目前未找到本次测试所用两款CPU缓存取指宽度的公开参数,无法验证该因素是否与观测到的现象相关。
待解答疑问
为什么AMD CPU能从结构体打包中获得高得多的收益:如果性能提升来自内存带宽消耗降低,两款CPU理应都能观测到明显效果;如果带宽占用相近,也无法定位导致两款平台性能差异的根本原因。
内容的提问来源于stack exchange,提问作者Maxbit

