use*_*489 7 c# parallel-processing multithreading parallel.for
对于某些操作,Parallel可以很好地随 CPU 数量进行扩展,但对于其他操作则不然。
考虑下面的代码,function1获得 10 倍的改进,同时function2获得 3 倍的改进。这是由于内存分配,还是GC?
void function1(int v) {
for (int i = 0; i < 100000000; i++) {
var q = Math.Sqrt(v);
}
}
void function2(int v) {
Dictionary<int, int> dict = new Dictionary<int, int>();
for (int i = 0; i < 10000000; i++) {
dict.Add(i, v);
}
}
var sw = new System.Diagnostics.Stopwatch();
var iterations = 100;
sw.Restart();
for (int v = 0; v < iterations; v++) function1(v);
sw.Stop();
Console.WriteLine("function1 no parallel: " + sw.Elapsed.TotalMilliseconds.ToString("### ##0.0ms"));
sw.Restart();
Parallel.For(0, iterations, function1);
sw.Stop();
Console.WriteLine("function1 with parallel: " + sw.Elapsed.TotalMilliseconds.ToString("### ##0.0ms"));
sw.Restart();
for (int v = 0; v < iterations; v++) function2(v);
sw.Stop();
Console.WriteLine("function2 no parallel: " + sw.Elapsed.TotalMilliseconds.ToString("### ##0.0ms"));
sw.Restart();
Parallel.For(0, iterations, function2);
sw.Stop();
Console.WriteLine("function2 parallel: " + sw.Elapsed.TotalMilliseconds.ToString("### ##0.0ms"));
Run Code Online (Sandbox Code Playgroud)
我机器上的输出:
void function1(int v) {
for (int i = 0; i < 100000000; i++) {
var q = Math.Sqrt(v);
}
}
void function2(int v) {
Dictionary<int, int> dict = new Dictionary<int, int>();
for (int i = 0; i < 10000000; i++) {
dict.Add(i, v);
}
}
var sw = new System.Diagnostics.Stopwatch();
var iterations = 100;
sw.Restart();
for (int v = 0; v < iterations; v++) function1(v);
sw.Stop();
Console.WriteLine("function1 no parallel: " + sw.Elapsed.TotalMilliseconds.ToString("### ##0.0ms"));
sw.Restart();
Parallel.For(0, iterations, function1);
sw.Stop();
Console.WriteLine("function1 with parallel: " + sw.Elapsed.TotalMilliseconds.ToString("### ##0.0ms"));
sw.Restart();
for (int v = 0; v < iterations; v++) function2(v);
sw.Stop();
Console.WriteLine("function2 no parallel: " + sw.Elapsed.TotalMilliseconds.ToString("### ##0.0ms"));
sw.Restart();
Parallel.For(0, iterations, function2);
sw.Stop();
Console.WriteLine("function2 parallel: " + sw.Elapsed.TotalMilliseconds.ToString("### ##0.0ms"));
Run Code Online (Sandbox Code Playgroud)
环境:
Win 11、.Net 6.0、Release build
i9 第 12 代、16 核、24 进程、32 GB DDR5
经过更多测试后,内存分配似乎不能很好地适应多线程。例如,如果我将函数 2 更改为:
void function2(int v) {
Dictionary<int, int> dict = new Dictionary<int, int>(10000000);
}
Run Code Online (Sandbox Code Playgroud)
结果是:
function2 no parallell: 124,0 ms
function2 parallell: 402,4 ms
Run Code Online (Sandbox Code Playgroud)
结论是内存分配不能很好地适应多线程吗?...
tl;dr:堆分配争用。
你的第一个函数是令人尴尬的并行。每个线程都可以通过与其他线程极少的交互来完成其计算。因此它可以很好地扩展到多线程。huseyin tugrul buyukisik 正确地指出,您的第一个计算使用了非共享的、每线程的处理器寄存器。
你的第二个函数,当它预分配字典时,并行性稍微好一些。每个线程的计算都独立于其他线程,除了它们各自使用计算机的 RAM 子系统这一事实。因此,当向机器级 RAM 写入和读取线程级缓存数据时,您会在硬件级别看到一些线程到线程的争用。
您的第二个不预分配内存的函数并不是令人尴尬的并行。为什么不?每个.Add()操作都必须在共享堆中分配一些数据。这不能并行完成,因为所有线程共享相同的堆。相反,它们必须同步。dotnet 库在尽可能并行化堆操作方面做得很好,但是当线程 A 分配堆数据时,它们至少无法避免线程 B 的一些阻塞。所以线程会互相减慢速度。
单独的进程而不是单独的线程是扩展工作负载(例如非预分配的第二个函数)的好方法。每个进程都有自己的堆。