【问题标题】:Linux C++: how to profile time wasted due to cache misses?Linux C++:如何分析由于缓存未命中而浪费的时间?
【发布时间】:2026-01-10 23:25:01
【问题描述】:

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

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

因此,我有这个额外的层,它影响了几乎所有的功能,并带有缓存。

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

【问题讨论】:

    标签: c++ linux caching profiling


    【解决方案1】:

    你可以试试cachegrind,它是前端kcachegrind。

    【讨论】:

      【解决方案2】:

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

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

      【讨论】:

      • 在哪里可以找到这样的事件列表?
      • perf list 打印可用事件的列表。事件列表取决于您正在运行的机器。在我登录的虚拟机上,我收到 10 个软件事件和两个“原始硬件事件描述符”事件。在我刚刚登录的物理机(至强 E5)上,我得到 26 个“硬件缓存事件”类型、10 个“硬件事件”类型、28 个“内核 PMU 事件”类型、10 个“软件事件”类型、两个“原始硬件事件”描述符”事件和一种“硬件断点”类型。
      【解决方案3】:

      您可以找到访问 CPU 性能计数器的工具。每个内核中可能有一个寄存器来计算 L1、L2 等未命中数。或者,Cachegrind 执行逐周期模拟。

      但是,我认为这并不具有洞察力。您的代理对象大概是由它们自己的方法修改的。 传统的分析器会告诉您这些方法花费了多少时间。没有任何分析工具会告诉您在没有缓存污染源的情况下性能会如何提高。这是减少程序工作集的大小和结构的问题,这不容易推断。

      Google 快速搜索出现了boost::intrusive_ptr,您可能会感兴趣。它似乎不支持weak_ptr 之类的东西,但是转换您的程序可能很简单,然后您就会确定非侵入式引用计数的成本。

      【讨论】:

      • 确实不可能将weak_ptr 与侵入计数器一起使用,因为侵入计数器会随对象一起被破坏......因此weak_ptr 无法检查对象是否在没有实际访问的情况下有效。
      • @Matthieu:如果已知依赖图是单个循环,我认为您可以使用每个对象的链接(必须只有一个)作为有效性标志。无论如何都是为了破坏。遍历随机图需要线程本地存储,但这并非不可能。
      【解决方案4】:

      继续@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 。如果你在做 smart_ptr,并且 sizeof(Obj) + 大于 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>;
      };
      

      (我不会这样编码,但它是为了展示。)

      双重间接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 未命中。

      【讨论】:

      • + 只要我们谈到这个主题,我希望 CPU 会这样做:当它进入缓存等待(L1 或 L2)时,它会记录触发它的 PC。然后,如果请求了一个中断,它应该允许它在缓存等待期间或之后得到响应,并让该信息被查询。 (如果它会导致等待重新启动,那没关系。)然后,如果软件想要获得很多这些并总结它们,它可以。就我个人而言,我建议不要使用按区域求和的部分,因为这样做只会抹掉解决问题的精确位置。
      • 不幸的是,在许多机器上,导致缓存未命中的指令的 PC(x86 领域中的 IP)不可用。停止退出等待缓存未命中返回的指令的 PC (IP) 可用,但它正在等待的 PC 不可用。我们甚至不知道停止指令正在等待什么——它所知道的只是它正在等待一个输入寄存器。 // 现在,一些较新的处理器将 PC 一直路由到内存单元,以执行 STLF 预测等工作。如果我还在英特尔,我早就这样做了。
      • AMD IBS(Instruction ased Sampling)在这里是相关的。
      • 我查了一下 - 令人印象深刻。令我惊讶的是,芯片骑师如何将他们的东西调整到更高的性能(尽管它们可能会达到极限),而软件开发人员则相反。 (当然,有趣的是,他们总是使用矩阵乘法作为标准速度测试,而真正的超大型飞机仅仅通过非真正必要的函数调用就浪费了大量时间。)
      【解决方案5】:

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

      【讨论】:

      • 不幸的是,Apple 不再支持 Shark,因此随着新的 OSX/iOS 版本的推出,它的工作时间将不再有效。 Zoom 仍在积极开发/更新中,并在几个月前发布了主要版本。
      • @federal:确实 - 很遗憾,Shark 现在陷入了被忽视的状态 - Instruments 并没有为 Shark 擅长的任务而削减它。我决定尽可能长时间地坚持使用 Snow Leopard 和 Xcode 3.2.6 - 也许一两年后 Xcode 4.x 和 Instruments 会更有用。
      • 更新:Zoom (v3.0) 现在在 Mac OS X 和 Linux 上配置文件。鲨鱼正式消失了。
      • Instruments 也有所改进,但我认为它仍然没有进行缓存未命中分析。
      【解决方案6】:

      如果您运行的是 AMD 处理器,您可以获得CodeAnalyst,显然是免费的,就像啤酒一样。

      【讨论】:

        【解决方案7】:

        我的建议是使用英特尔的PTU(性能调整实用程序)。

        此实用程序是 VTune 的直接后代,并提供可用的最佳采样分析器。您将能够跟踪 CPU 在哪里花费或浪费时间(在可用硬件事件的帮助下),这不会降低您的应用程序速度或扰乱配置文件。 当然,您将能够收集您正在寻找的所有缓存行未命中事件。

        【讨论】:

        • 问题是,缓存污染会导致到处都是miss。有什么模式可以寻找?
        • 您需要找出的第一件事是:您的特定应用程序中是否真的存在问题。像用户使用它一样分析您的应用程序,然后检查您的瓶颈所在的报告。您可能会发现大量的 L2 Cache Line Miss,但这可能是由您的应用程序的其他部分引起的,并发现了您不担心的其他问题。这并不意味着你的智能指针没有问题,但它隐藏在更紧迫的瓶颈后面。告诉我进展如何,我很乐意为任何性能问题提供帮助。
        • @Potatoswatter - 多年后才注意到评论。 WRT模式,我寻找频率模式。例如。如果您有数据地址,则按 L1/L2/TLB 中的关联集进行直方图。尝试傅立叶变换来寻找频率。这些是空间的 - 如果你有时间错过,你可以寻找时间和空间频率。
        【解决方案8】:

        另一个基于 CPU 性能计数器的分析工具是 oprofile。您可以使用 kcachegrind 查看其结果。

        【讨论】:

          【解决方案9】:

          这是general answer

          例如,如果您的程序有 50% 的时间用于缓存未命中,那么当您暂停它时,有 50% 的时间程序计数器将位于它正在等待内存获取的确切位置导致缓存未命中。

          【讨论】:

          • 但是,将 50% 的时间用于程序计数器在退休时停止指向缓存未命中与说程序将 50% 的时间用于缓存未命中或者说如果删除所有缓存未命中,该程序的速度将加倍。在推测性机器中,缓存未命中可能不会停止引退,但可能会停止另一条本身停止引退的指令。
          • @KrazyGlew:我相信你是对的,芯片架构师在他们对速度的值得称赞的追求中,使得追踪哪些代码让他们头疼并不容易 :)
          • @Mike_Dunlavey:过失。作为 CPU 架构师,以及作为负责使分析缓存未命中(EMON 分析)成为可能的 CPU 架构师。但是,我也是一名性能分析师,在成为 CPU 架构师之前是一名性能分析师。如果我知道一种廉价的方法来准确测量缓存未命中损失了多少时间,我会构建它。与此同时,我能想到的最好的办法就是描述正在测量的内容。
          • @KrazyGlew:上次我直接与芯片级人员 (BCE) 合作时,他们想帮助我解决某种测量计时器问题。我说谢谢,但不谢谢。测量并没有告诉我我需要知道什么。我需要知道的是,在我选择的随机时间,它现在在等待什么 (WIIWFRN :) 看到区别了吗?如果我在 10 次随机时间问它这个问题,并且它在其中 3 次告诉我它由于 IP=foo 而处于缓存等待状态,我确切地知道需要修复哪些代码才能获得 30%(大约)。那时,一个 ICE 就足够了。