You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Intel与AMD x86 CPU非对齐访问性能收益差异原因问询

结构体打包哈希表的跨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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 03:51:28