我对大内存分配对.Net运行时的可伸缩性的影响感到奇怪。在我的测试应用程序中,我在一个紧密的循环中创建了许多字符串,持续了固定的循环次数,并吐出了每秒循环迭代的速率。当我在几个线程中运行这个循环时,出现了奇怪的现象 - 看起来速率并没有线性增加。当你创建大字符串时,问题会变得更严重。
让我告诉你结果。我的机器是一个运行Windows Server 2008 R1,32位的8GB 8核盒子。它有两个4核Intel Xeon 1.83ghz(E5320)处理器。执行的“工作”是对字符串的ToUpper()
和ToLower()
的一组交替调用。我为一个线程,两个线程等运行测试 - 最多。下表中的列是:
第一个例子从一个线程开始,然后是两个线程,最终用八个线程运行测试。每个线程创建10,000个字符串,每个字符串1024个字符:
Creating 10000 strings per thread, 1024 chars each, using up to 8 threads GCMode = Server Rate Linear Rate % Variance Threads -------------------------------------------------------- 322.58 322.58 0.00 % 1 689.66 645.16 -6.90 % 2 882.35 967.74 8.82 % 3 1081.08 1290.32 16.22 % 4 1388.89 1612.90 13.89 % 5 1666.67 1935.48 13.89 % 6 2000.00 2258.07 11.43 % 7 2051.28 2580.65 20.51 % 8 Done.
在第二个例子中,我将每个字符串的字符数增加到32,000。
Creating 10000 strings per thread, 32000 chars each, using up to 8 threads GCMode = Server Rate Linear Rate % Variance Threads -------------------------------------------------------- 14.10 14.10 0.00 % 1 24.36 28.21 13.64 % 2 33.15 42.31 21.66 % 3 40.98 56.42 27.36 % 4 48.08 70.52 31.83 % 5 61.35 84.63 27.51 % 6 72.61 98.73 26.45 % 7 67.85 112.84 39.86 % 8 Done.
注意方差与线性速率的差异;在第二个表中,实际费率比线性费率低39%。
我的问题是:为什么这个应用程序不能线性扩展?
我最初认为这可能是由于虚假共享,但正如您在源代码中看到的那样,我不会共享任何集合,而且字符串非常大。可能存在的唯一重叠是在一个字符串的开头和另一个字符串的结尾。
我正在使用gcServer enabled = true,以便每个核心都有自己的堆和垃圾收集器线程。
我认为我分配的对象不会被发送到大对象堆,因为它们大于85000字节。
我认为由于实习MSDN,字符串值可能会在引擎盖下共享,因此我尝试编译实习禁用。这比上面显示的结果更糟糕
我尝试使用小型和大型整数数组的相同示例,其中我遍历每个元素并更改值。它产生类似的结果,跟随更大的分配表现更差的趋势。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Diagnostics;
using System.Runtime;
using System.Runtime.CompilerServices;
namespace StackOverflowExample
{
public class Program
{
private static int columnWidth = 14;
static void Main(string[] args)
{
int loopCount, maxThreads, stringLength;
loopCount = maxThreads = stringLength = 0;
try
{
loopCount = args.Length != 0 ? Int32.Parse(args[0]) : 1000;
maxThreads = args.Length != 0 ? Int32.Parse(args[1]) : 4;
stringLength = args.Length != 0 ? Int32.Parse(args[2]) : 1024;
}
catch
{
Console.WriteLine("Usage: StackOverFlowExample.exe [loopCount] [maxThreads] [stringLength]");
System.Environment.Exit(2);
}
float rate;
float linearRate = 0;
Stopwatch stopwatch;
Console.WriteLine("Creating {0} strings per thread, {1} chars each, using up to {2} threads", loopCount, stringLength, maxThreads);
Console.WriteLine("GCMode = {0}", GCSettings.IsServerGC ? "Server" : "Workstation");
Console.WriteLine();
PrintRow("Rate", "Linear Rate", "% Variance", "Threads"); ;
PrintRow(4, "".PadRight(columnWidth, '-'));
for (int runCount = 1; runCount <= maxThreads; runCount++)
{
// Create the workers
Worker[] workers = new Worker[runCount];
workers.Length.Range().ForEach(index => workers[index] = new Worker());
// Start timing and kick off the threads
stopwatch = Stopwatch.StartNew();
workers.ForEach(w => new Thread(
new ThreadStart(
() => w.DoWork(loopCount, stringLength)
)
).Start());
// Wait until all threads are complete
WaitHandle.WaitAll(
workers.Select(p => p.Complete).ToArray());
stopwatch.Stop();
// Print the results
rate = (float)loopCount * runCount / stopwatch.ElapsedMilliseconds;
if (runCount == 1) { linearRate = rate; }
PrintRow(String.Format("{0:#0.00}", rate),
String.Format("{0:#0.00}", linearRate * runCount),
String.Format("{0:#0.00} %", (1 - rate / (linearRate * runCount)) * 100),
runCount.ToString());
}
Console.WriteLine("Done.");
}
private static void PrintRow(params string[] columns)
{
columns.ForEach(c => Console.Write(c.PadRight(columnWidth)));
Console.WriteLine();
}
private static void PrintRow(int repeatCount, string column)
{
for (int counter = 0; counter < repeatCount; counter++)
{
Console.Write(column.PadRight(columnWidth));
}
Console.WriteLine();
}
}
public class Worker
{
public ManualResetEvent Complete { get; private set; }
public Worker()
{
Complete = new ManualResetEvent(false);
}
public void DoWork(int loopCount, int stringLength)
{
// Build the string
string theString = "".PadRight(stringLength, 'a');
for (int counter = 0; counter < loopCount; counter++)
{
if (counter % 2 == 0) { theString.ToUpper(); }
else { theString.ToLower(); }
}
Complete.Set();
}
}
public static class HandyExtensions
{
public static IEnumerable<int> Range(this int max)
{
for (int counter = 0; counter < max; counter++)
{
yield return counter;
}
}
public static void ForEach<T>(this IEnumerable<T> items, Action<T> action)
{
foreach(T item in items)
{
action(item);
}
}
}
}
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<runtime>
<gcServer enabled="true"/>
</runtime>
</configuration>
要在您的盒子上运行StackOverflowExample.exe,请使用以下命令行参数调用它:
StackOverFlowExample.exe [loopCount] [maxThreads] [stringLength]
loopCount
:每个线程操纵字符串的次数。maxThreads
:要前进的主题数。stringLength
:填充字符串的字符数。答案 0 :(得分:5)
您可能希望查看this question of mine。
我遇到了类似的问题,这是因为CLR在分配内存时执行线程间同步以避免重叠分配。现在,使用服务器GC,锁定算法可能会有所不同 - 但沿着这些相同的行可能会影响您的代码。
答案 1 :(得分:2)
您运行此硬件的硬件无法对多个进程或线程进行线性扩展。
你有一个记忆库。这是一个瓶颈(多通道内存可能会改善访问,但不是为了更多的进动而不是你的内存库(似乎e5320处理器支持1-4个内存通道)。
每个物理CPU包只有一个内存控制器(在你的情况下只有两个),这是一个瓶颈。
每个cpu包有2个l2缓存。这是一个瓶颈。如果缓存耗尽,将发生缓存一致性问题。
在管理流程调度和内存管理方面甚至没有出现OS / RTL / VM问题,这也会导致非线性扩展。
我认为你的结果非常合理。多线程显着加速,每次增加到8 ...
真的,您有没有读过任何建议商品多CPU硬件能够线性扩展多个进程/线程的东西?我没有。
答案 2 :(得分:0)
您的初始帖子存在根本缺陷 - 您假设通过并行执行可以实现线性加速。它不是,也从来没有。请参阅Amdahl's Law(是的,我知道,维基百科,但它比其他任何内容都容易)。
从CLR提供的抽象中看,您的代码似乎没有依赖关系 - 但是,正如LBushkin指出的那样,情况并非如此。正如SuperMagic指出的那样,硬件本身意味着执行线程之间的依赖关系。几乎任何可以并行化的问题都是如此 - 即使是独立的机器,使用独立的硬件,问题的某些部分通常需要一些同步元素,并且同步会阻止线性加速。
答案 3 :(得分:0)
内存分配器对应用程序speedup的影响与分配数的关系比分配的数量更密切。它也受分配延迟(在单个线程上完成单个分配的时间量)的影响更大,在CLR的情况下由于使用bump-pointer allocator (see section 3.4.3)而非常快。
你的问题是问为什么实际加速是次线性的,为了回答你应该检查Amdahl's Law。
回到Notes on the CLR Garbage Collector,您可以看到分配上下文属于特定线程(第3.4.1节),这减少了(但并未消除)多线程分配期间所需的同步量。如果你发现分配确实是一个弱点,我建议尝试一个对象池(可能是每个线程)来减少收集器的负载。通过减少分配的绝对数量,您将减少收集器必须运行的次数。但是,这也会导致更多的对象进入第2代,这在需要时收集的速度最慢。
最后,Microsoft继续在较新版本的CLR中改进垃圾收集器,因此您应该定位最新版本的版本(至少是.NET 2)。
答案 4 :(得分:0)
很好的问题卢克!我对答案非常感兴趣。
我怀疑你并不期望线性缩放,但比39%的差异更好。
NoBugz - 基于280Z28的链接,每个核心实际上有一个GC堆,其中GCMode = Server。每堆还应该有一个GC线程。这不应该导致你提到的并发问题吗?
遇到了类似的问题 由于CLR的表现 线程间同步时 分配内存以避免重叠 分配。现在,使用服务器GC, 锁定算法可能不同 - 但是同样的话可能会影响你的代码
LBushkin - 我认为这是关键问题,GCMode = Server在分配内存时是否还会导致线程间锁定?任何人都知道 - 或者可以简单地通过SuperMagic提到的硬件限制来解释?