【问题标题】:Performance implications of a large number of mutexes大量互斥体的性能影响
【发布时间】:2015-06-12 19:42:27
【问题描述】:

假设我有一个包含 1,000,000 个元素的数组,以及多个工作线程,每个线程都在处理这个数组中的数据。工作线程可能正在使用新数据更新已填充的元素,但每个操作仅限于单个数组元素,并且独立于任何其他元素的值。

使用单个互斥锁来保护整个数组显然会导致高争用。在另一个极端,我可以创建一个与原始数组长度相同的互斥体数组,并且对于每个元素array[i],我会在对其进行操作时锁定mutex[i]。假设数据分布均匀,这将主要消除锁争用,但会消耗大量内存。

我认为更合理的解决方案是拥有一组n 互斥体(其中 1 array[i],我会在操作时锁定mutex[i % n]。如果n 足够大,我仍然可以尽量减少争用。

所以我的问题是,除了增加内存使用量之外,以这种方式使用大量(例如 >= 1000000)互斥量是否会降低性能?如果是这样,您可以合理使用多少互斥锁才能开始看到降级?

我确信这个问题的答案在某种程度上是特定于平台的;我在 Linux 上使用 pthreads。我也在努力建立自己的基准,但我正在处理的数据规模使得这很耗时,因此我们将不胜感激。


这是最初的问题。对于那些询问有关该问题的更详细信息的人,我有 4 个多 GB 的二进制数据文件,描述了正在分析的大约 50 亿个事件附近的某个地方。有问题的数组实际上是支持一个非常大的链式哈希表的指针数组。我们将这四个数据文件读入哈希表,如果它们共享某些特征,可能会将它们聚合在一起。现有的实现有 4 个线程,每个线程读取一个文件并将该文件中的记录插入到哈希表中。哈希表有 997 个锁和 997*9973 = ~10,000,000 个指针。在插入带有哈希h的元素时,我先锁定mutex[h % 997],然后再插入或修改bucket[h % 9943081]中的元素。这没问题,据我所知,我们没有太多的争用问题,但存在性能瓶颈,因为我们只使用了 16 核机器的 4 核。 (因为文件的大小通常不一样,所以我们会更少。)一旦所有的数据都被读入内存,然后我们分析它,它使用新线程和新的锁定策略调整到不同的工作量。

我正在尝试通过切换到线程池来提高数据加载阶段的性能。在新模型中,我仍然为每个文件设置一个线程,它只是以大约 1MB 的块读取文件,并将每个块传递给池中的工作线程以进行解析和插入。到目前为止,性能提升很小,我所做的分析似乎表明锁定和解锁阵列所花费的时间可能是罪魁祸首。锁定内置于我们正在使用的哈希表实现中,但它确实允许指定要使用的锁的数量,而与表的大小无关。我希望在不改变哈希表实现本身的情况下加快速度。

【问题讨论】:

  • 哪个操作系统?我在 CentOS 上尝试过一次,使用约 1M 互斥体的质数,而不是约 1K 互斥体的质数(哦,顺便说一句,使用质数),并且由于我从未发现的原因,性能受到了巨大的影响。
  • 方案A:锁定整个阵列。你是对的:这很简单……但可能会导致高争用。计划 B:为每个元素创建和管理互斥体。可能不是一个好主意...建议:考虑read write,或“RCU”锁:docs.oracle.com/cd/E19455-01/806-5257/6je9h032u/index.html
  • @paulsm4 读/写锁对这种特殊情况没有帮助,因为我正在从输入文件构建数据结构。此时数据结构本质上是“只写”的,因此共享阅读器不会有帮助。
  • @AmiTavory 这是 Amazon EC2 实例上的 AWS linux,因此它应该与 CentOS 非常相似。我最初的实现使用了 997 个互斥锁。到目前为止,增加到 9973 似乎 减少而不是增加吞吐量,但我仍在努力设置有用的性能测试。您的经历与我的怀疑相符,但我很想知道为什么会这样。
  • 我只想指出,几乎总有更好的方法来构建事物,而不是让大量线程都紧紧地争夺对同一个集合的访问权。如果我们更多地了解集合中的对象类型以及对它们执行的操作类型,我们可能会想出一个解决方案,让线程不那么激烈。

标签: c multithreading pthreads


【解决方案1】:

(对您的问题的一个非常片面且可能是间接的回答。)

曾经尝试(在 CentOS 上)将锁的数量从约 1K 的素数提高到约 1M 的素数,从而获得了巨大的性能冲击。虽然我从未完全理解其原因,但我最终发现(或只是说服自己)这是个错误的问题。

假设您有一个长度为 M 的数组,其中包含 n 个工作器。此外,您使用散列函数来保护具有 m 锁的 M 元素(例如,通过一些随机分组)。然后,使用Square Approximation to the Birthday Paradox,两个工人之间发生碰撞的机会 - p - 由以下公式给出:

p ~ n2 / (2m)


因此,您需要的互斥体数量 m 根本不依赖于 M - 它是 p 的函数和 n 只。

【讨论】:

  • 在 glibc 下 sizeof(pthread_mutex_t) 是 24,所以你的 ~1M 互斥数组占用了 ~24M 的内存,很容易耗尽你的 L3 缓存并导致很多非常慢的主内存丢失。
  • (就是x86上的glibc,其他架构大小不同)。
  • 从技术上讲,@caf 按要求回答了这个问题,但我将把它标记为答案,因为它回答了我应该问的下一个问题。谢谢你们俩。
【解决方案2】:

在 Linux 下,除了与更多互斥锁相关的内存之外,没有其他成本。

但是,请记住,您的互斥锁使用的内存必须包含在您的工作集中 - 如果您的工作集大小超过相关的缓存大小,您将看到性能显着下降。这意味着您不想要一个过大的互斥数组。

正如Ami Tavory 指出的那样,争用取决于互斥锁的数量和线程的数量,而不是受保护的数据元素的数量 - 所以没有理由将互斥锁的数量与数据元素的数量联系起来(使用明显的附带条件是,拥有更多个互斥锁比元素多)。

【讨论】:

    【解决方案3】:

    在一般情况下,我会建议

    • 只需锁定整个数组(简单,如果您的应用程序除了访问数组之外​​主要在做“其他事情”,则通常“足够好”)

      ...或...

    • 在整个数组上实现读/写锁(假设读取等于或超过写入)

    显然您的方案与这两种情况都不匹配。

    问:您是否考虑过实现某种“写入队列”?

    最坏的情况,你只需要 一个 互斥体。最好的情况是,您甚至可以使用无锁机制来管理您的队列。在这里寻找一些可能适用的想法:https://msdn.microsoft.com/en-us/library/windows/desktop/ee418650%28v=vs.85%29.aspx

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-08-05
      • 1970-01-01
      • 2012-07-04
      • 2011-04-10
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多