我正在尝试使用Google性能工具CPU Profiler调试多线程程序上的性能问题。使用单线程需要250毫秒,而4个线程需要900毫秒。

我的程序有一个mmap文件,该文件在线程之间共享,所有操作都是只读的。另外,我的程序还会创建大量不跨线程共享的对象。 (特别是我的程序使用CRF ++库进行一些查询)。我试图弄清楚如何使我的程序在多线程中表现更好。 gperf工具的CPU分析器生成的调用图显示,我的程序在_L_unlock_16中花费了大量时间(大约50%)。

在网络上搜索_L_unlock_16时,发现了一些错误报告,这些错误报告具有规范性,暗示其与libpthread相关。但是除此之外,我无法找到任何有用的调试信息。

我的程序的简要说明。我的文件里只有几句话(4)。在我的程序中,我有一个processWord(),它使用CRF ++处理单个单词。这个processWord()是每个线程执行的内容。我的main()从文件中读取单词,每个线程并行运行processWord()。如果我处理一个字(因此只有1个线程)需要250毫秒,所以如果我处理所有4个字(因此要处理4个线程),我希望它可以在250毫秒内完成,但是如上所述,这需要900毫秒。
这是执行的调用图-https://www.dropbox.com/s/o1mkh477i7e9s4m/cgout_n2.png

我想了解为什么我的程序在_L_unlock_16上花费大量时间,以及如何减轻它的影响。

最佳答案

同样,_L_unlock_16不是您的代码的函数。您是否看过该函数上方的stracktrace?程序等待时,它的调用者是什么?您已经说过该程序浪费了50%的内部等待时间。但是,程序的哪一部分命令了该操作?还是从内存alloc / dealloc操作中获得的吗?

该函数似乎来自libpthread。 CRF +是否以任何方式处理线程/ libpthread?如果是,则库配置错误?也许它通过在各处添加锁来实现某些“基本线程安全性”,而对于多线程而言,它根本不是很好的构建方式?文档对此有何评论?

就个人而言,我猜想它会忽略线程,而您已经添加了所有线程。我可能是错的,但是如果这是真的,那么CRF ++可能根本不会调用该“解锁”功能,并且如何从协调线程/锁/队列/消息等的代码中调用“解锁”?暂停几次该程序,然后查看谁调用了解锁。如果确实花了50%的时间坐在解锁中,您将很快知道是谁导致使用该锁,并且您将能够消除它或至少进行更精细的研究。

编辑#1:

恩..当我说“ stacktrace”时,我的意思是stacktrace,而不是callgraph。在微不足道的情况下,Callgraph可能看起来不错,但在更复杂的情况下,Callgraph会被打乱且难以阅读,并将珍贵的细节隐藏为“紧凑的”形式。但是,幸运的是,这里的情况看起来足够简单。

请注意开头:“过程字,99x”。我认为“ 99x”是通话次数。然后,查看“ tagger-parse”:97x。从那:


61x进入rebuildFeatures,其中41x直接进入解锁,而20(13)间接进入
23x进入buildLattice,其中21x进入解锁


我猜想这是CRF ++大量使用锁定的原因。对我来说,您似乎只是观察CRF内部锁定的效果。内部肯定不是无锁的。

似乎每个“ processWord”至少锁定一次。不查看代码就很难说(它是开源的吗?我还没有检查过..),从堆栈跟踪中可以明显看出来,但是如果它确实对每个“ processWord”锁定一次,那么它甚至可能是一种“全局锁”可保护“所有内容”免受“所有线程”的侵扰,并使所有作业序列化。随你。无论如何,显然,锁定并等待的是CRF ++的内部。

如果您的CRF对象实际上(确实)未在线程之间共享,则从CRF中删除线程配置标志,祈祷它们足够理智,不使用任何静态变量或全局对象,并在最顶层的工作中添加一些自己的锁定(如果需要) /结果级别,然后重试。您现在应该拥有更快的速度。

如果共享了CRF对象,请取消共享它们,然后参见上文。

但是,如果在后台共享它们,那么几乎没有什么可行的。将您的库更改为具有更好的线程支持的库,或者修复库,或者忽略它并以当前性能使用它。

最后一个建议听起来很奇怪(它运行缓慢,对吗?为什么要忽略它?),但实际上是最重要的建议,您应该首先尝试。如果并行任务具有类似的“数据配置文件”,则很有可能它们将尝试在大约相同的时间点击中相同的锁。想象一下一个中等大小的缓存,其中包含按其首字母排序的单词。在顶层,有26个条目。每个条目都有一个锁和一个单词列表。如果您运行100个线程,每个线程首先检查“ mom”,然后依次检查“ dad”和“ son”,则所有这100个线程将首先命中并在“ M”处等待,然后在“ D”处等待,然后在“ S”处等待”。好吧,大约/大概是这样。但是你明白了。如果数据配置文件更加随机,则它们相互阻塞的可能性会大大降低。请注意,处理一个单词是一个..小任务,您尝试处理相同的单词。即使内部CRF的锁定很灵巧,也一定会碰到相同的区域。使用更分散的数据再试一次。

再加上线程成本。如果有人通过使用锁来防止比赛,那么每次锁/解锁都要付出代价,因为至少他们必须“停止并检查锁是否打开”(对不起,措辞不准确)。如果要处理的数据相对于锁检查量较小,则添加更多线程将无济于事,只会浪费时间。为了检查一个单词,甚至可能只对单个锁进行处理要比对单词进行处理花费更多的时间!但是,如果要处理的数据量较大,则与处理数据相比,翻转锁的成本可能开始变得可忽略。

准备一组100个或更多的单词。在一个线程上运行并测量它。然后随机划分单词,并在2和4个线程上运行它。并测量。如果效果更好,请尝试输入1000和10000个字。当然,越多越好,请记住,测试不应持续到下一个生日;)

如果您发现将4万个线程(每秒钟2500w)上的1万个单词的工作速度比一个线程快40%-30%-甚至快25%-那就开始吧!您只是给它做了太小的工作。它是为较大的产品量身定制和优化的!

但是,另一方面,可能发生的情况是,将4万个线程拆分成10k个单词不能更快地工作,或者更糟的是,它工作得更慢-然后可能表明该库处理多线程非常错误。现在尝试其他操作,例如从中剥离线程或对其进行修复。

10-04 12:53