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

如何通过结构体打包与对齐减少缓存命中次数?求实验测量方法

提升结构体数据局部性:缓存行优化与命中测量方法

原代码与输出

测试代码

#include <stdio.h>

struct double16{
    double v0;
    double v1;
    double v2;
    double v3;
    double v4;
    double v5;
    double v6;
    double v7;
    double v8;
    double v9;
    double v10;
    double v11;
    double v12;
    double v13;
    double v14;
    double v15;
};/* expected size is 128 bytes*/

// test - padding within a single struct,
int test_struct_padding() {
    printf("%s: %ld\n", "double16", sizeof(struct double16));
    return 0;
}

// test - address of struct,
int test_struct_address() {
    printf("%s: %ld\n", "double16", sizeof(struct double16));

    struct double16 dd;
    struct double16 ee;

    printf("address of %s: %p\n", "dd", &dd);

    // dd is only 128 bytes itself, but distance to next struct is 128 bytes(on 64 bit system)
    printf("space between %s and %s: %ld\n", "dd", "ee", (long)(&ee) - (long)(&dd));

    return 0;
}

int main(int argc, char * argv[]) {
    test_struct_padding();
    test_struct_address();

    return 0;
}

运行输出

double16: 128
double16: 128
address of dd: 0x7ff7bb3c8540
space between dd and ee: 128

已知缓存行大小为64字节(macOS可通过sysctl hw.cachelinesize命令获取),以下是优化方案与缓存命中测量方法:


一、利用打包与对齐优化数据局部性

核心思路是将频繁同时访问的结构体成员放在同一缓存行内,避免跨缓存行访问导致的额外缓存命中。

1. 调整成员布局适配访问模式

如果你的访问模式是交错访问(比如同时使用v0与v8、v1与v9等),原布局中v0-v7在第一个64字节缓存行,v8-v15在第二个,交错访问会触发两次缓存命中。此时可以重新排列成员,把成对的成员放在相邻位置:

struct double16 {
    double v0, v8;
    double v1, v9;
    double v2, v10;
    double v3, v11;
    double v4, v12;
    double v5, v13;
    double v6, v14;
    double v7, v15;
} __attribute__((aligned(64)));

调整后,每对vN与v(N+8)都在同一缓存行内,一次缓存加载即可获取两个成员的数据,减少缓存命中次数。

2. 显式缓存行对齐

使用编译器扩展(GCC/Clang支持__attribute__((aligned(64))))强制结构体起始地址对齐到64字节边界,避免结构体跨缓存行起始的情况。即使原结构体大小是128字节(64的倍数),显式对齐也能确保内存布局的稳定性,防止因编译器默认对齐规则变化导致的布局偏移。


二、实验测量缓存命中的方法

1. 使用性能计数器工具

在macOS上,可通过perf命令(需先安装)直接统计缓存命中与未命中次数:

perf stat -e cache-misses,cache-references ./your_program

输出中cache-references是总缓存访问次数,cache-misses是未命中次数,缓存命中率计算公式为:
命中率 = (cache-references - cache-misses) / cache-references * 100%

如果没有perf,也可以用Apple的instruments工具,选择"Time Profiler"或"Cache Misses"模板,可视化查看缓存行为。

2. 手动基准测试(基于访问时间)

缓存命中的访问延迟通常在1-10纳秒,未命中则在50-200纳秒,通过测量不同访问模式的耗时差异,可间接判断缓存命中情况。

示例测试代码(需关闭编译器优化,或用volatile防止优化):

#include <stdio.h>
#include <time.h>

#define ITERATIONS 100000000

struct double16 {
    double v0, v1, v2, v3, v4, v5, v6, v7,
           v8, v9, v10, v11, v12, v13, v14, v15;
} __attribute__((aligned(64)));

// 顺序访问(同一缓存行内成员优先)
double test_ordered(struct double16 *d) {
    volatile double sum = 0;
    clock_t start = clock();
    for (int i = 0; i < ITERATIONS; i++) {
        sum += d->v0 + d->v1 + d->v2 + d->v3 +
               d->v4 + d->v5 + d->v6 + d->v7 +
               d->v8 + d->v9 + d->v10 + d->v11 +
               d->v12 + d->v13 + d->v14 + d->v15;
    }
    clock_t end = clock();
    printf("顺序访问耗时: %.3f 秒\n", (double)(end - start)/CLOCKS_PER_SEC);
    return sum;
}

// 交错访问(跨原布局的缓存行)
double test_interleaved(struct double16 *d) {
    volatile double sum = 0;
    clock_t start = clock();
    for (int i = 0; i < ITERATIONS; i++) {
        sum += d->v0 + d->v8 + d->v1 + d->v9 +
               d->v2 + d->v10 + d->v3 + d->v11 +
               d->v4 + d->v12 + d->v5 + d->v13 +
               d->v6 + d->v14 + d->v7 + d->v15;
    }
    clock_t end = clock();
    printf("交错访问耗时: %.3f 秒\n", (double)(end - start)/CLOCKS_PER_SEC);
    return sum;
}

int main() {
    struct double16 d;
    test_ordered(&d);
    test_interleaved(&d);
    return 0;
}

测试注意事项:

  • 编译时关闭优化,比如GCC用-O0参数
  • 多次运行取平均值,避免系统负载干扰
  • 使用volatile修饰sum,防止编译器将访问操作优化为常量计算

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 12:51:56