3. 循环分块
任务目标
本案例针对大型数组的连续遍历场景,通过循环分块(Loop Tiling)技术将大跨度访存拆分为适配缓存容量的小块处理,减少cache miss,在几乎不增加额外空间的前提下提升时间效率。核心是理解分块大小对缓存命中率的影响,并掌握可移植的分块实现方法。
核心原理与实现
假设数组规模远大于L2缓存容量,直接顺序遍历时,每访问一个新元素都可能触发cache line替换。循环分块将外层大循环切分为BLOCK大小的内层子块,使子块在较短时间内重复访问的数据能驻留在缓存中。本案例以矩阵逐元素平方累加为例(访存模式与图像处理、数值计算中的stencil操作一致),给出可直接编译运行的最小实现。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define N 10000 // 数组长度(约80KB,超过L1但小于L2)
#define BLOCK 256 // 分块大小,需根据目标平台缓存调整
// 非分块版本:顺序遍历整个数组
double sum_square_naive(const double *arr, int n) {
double sum = 0.0;
for (int i = 0; i < n; i++) {
sum += arr[i] * arr[i];
}
return sum;
}
// 循环分块版本:按BLOCK大小分块处理
double sum_square_tiled(const double *arr, int n) {
double sum = 0.0;
for (int i = 0; i < n; i += BLOCK) {
int end = (i + BLOCK < n) ? i + BLOCK : n;
for (int j = i; j < end; j++) {
sum += arr[j] * arr[j];
}
}
return sum;
}
int main() {
// 初始化数组(使用确定性数据保证可验证)
double *arr = (double*)malloc(N * sizeof(double));
for (int i = 0; i < N; i++) {
arr[i] = (double)(i % 100) * 0.5; // 值范围[0, 49.5]
}
// 预热并计时(clock()精度足够区分差异)
volatile double r1 = sum_square_naive(arr, N);
volatile double r2 = sum_square_tiled(arr, N);
clock_t t0 = clock();
double s1 = sum_square_naive(arr, N);
clock_t t1 = clock();
double s2 = sum_square_tiled(arr, N);
clock_t t2 = clock();
double time_naive = (double)(t1 - t0) / CLOCKS_PER_SEC;
double time_tiled = (double)(t2 - t1) / CLOCKS_PER_SEC;
// 验证结果一致性(必须完全相等)
printf("naive sum = %.6f, tiled sum = %.6f\n", s1, s2);
printf("naive time = %.6f s, tiled time = %.6f s\n", time_naive, time_tiled);
printf("speedup = %.2fx\n", time_naive / time_tiled);
free(arr);
return 0;
}
编译与运行
gcc -O2 -o loop_tiling loop_tiling.c
./loop_tiling
验证方法与判定标准
- 正确性:程序输出的两个sum值必须完全一致(误差为0),否则说明分块逻辑破坏了累加顺序导致浮点差异——本案例中累加顺序未改变,因此应严格相等。
- 性能观察:在N=10000、BLOCK=256时,tiled版本通常比naive快1.1~1.5倍(具体取决于CPU缓存架构)。若速度反而更慢,说明BLOCK选择不当(过大或过小),可尝试BLOCK=64/128/512对比。
- 可复现性:多次运行,speedup波动应在±10%以内(受系统其他进程影响)。
关键参数调优建议
BLOCK的理想值是使子块数据量(BLOCK×8字节)不超过L1缓存的一半。例如L1为32KB时,BLOCK≤2048;L1为64KB时,BLOCK≤4096。实际最优值需在目标硬件上扫描测试。若数组为多维(如矩阵),分块需同时作用于多个维度,原理与本例一致。
上图对比了两种访存模式:非分块版本每处理一个cache line后立即丢弃,分块版本则让块内数据在缓存中保持活跃,减少重复从主存加载的开销。实际加速比受数组大小、缓存层级和BLOCK取值共同影响,建议在目标硬件上以本代码为基准扫描BLOCK参数。