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

-O0编译的朴素DGEMM比-O1更快?求性能异常原因与排查方向

朴素DGEMM在-O1下性能劣于-O0的问题分析

测试代码

朴素DGEMM实现

void dgemm_naive(const int M, const int N, const int K, const double alpha,
                 const double *A, const int lda, const double *B, const int ldb,
                 const double beta, double *C, const int ldc) {
  for (int i = 0; i < M; ++i) {
    for (int j = 0; j < N; ++j) {
      double cij = beta * C[ldc * i + j];
      double abij{0.0};

      for (int k = 0; k < K; ++k) {
        abij += A[lda * i + k] * B[ldb * k + j];
      }

      C[ldc * i + j] = cij + alpha * abij;
    }
  }
}

测试框架与计时代码

#include <chrono>
#include <cstdio>
#include <new>

void dgemm_naive(const int M, const int N, const int K, const double alpha,
                 const double *A, const int lda, const double *B, const int ldb,
                 const double beta, double *C, const int ldc);

template <typename T> inline T min(T x, T y) {
  return (((x) < (y)) ? (x) : (y));
}


void run_test(const int m, const int n, const int k, double alpha,
                 double beta,
                 void (*dgemm)(const int, const int, const int, const double,
                               const double *, const int, const double *,
                               const int, const double, double *, const int),
                 const int niter) {

  double *A = new (std::nothrow) double[m * k];
  double *B = new (std::nothrow) double[k * n];
  double *C = new (std::nothrow) double[m * n];

  if (A == nullptr || B == nullptr ||
      C == nullptr) // handle case where new returned null
  {
    // Do error handling here
    printf("Could not allocate memory\n");
  }

  for (int i = 0; i < (m * k); i++) {
    A[i] = i + 1;
  }

  for (int i = 0; i < (k * n); i++) {
    B[i] = -i - 1;
  }

  for (int i = 0; i < (m * n); i++) {
    C[i] = 0.0;
  }

  auto t1 = std::chrono::high_resolution_clock::now();
  for (int i = 0; i < niter; i++) {
    dgemm(m, n, k, alpha, A, k, B, n, beta, C, n);
  }
  auto t2 = std::chrono::high_resolution_clock::now();

  auto ms_int = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1);

  // // To make sure computations don't get optimized out. 
  // printf("C[0, 0] = %f\n", C[0]);
  // printf("C[M-1, N-1] = %f\n", C[(m-1)*n+(n-1)]);
  
  delete[] A;
  delete[] B;
  delete[] C;

  printf("Time: %ld ms\n", static_cast<long>(ms_int.count()));

}

int main() {
  int m = 1024, k = 1024, n = 1024;
  double alpha = 1.0, beta = 0.0;
  run_test(m, n, k, alpha, beta, dgemm_naive, 1);
  return 0;
}

测试结果

在AMD Ryzen 7 3800XT上使用clang 18.1.3或gcc 13.3.0编译,得到如下计时结果:

-O0 Time: 30120 ms
-O1 Time: 54488 ms

Intel i7-8550U笔记本上也存在类似相对差异(整体速度更慢)。

已完成的分析

通过cachegrind和perf分析发现,-O1编译版本存在缓存缺失问题。查看汇编与LLVM IR可知,循环顺序未改变,仅将循环不变的索引计算移到了循环外,仅保留对C、A、B的三次读取。

待解答的问题

  1. 我的假设是否正确:缓存缺失一定源于内存加载顺序的改变?(已验证内存对齐无问题)
  2. 下一步的排查方向是什么?

问题1解答

你的假设不正确。缓存缺失的诱因不止内存加载顺序一种,即使加载顺序不变,编译器优化也可能通过以下方式影响缓存行为:

  • 指令调度与内存访问时机冲突:-O1会进行指令重排,可能让CPU在等待某条内存加载完成时提前发起其他内存请求,打乱缓存行的预取模式;
  • 循环展开降低缓存复用:-O1可能对循环做部分展开,导致单次循环内访问的内存地址范围变大,超出缓存行覆盖范围,降低数据复用率;
  • 寄存器分配间接影响内存访问:-O0会把更多变量存在栈上,而-O1优先用寄存器保存中间值,这可能改变内存访问的时机和批量性,影响缓存命中效率。

问题2下一步排查方向

  • 细粒度缓存事件统计:用perf stat -e L1-dcache-loads,L1-dcache-load-misses,L2-dcache-load-misses,L3-dcache-load-misses,dTLB-load-misses对比-O0和-O1各层级缓存及TLB的缺失率,定位问题发生的层级;
  • 检查循环展开与向量化:查看编译后的汇编代码,确认-O1是否开启了循环展开或自动向量化(即使未指定-O3),这类优化会改变内存访问的步长和批量模式;
  • 量化数据复用率:手动计算两种编译版本下矩阵B的列访问(代码中B为按列访问,本身效率较低)在缓存中的复用次数,验证-O1是否降低了复用率;
  • 逐步关闭-O1优化项:尝试添加-fno-inline、-fno-loop-unroll、-fno-schedule-insns等选项,逐一关闭-O1的优化特性,定位是哪项优化导致性能退化;
  • 监测内存带宽利用率:用工具对比两种版本的内存带宽占用情况,确认-O1是否因内存访问模式更零散导致带宽利用率下降,进而拖慢整体速度。

内容的提问来源于stack exchange,提问作者ligro

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 05:15:04