_L_unlock_16再次不是您的代码的功能。你看过那个函数上面的stracktraces吗?程序等待时它的调用者是什么?您说过该程序浪费了 50% 的等待时间。但是,程序的哪一部分命令了该操作?又是来自内存分配/释放操作吗?
该函数似乎来自 libpthread。CRF+ 是否以任何方式处理线程/libpthread?如果是,那么库可能配置错误?或者它可能通过在任何地方添加锁来实现一些“基本线程安全”,并且根本不适合多线程?文档对此有何评论?
就个人而言,我猜它会忽略线程并且您已经添加了所有线程。我可能是错的,但如果这是真的,那么 CRF++ 可能根本不会调用那个“解锁”函数,而“解锁”是从你的协调线程/锁/队列/消息等的代码中调用的?暂停程序几次,看看是谁调用了解锁。如果它真的花费 50% 的时间在解锁上,你会很快知道是谁导致了这个锁被使用,你将能够消除它,或者至少进行更精细的研究。
编辑#1:
嗯..当我说“堆栈跟踪”时,我的意思是堆栈跟踪,而不是调用图。Callgraph 在微不足道的情况下可能看起来不错,但在更复杂的情况下,它会被破坏和不可读,并且会将宝贵的细节隐藏为“压缩”形式。但是,幸运的是,这里的情况看起来很简单。
请注意开头:“Process word, 99x”。我假设“99x”是通话次数。然后,查看“tagger-parse”:97x。从此:
- 61x 进入重建功能,其中 41x 直接进入解锁,20(13) 间接进入
- 23x 用于 buildLattice,21x 用于解锁
我猜这是 CRF++ 大量使用锁定。对我来说,您似乎只是观察了 CRF 内部锁定的效果。它当然不是内部无锁的。
每个“processWord”似乎至少锁定一次。不看代码很难说(它是开源的吗?我还没有检查过..),从堆栈跟踪来看会更明显,但如果它真的每个“processWord”锁定一次,它甚至可能是一种“全局锁”,它保护“一切”不受“所有线程”的影响,并导致所有作业序列化。任何。无论如何,很明显,锁定和等待的是 CRF++ 的内部结构。
如果你的 CRF 对象真的(真的)没有跨线程共享,那么从 CRF 中删除线程配置标志,祈祷它们足够理智,不使用任何静态变量或全局对象,在最顶层的工作中添加一些自己的锁定(如果需要) /result 级别并重试。你现在应该更快地拥有它。
如果 CRF 对象是共享的,请取消共享它们并参见上文。
但是,如果它们是在幕后共享的,那么就没有什么可行的了。将您的库更改为具有更好线程支持的库,或者修复库,或者忽略并以当前性能使用它。
最后一个建议可能听起来很奇怪(它工作很慢,对吧?那为什么要忽略它?),但实际上是最重要的,你应该先尝试一下。如果并行任务具有相似的“数据配置文件”,那么它们很可能会在相同的近似时间尝试命中相同的锁。想象一个中等大小的缓存,它保存按首字母排序的单词。在顶层有一个数组,比如说,26 个条目。每个条目都有一个锁和里面的单词列表。如果您运行 100 个线程,每个线程将首先检查“妈妈”,然后是“爸爸”,然后是“儿子”——那么所有这 100 个线程将首先在“M”处相互等待,然后在“D”处,然后在“S”处”。好吧,大约/可能当然。但你明白了。如果数据配置文件更随机,那么他们' d 互相阻挡少得多。请注意,处理一个单词是一项.. 小任务,并且您尝试处理相同的单词。即使内部 CRF 的锁定是智能的,它也必然会命中相同的区域。使用更分散的数据再试一次。
再加上线程成本的事实。如果使用锁来防止竞争,那么每次锁定/解锁都会产生成本,因为至少他们必须“停止并检查锁是否打开”(对不起,措辞非常不准确)。如果要处理的数据相对于锁检查的数量很小,那么添加更多线程将无济于事,只会浪费时间。对于检查一个单词,甚至可能发生单独处理单个锁的时间比处理单词要长!但是,如果要处理的数据量更大,那么与处理数据相比,翻转锁的成本可能开始可以忽略不计。
准备一组 100 个或更多的单词。在一个线程上运行并测量它。然后随机划分单词并在 2 和 4 个线程上运行。并测量。如果不是更好,请尝试 1000 和 10000 字。当然,越多越好,请记住测试不应持续到您的下一个生日;)
如果您注意到 10k 个单词分成 4 个线程(每天 2500w)的工作效率比在一个线程上快 40%-30% 甚至 25% - 来吧!你只是给了它一个太小的工作。它是为更大的量身定制和优化的!
但是,另一方面,10k 字拆分为 4 个线程可能不会工作得更快,或者更糟,工作得更慢 - 这可能表明库处理多线程非常错误。现在尝试其他的事情,比如剥去螺纹或修复它。