multithreading - 达到特定核心数后出现严重的多线程内存瓶颈

标签 multithreading performance memory-management

我们首次在具有 > 12 个内核的机器上测试我们的软件以实现可扩展性,并且在添加第 12 个线程后,我们遇到了令人讨厌的性能下降。在花了几天时间之后,我们对接下来要尝试什么感到困惑。

测试系统是双 Opteron 6174(2x12 核),内存为 16 GB,Windows Server 2008 R2。

基本上,性能在 10 到 12 个线程时达到峰值,然后一落千丈,很快就会以与大约 4 个线程时大致相同的速度执行工作。下降幅度相当大,并且通过 16 - 20 个线程,它在吞吐量方面达到了最低点。我们已经对运行多个线程的单个进程和运行单个线程的多个进程进行了测试——结果几乎相同。该处理是相当内存密集型和磁盘密集型的。

我们相当肯定这是内存瓶颈,但我们不认为这是缓存问题。证据如下:

  • 当从 12 个线程扩展到 24 个线程时,CPU 使用率继续从 50% 攀升至 100%。如果我们遇到同步/死锁问题,我们预计 CPU 使用率会在达到 100% 之前达到顶峰。
  • 在后台复制大量文件时进行测试对处理速度的影响很小。我们认为这排除了磁盘 i/o 的瓶颈。
  • 提交费用只有大约 4 GB,因此我们应该远低于分页成为问题的阈值。
  • 最好的数据来自使用 AMD 的 CodeAnalyst 工具。 CodeAnalyst 显示 Windows 内核从使用 12 个线程时占用 CPU 时间的 6% 到使用 24 个线程时占用 CPU 时间的 80-90%。大部分时间花在 ExAcquireResourceSharedLite (50%) 和 KeAcquireInStackQueuedSpinLockAtDpcLevel (46%) 函数上。以下是从 12 线程运行到 24 线程运行时内核因素变化的亮点:

    说明:5.56(多倍)
    时钟周期:10.39
    内存操作:4.58
    缓存未命中率:0.25(实际缓存未命中率为0.1,比12线程小4倍)
    平均缓存未命中延迟:8.92
    总缓存未命中延迟:6.69
    内存银行负载冲突:11.32
    内存银行存储冲突:2.73
    内存转发:7.42

  • 我们认为这可能是 this paper 中描述的问题的证据。 ,但是我们发现将每个工作线程/进程固定到特定核心并没有改善结果(如果有的话,性能会变差一些)。

    这就是我们所处的位置。关于这个瓶颈的确切原因或我们如何避免它的任何想法?

    最佳答案

    我不确定我是否完全理解这些问题,以便我可以为您提供解决方案,但根据您的解释,我可能有一些其他的观点可能会有所帮助。

    我用 C 编程,所以对我有用的东西可能不适用于你的情况。

    您的处理器有 12MB 的 L3 和 6MB 的 L2,这很大,但在我看来,它们很少足够大!

    您可能正在使用 rdtsc 对各个部分进行计时。当我使用它时,我有一个统计结构,我将执行代码的不同部分的测量结果发送到其中。平均值、最小值、最大值和观察次数是显而易见的,但标准偏差也有其作用,因为它可以帮助您决定是否应该研究较大的最大值。标准偏差仅在需要读出时才需要计算:在此之前它可以存储在其分量中(n,sum x,sum x^2)。除非您正在计时非常短的序列,否则您可以省略前面的同步指令。确保量化时间开销,如果只是为了能够将其排除为无关紧要的话。

    当我对多线程进行编程时,我尝试使每个内核/线程的任务尽可能“受内存限制”。内存受限是指不做需要不必要的内存访问的事情。不必要的内存访问通常意味着尽可能多的内联代码和尽可能少的操作系统访问。对我来说,操作系统在调用它会产生多少内存工作方面是一个很大的未知数,所以我尽量减少对它的调用。以同样的方式,但通常对性能影响较小,我尽量避免调用应用程序函数:如果必须调用它们,我宁愿它们不调用很多其他东西。

    以同样的方式,我最小化内存分配:如果我需要几个,我将它们加在一起,然后将一个大的分配分割为更小的分配。这将有助于以后的分配,因为在找到返回的 block 之前,它们需要遍历更少的 block 。我只在绝对必要时才阻止初始化。

    我还尝试通过内联来减少代码大小。在移动/设置小块内存时,我更喜欢使用基于 rep movsb 和 rep stosb 的内在函数,而不是调用 memcopy/memset,它们通常都针对较大的 block 进行了优化,并且在大小上没有特别限制。

    我最近才开始使用自旋锁,但我将它们实现为内联(任何事情都比调用操作系统更好!)。我猜操作系统替代品是关键部分,虽然它们很快,但本地自旋锁更快。由于它们执行附加处理,这意味着它们会阻止在此期间执行应用程序处理。这是实现:

    inline void spinlock_init (SPINLOCK *slp)
    {
      slp->lock_part=0;
    }
    
    inline char spinlock_failed (SPINLOCK *slp)
    {
      return (char) __xchg (&slp->lock_part,1);
    }
    

    或者更详细(但不过分):
    inline char spinlock_failed (SPINLOCK *slp)
    {
      if (__xchg (&slp->lock_part,1)==1) return 1;
      slp->count_part=1;
      return 0;
    }
    

    并释放
    inline void spinlock_leave (SPINLOCK *slp)
    {
      slp->lock_part=0;
    }
    

    或者
    inline void spinlock_leave (SPINLOCK *slp)
    {
      if (slp->count_part==0) __breakpoint ();
      if (--slp->count_part==0) slp->lock_part=0;
    }
    

    计数部分是我从嵌入式(和其他编程)中带来的,用于处理嵌套中断。

    我也是 IOCP 的忠实粉丝,因为它们在处理 IO 事件和线程方面的效率很高,但您的描述并未表明您的应用程序是否可以使用它们。无论如何,您似乎都在节省它们,这很好。

    关于multithreading - 达到特定核心数后出现严重的多线程内存瓶颈,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3718269/

    相关文章:

    c# - 如何在 C# 中使用线程制作重复按钮?

    java - 如何等待 Android runOnUiThread 完成?

    java - 快速访问来电者信息

    ios - 如何正确释放CVMetalTextureCacheRef和CVMetalTextureRef类型

    将堆栈内存用于不完整结构的 C 最佳实践

    c++ - 在等待之前必须检查 std::condition_variable 谓词吗?

    java - Spring Boot中通过读取文件进行操作

    php - CodeIgniter Performance 多次加载 View 与 View 循环

    c - 为什么 C 比 Go 或 D 更快地构建小程序?

    c++ - 删除运算符如何在我的代码中工作?