Multithreading 达到特定数量的内核后出现严重的多线程内存瓶颈
我们第一次在一台超过12核的机器上测试我们的软件的可伸缩性,在添加了第12个线程之后,我们遇到了性能的急剧下降。在这方面花了几天的时间后,我们对于下一步该做什么感到困惑 测试系统是一个双Opteron 6174(2x12核),内存为16 GB,Windows Server 2008 R2 基本上,性能从10-12个线程达到峰值,然后从悬崖上跌落,很快就会以大约与4个线程相同的速度执行工作。下降幅度相当大,在16-20个线程中,吞吐量降至最低。我们使用运行多线程的单个进程和运行单线程的多个进程进行了测试,结果基本相同。该处理相当占用内存,并且占用磁盘空间 我们相当肯定这是内存瓶颈,但我们不认为这是缓存问题。证据如下:Multithreading 达到特定数量的内核后出现严重的多线程内存瓶颈,multithreading,performance,memory-management,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成为瓶颈的可能性
- 提交费用只有大约4gbs,因此我们应该远远低于分页成为问题的阈值
- 最好的数据来自使用AMD的CodeAnalyst工具。CodeAnalyst显示,windows内核在使用12个线程时占用了大约6%的cpu时间,而在使用24个线程时占用了80-90%的cpu时间。绝大多数时间都花在ExAcquireResourceSharedLite(50%)和KeAcquireInStackQueuedSpinLockAtDpcLevel(46%)函数上。以下是从12个线程运行到24个线程运行时内核因子变化的亮点:
说明:5.56(更多倍)
时钟周期:10.39
内存操作:4.58
缓存未命中率:0.25(实际缓存未命中率为0.1,比使用12个线程时小4倍)
平均缓存未命中延迟:8.92
总缓存未命中延迟:6.69
内存组负载冲突:11.32
Mem银行商店冲突:2.73
转发的Mem:7.42
这就是我们现在的处境。关于这个瓶颈的确切原因或我们如何避免它,有什么想法吗?要解决您的要点: 1) 如果您有12个内核在100%使用率和12个内核空闲,那么您的CPU总使用率将为50%。如果您的同步是自旋锁式的,那么即使没有完成有用的工作,线程仍然会使其CPU饱和 2) 跳过 3) 我同意你的结论。将来,您应该知道Perfmon有一个计数器:Process\Page Faults/sec可以验证这一点 4) 如果您没有ntoskrnl的专用符号,CodeAnalyst可能无法在其配置文件中告诉您正确的函数名。相反,它只能指向具有符号的最近函数。您可以使用CodeAnalyst通过概要文件获得堆栈跟踪吗?这可以帮助您确定线程执行的驱动内核使用的操作
此外,我以前在微软的团队提供了许多性能分析工具和指导原则,包括对CPU配置文件进行堆栈跟踪。我不确定自己是否完全理解这些问题,因此我可以为您提供解决方案,但根据您的解释,我可能有一些其他观点可能会有所帮助 我用C语言编程,所以对我有用的东西可能不适用于你的情况 你们的处理器有12MB的L3和6MB的L2,这两种处理器都很大,但在我看来,它们很少足够大 您可能正在使用rdtsc为各个部分计时。当我使用它时,我有一个统计结构,我将执行代码不同部分的测量结果发送到其中。观察值的平均值、最小值、最大值和数量是显而易见的,但标准偏差也有其作用,因为它可以帮助您决定是否应研究较大的最大值。只有在需要读取标准偏差时才需要计算标准偏差:在此之前,标准偏差可以存储在其组件中(n,总和x,总和x^2)。除非对非常短的序列计时,否则可以省略前面的同步指令。确保你量化了时间开销,如果只是为了能够排除它无关紧要的话 当我编写多线程程序时,我试图使每个核心/线程的任务尽可能“内存有限”。所谓内存有限,我的意思是不做需要不必要的内存访问的事情。不必要的内存访问通常意味着尽可能多的内联代码和尽可能少的操作系统访问。对我来说,操作系统是一个很大的未知因素,因为调用它会产生多少内存工作,所以我尽量将对它的调用控制在最低限度。以同样的方式,但通常在对性能影响较小的程度上,我尽量避免调用应用程序函数:如果必须调用它们,我宁愿它们不要调用很多其他东西 以同样的方式,我最小化内存分配:如果我需要几个,我将它们加在一起,然后将一个大的分配细分为更小的分配。这将有助于以后的分配,因为在找到返回的块之前,它们需要循环通过更少的块。我只在绝对必要时阻止初始化 我还尝试通过内联来减少代码大小。在移动/设置小块内存时,我更喜欢使用基于rep movsb和rep stosb的内部函数,而不是调用memcopy/memset,后者通常都是o
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;
}