Linux C++:如何分析由于缓存未命中而浪费的时间?

ano*_*non 44 c++ linux profiling caching

我知道我可以使用gprof来对我的代码进行基准测试.

但是,我有这个问题 - 我有一个智能指针,具有额外的间接级别(将其视为代理对象).

因此,我有这个额外的层,几乎影响所有功能,并带有缓存的螺丝.

有没有办法测量我的CPU因缓存未命中而浪费的时间?

谢谢!

小智 18

你可以试试cachegrind,它是前端的kcachegrind.


Pot*_*ter 11

您可以找到一个访问CPU性能计数器的工具.每个核心中可能存在一个寄存器,其中包含L1,L2等未命中数.另外,Cachegrind执行逐周期模拟.

但是,我认为这不会有洞察力.您的代理对象可能是由他们自己的方法修改的.传统的分析器会告诉您这些方法花了多少时间.没有配置文件工具会告诉您如果没有缓存污染源,性能将如何提高.这是减少程序工作集的大小和结构的问题,这不容易推断.

我们boost::intrusive_ptr可能会对您进行快速Google搜索.它似乎不支持类似的东西weak_ptr,但转换你的程序可能是微不足道的,然后你肯定会知道非侵入式引用的成本.


And*_*ner 10

Linux支持perf从2.6.31开始.这允许您执行以下操作:

  • 使用-g编译代码以包含调试信息
  • 运行您的代码,例如使用最后一级缓存未命中计数器: perf record -e LLC-loads,LLC-load-misses yourExecutable
  • perf report
    • 确认初始消息后,选择该LLC-load-misses行,
    • 那么例如第一个功能和
    • 然后annotate.您应该看到这些行(在汇编代码中,由原始源代码包围)和一个数字,表示发生缓存未命中的行的最后一级缓存未命中的部分.


Kra*_*lew 6

继续沿着@ Mike_Dunlavey的回答:

首先,使用您最喜欢的工具获取基于时间的配置文件:VTune或PTU或OProf.

然后,获取缓存未命中配置文件.L1缓存未命中,或L2缓存未命中,或......

即第一个配置文件将"花费的时间"与每个程序计数器相关联.第二个将"缓存未命中数"值与每个程序计数器相关联.

注意:我经常"减少"数据,按功能加总,或者(如果我有技术)循环.或者说是64字节的二进制数.比较单个程序计数器通常没有用,因为性能计数器是模糊的 - 您看到报告缓存未命中的位置通常是与实际发生位置不同的几个指令.

好的,现在绘制这两个配置文件以进行比较.以下是一些我认为有用的图表:

"冰山"图表:X轴是PC,正Y轴是时间,负Y访问是缓存未命中. 寻找上下两个地方.

("交错"图表也很有用:相同的想法,X轴是PC,绘制时间和缓存错误的Y轴,但是具有不同颜色的窄垂直线,通常是红色和蓝色.放置的地方有很多时间和缓存花费的错过将有精细交错的红色和蓝色线条,几乎看起来是紫色的.这延伸到L2和L3缓存未命中,都在同一个图表上.顺便说一句,你可能想要"标准化"数字,要么是年龄的%时间或缓存未命中,或者更好的是,最大数据时间点或缓存未命中的%年龄.如果你得到错误的比例,你将看不到任何东西.)

XY图表:对于每个采样区(PC,或函数,或循环,或......),绘制一个X坐标为标准化时间的点,其Y坐标为标准化高速缓存未命中.如果您在右上角获得了大量数据点 - 大%年龄时间和大%年龄缓存未命中 - 这是有趣的证据.或者,忘记点数 - 如果上角的所有百分比总和很大......

不幸的是,请注意,您经常需要自己进行这些分析.最后我检查过VTune不会为你做这件事.我用过gnuplot和Excel.(警告:Excel死亡超过64,000个数据点.)


更多建议:

如果你的智能指针是内联的,你可能会得到所有地方的计数.在理想的世界中,您可以将PC追溯到原始的源代码行.在这种情况下,您可能希望稍微推迟减少:查看所有个人PC; 将它们映射回源代码行; 然后将它们映射到原始函数中.许多编译器(例如GCC)都有符号表选项,允许您执行此操作.

顺便说一句,我怀疑你的问题不是智能指针导致缓存颠簸.除非你在整个地方都在做smart_ptr <int>.如果你正在做smart_ptr <Obj>,并且sizeof(Obj)+大于say,4*sizeof(Obj*)(如果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>;
};
Run Code Online (Sandbox Code Playgroud)

(我不会这样编码,但它可以用于阐述.)

双重间接this-> handle-> ptr可能是一个很大的性能问题.甚至是三重间接,this-> handle-> ptr-> field.至少,在具有5个周期L1高速缓存命中的机器上,每个this-> handle-> ptr->字段将花费10个周期.并且比单指针追逐更难重叠.但是,更糟糕的是,如果每个都是L1高速缓存未命中,即使它只是到L2的20个周期......好吧,隐藏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>;
};
Run Code Online (Sandbox Code Playgroud)

无论如何 - 时间档案是你最好的朋友.


哦,是的 - 英特尔EMON硬件还可以告诉你在PC上等待的周期数.这可以区分大量的L1未命中和少量的L2未命中.


Pau*_*l R 5

这取决于您使用的操作系统和CPU.例如,对于Mac OS X和x86或ppc,Shark将执行缓存未命中分析.同上放大 Linux.


Art*_*ski 5

如果您正在运行AMD处理器,您可以获得CodeAnalyst,显然像啤酒一样免费.