【发布时间】:2026-01-10 23:25:01
【问题描述】:
我知道我可以使用 gprof 对我的代码进行基准测试。
但是,我遇到了这个问题——我有一个智能指针,它具有额外的间接级别(将其视为代理对象)。
因此,我有这个额外的层,它影响了几乎所有的功能,并带有缓存。
有没有办法测量我的 CPU 因缓存未命中而浪费的时间?
【问题讨论】:
标签: c++ linux caching profiling
我知道我可以使用 gprof 对我的代码进行基准测试。
但是,我遇到了这个问题——我有一个智能指针,它具有额外的间接级别(将其视为代理对象)。
因此,我有这个额外的层,它影响了几乎所有的功能,并带有缓存。
有没有办法测量我的 CPU 因缓存未命中而浪费的时间?
【问题讨论】:
标签: c++ linux caching profiling
你可以试试cachegrind,它是前端kcachegrind。
【讨论】:
Linux 从 2.6.31 开始支持 perf。这允许您执行以下操作:
perf record -e LLC-loads,LLC-load-misses yourExecutable
perf report
LLC-load-misses 行,annotate。您应该看到行(在汇编代码中,由原始源代码包围)和一个数字,指示发生缓存未命中的行的最后一级缓存未命中的比例。【讨论】:
perf list 打印可用事件的列表。事件列表取决于您正在运行的机器。在我登录的虚拟机上,我收到 10 个软件事件和两个“原始硬件事件描述符”事件。在我刚刚登录的物理机(至强 E5)上,我得到 26 个“硬件缓存事件”类型、10 个“硬件事件”类型、28 个“内核 PMU 事件”类型、10 个“软件事件”类型、两个“原始硬件事件”描述符”事件和一种“硬件断点”类型。
您可以找到访问 CPU 性能计数器的工具。每个内核中可能有一个寄存器来计算 L1、L2 等未命中数。或者,Cachegrind 执行逐周期模拟。
但是,我认为这并不具有洞察力。您的代理对象大概是由它们自己的方法修改的。 传统的分析器会告诉您这些方法花费了多少时间。没有任何分析工具会告诉您在没有缓存污染源的情况下性能会如何提高。这是减少程序工作集的大小和结构的问题,这不容易推断。
Google 快速搜索出现了boost::intrusive_ptr,您可能会感兴趣。它似乎不支持weak_ptr 之类的东西,但是转换您的程序可能很简单,然后您就会确定非侵入式引用计数的成本。
【讨论】:
weak_ptr 与侵入计数器一起使用,因为侵入计数器会随对象一起被破坏......因此weak_ptr 无法检查对象是否在没有实际访问的情况下有效。
继续@Mike_Dunlavey 的回答:
首先,使用您最喜欢的工具获取基于时间的配置文件:VTune 或 PTU 或 OProf。
然后,获取缓存未命中配置文件。 L1 缓存未命中,或 L2 缓存未命中,或 ...
即第一个配置文件将“花费的时间”与每个程序计数器相关联。 第二个将“缓存未命中数”值与每个程序计数器相关联。
注意:我经常“减少”数据,按功能或(如果我有技术)按循环汇总。或者按 64 字节的箱。比较单个程序计数器通常没有用,因为性能计数器是模糊的 - 您看到报告缓存未命中的地方通常与实际发生的地方有几条不同的指令。
好的,现在绘制这两个配置文件以比较它们。以下是一些我认为有用的图表:
“冰山”图表:X 轴是 PC,正 Y 轴是时间,负 Y 访问是缓存未命中。 寻找上下波动的地方。
(“交错”图表也很有用:同样的想法,X 轴是 PC,绘制时间和缓存未命中 Y 轴,但有不同颜色的窄垂直线,通常是红色和蓝色。两者都有很多的地方花费的时间和缓存未命中将有精细交错的红线和蓝线,几乎看起来是紫色的。这延伸到 L2 和 L3 缓存未命中,都在同一张图上。顺便说一句,您可能希望将数字“标准化”,或者到 %总时间或缓存未命中的年龄,或者更好的是,最大数据时间点或缓存未命中的百分比。如果比例错误,您将看不到任何内容。)
XY 图表:为每个采样箱(PC,或函数,或循环,或...)绘制一个点,其 X 坐标为归一化时间,其 Y 坐标为标准化缓存未命中。如果您在右上角获得大量数据点 - 大 %age 时间和大 %age 缓存未命中 - 这是有趣的证据。或者,忘记点数 - 如果上角所有百分比的总和很大......
不幸的是,请注意,您经常必须自己进行这些分析。最后我检查了 VTune 不会为你做这件事。我用过 gnuplot 和 Excel。 (警告:Excel 在超过 64000 个数据点时死亡。)
更多建议:
如果您的智能指针是内联的,您可能会得到所有的计数。在理想情况下,您将能够将 PC 追溯到源代码的原始行。在这种情况下,您可能需要稍微推迟减少:查看所有单独的 PC;将它们映射回源代码行;然后将它们映射到原始函数中。许多编译器,例如GCC,有允许你这样做的符号表选项。
顺便说一句,我怀疑您的问题不在于智能指针导致缓存抖动。除非你到处都在做 smart_ptr
更有可能是智能指针执行的额外间接级别导致了你的问题。
巧合的是,我在午餐时与一个人交谈,他有一个使用句柄的引用计数智能指针,即间接级别,类似于
template<typename T> class refcntptr {
refcnt_handle<T> handle;
public:
refcntptr(T*obj) {
this->handle = new refcnt_handle<T>();
this->handle->ptr = obj;
this->handle->count = 1;
}
};
template<typename T> class refcnt_handle {
T* ptr;
int count;
friend refcnt_ptr<T>;
};
(我不会这样编码,但它是为了展示。)
双重间接this->handle->ptr可能是一个很大的性能问题。甚至是三重间接,this->handle->ptr->field。至少,在一台有 5 个周期的 L1 缓存命中的机器上,每个 this->handle->ptr->field 需要 10 个周期。并且比单指针追逐更难重叠。但是,更糟糕的是,如果每个都是 L1 缓存未命中,即使它只有 20 个周期到 L2……嗯,隐藏 2*20=40 个缓存未命中延迟周期比单个 L1 未命中要困难得多。
一般来说,避免智能指针中的间接级别是一个很好的建议。不是指向所有智能指针指向的句柄,而句柄本身指向对象,您可以通过让智能指针指向对象和句柄来使智能指针更大。 (这不再是通常所说的句柄,而更像是一个信息对象。)
例如
template<typename T> class refcntptr {
refcnt_info<T> info;
T* ptr;
public:
refcntptr(T*obj) {
this->ptr = obj;
this->info = new refcnt_handle<T>();
this->info->count = 1;
}
};
template<typename T> class refcnt_info {
T* ptr; // perhaps not necessary, but useful.
int count;
friend refcnt_ptr<T>;
};
无论如何 - 时间档案是你最好的朋友。
哦,是的 - 英特尔 EMON 硬件还可以告诉您在 PC 上等待了多少个周期。这可以区分大量的 L1 未命中和少量的 L2 未命中。
【讨论】:
【讨论】:
如果您运行的是 AMD 处理器,您可以获得CodeAnalyst,显然是免费的,就像啤酒一样。
【讨论】:
我的建议是使用英特尔的PTU(性能调整实用程序)。
此实用程序是 VTune 的直接后代,并提供可用的最佳采样分析器。您将能够跟踪 CPU 在哪里花费或浪费时间(在可用硬件事件的帮助下),这不会降低您的应用程序速度或扰乱配置文件。 当然,您将能够收集您正在寻找的所有缓存行未命中事件。
【讨论】:
另一个基于 CPU 性能计数器的分析工具是 oprofile。您可以使用 kcachegrind 查看其结果。
【讨论】:
例如,如果您的程序有 50% 的时间用于缓存未命中,那么当您暂停它时,有 50% 的时间程序计数器将位于它正在等待内存获取的确切位置导致缓存未命中。
【讨论】: