4. 循环交换(Loop Interchange)
任务目标
本案例通过一个矩阵乘法核心片段,演示循环交换(Loop Interchange)如何将内层循环的访存模式从“按列跳跃”改为“按行连续”,从而提升Cache命中率。你将看到同一逻辑的两种循环顺序,并验证它们在访存连续性上的差异。
核心原理与实现
矩阵按行存储时,A[i][k]的地址随k递增而连续。若内层循环遍历k,则每次迭代访问相邻内存地址,Cache行利用率高;反之若内层循环遍历i,则每次跳过一个完整行,造成Cache行浪费。以下代码提供两种版本:bad_access(内层循环跳行)与good_access(内层循环连续),两者计算完全相同的矩阵乘法结果,仅循环顺序不同。
完整代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define N 512
// 内层循环遍历 i(跳行访问,Cache不友好)
void bad_access(double A[N][N], double B[N][N], double C[N][N]) {
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++) {
double sum = 0.0;
for (int k = 0; k < N; k++)
sum += A[i][k] * B[k][j]; // B按列访问,跳行
C[i][j] = sum;
}
}
// 内层循环遍历 k(连续访问,Cache友好)
void good_access(double A[N][N], double B[N][N], double C[N][N]) {
for (int i = 0; i < N; i++)
for (int k = 0; k < N; k++) {
double r = A[i][k];
for (int j = 0; j < N; j++)
C[i][j] += r * B[k][j]; // B按行连续访问
}
}
// 初始化矩阵(确定性填充,便于验证)
void init(double A[N][N], double B[N][N], double C[N][N]) {
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++) {
A[i][j] = (i + j) % 100;
B[i][j] = (i * j) % 100;
C[i][j] = 0.0;
}
}
// 验证两个版本结果一致
int verify(double C1[N][N], double C2[N][N]) {
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
if (C1[i][j] != C2[i][j]) return 0;
return 1;
}
int main() {
static double A[N][N], B[N][N], C_bad[N][N], C_good[N][N];
init(A, B, C_bad);
init(A, B, C_good);
clock_t t1 = clock();
bad_access(A, B, C_bad);
clock_t t2 = clock();
good_access(A, B, C_good);
clock_t t3 = clock();
printf("bad_access 耗时: %.3f 秒\n", (double)(t2 - t1) / CLOCKS_PER_SEC);
printf("good_access 耗时: %.3f 秒\n", (double)(t3 - t2) / CLOCKS_PER_SEC);
printf("结果一致: %s\n", verify(C_bad, C_good) ? "是" : "否");
return 0;
}
运行与验证
- 编译:
gcc -O2 -o loop_interchange loop_interchange.c(-O2为必要优化,否则差异不明显) - 运行:
./loop_interchange - 通过条件:输出中“结果一致: 是”,且
good_access耗时明显低于bad_access(通常差距在2倍以上,具体数值取决于CPU与Cache大小)。
预期输出示例
bad_access 耗时: 0.312 秒
good_access 耗时: 0.098 秒
结果一致: 是
以上耗时仅为逻辑一致的示例,实际数值随硬件不同而变化;关键在于good_access显著更快且结果一致。
关键说明
- 本案例使用
static数组避免栈溢出,N=512时两个版本各需约2MB数据,适合L2/L3 Cache演示。 - 若你的环境未显示明显差异,可增大N至1024(内存约8MB)或改用
-O3编译。 - 循环交换的核心价值在于将内层循环映射到内存连续方向,本案例中
good_access对B矩阵的访问从“列跳跃”变为“行连续”,这正是Cache友好的关键。