std vector数据加载累加时缓存缺失及perf相关技术咨询
缓存缺失相关问题解答
问题背景
Perf报告显示,一个从磁盘加载二进制Blob并累加元素的简单C++应用存在约3×10⁷次缓存缺失。程序代码如下:
#include <fstream> #include <iostream> #include <numeric> #include <vector> std::vector<float> read_blob(const std::string& file_path, int sz) { std::vector<float> data(sz); std::fstream file(file_path, std::ios::binary | std::ios::in); if (!file.is_open()) { throw std::runtime_error{"Cannot open buffer from disk"}; } file.read((char*)data.data(), sizeof(float) * sz); file.close(); return data; } int main() { constexpr size_t sz{311040000}; const std::string path{"/tmp/blob.bin"}; std::vector<float> data = read_blob(path, sz); float sum = std::accumulate(data.begin(), data.end(), 0.0F) / (float)data.size(); std::cout << "the sum is " << sum << std::endl; }
程序使用-O3 -march=native编译。进一步分析发现缓存缺失的分布如下:
| 模块 | 缓存缺失数量 |
|---|---|
| brnf_frag_data_storage ([br_netfilter]) | 2.058×10⁷ (60%) |
| Main | 1.325×10⁷ (40%) |
| Total | 3.838×10⁷ |
1. main函数执行前发生了什么?
- 程序启动阶段,操作系统会完成进程初始化流程:将可执行文件加载到内存、解析动态链接库(若为动态编译)、初始化进程环境变量、分配栈空间等。
- 统计到的
brnf_frag_data_storage属于内核br_netfilter(网桥网络过滤器)模块,这部分缓存缺失和用户态程序无关,是内核处理网络分片存储时产生的——大概率是系统中其他网络活动触发了该模块操作,恰好被perf统计到(perf默认会统计进程全生命周期内的所有事件,包括内核态相关操作)。 - 此外,C++全局对象的构造、标准库的初始化(比如
std::cout的初始化)也会在main执行前完成,但这部分产生的缓存缺失量级远低于内核模块的统计结果。
2. 能否配置perf仅统计main函数启动后的事件?
可以,有几种可行方案:
- 符号过滤:使用
perf record -e cache-misses --filter 'filter main:*' ./your_program,直接指定只统计main函数及后续调用栈的事件(需确保perf支持符号过滤)。 - 手动标记起点:在
main开头插入空函数(如void perf_mark() {}),编译后用perf record -e cache-misses -g ./your_program,后续在perf report中仅分析perf_mark之后的调用栈事件。 - 忽略内核态事件:如果只关心用户态缓存缺失,可执行
perf record -e cache-misses --user-only ./your_program,这样会排除内核模块(如br_netfilter)产生的事件。 - 探针触发统计:用
perf probe在main入口创建探针,再统计探针触发后的事件:perf probe -x ./your_program main perf record -e cache-misses -e probe:main --filter 'probe:main' ./your_program
3. 鉴于vector长度,每约31个元素就出现一次缓存缺失,为何频率如此之高?
先明确数据规模:vector包含311040000个float元素,每个float占4字节,总大小为311040000 * 4 = 1.244GB,远超过常见CPU的L3缓存容量(通常为几MB到几十MB),核心原因如下:
- 缓存层级的累计缺失:perf统计的是L1、L2、L3所有层级的缓存缺失总和,并非仅L1缺失。比如数据从L2加载到L1失败、从L3加载到L2失败都会被计入,累加后会让平均缺失间隔变小。
- 数据加载与遍历的时序冲突:磁盘读取数据到内存时是按页(通常4KB)加载,理论上每1024个
float元素触发一次页缺失,但std::accumulate可能在数据刚写入内存就开始遍历,CPU预取器还未将后续数据加载到缓存,导致频繁缺失。 - 内存压力导致缓存淘汰:如果系统内存紧张,刚加载到缓存的页可能被其他进程或内核操作挤出缓存,触发二次缺失,进一步提升缺失频率。
内容的提问来源于stack exchange,提问作者fabian
相关产品推荐
相关产品推荐

