为什么 Parallel.For 无法快速处理堆密集型操作?

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)

结论是内存分配不能很好地适应多线程吗?...

O. *_*nes 3

tl;dr:堆分配争用。

你的第一个函数是令人尴尬的并行。每个线程都可以通过与其他线程极少的交互来完成其计算。因此它可以很好地扩展到多线程。huseyin tugrul buyukisik 正确地指出,您的第一个计算使用了非共享的、每线程的处理器寄存器。

你的第二个函数,当它预分配字典时,并行性稍微好一些。每个线程的计算都独立于其他线程,除了它们各自使用计算机的 RAM 子系统这一事实。因此,当向机器级 RAM 写入和读取线程级缓存数据时,您会在硬件级别看到一些线程到线程的争用。

您的第二个不预分配内存的函数并不是令人尴尬的并行。为什么不?每个.Add()操作都必须在共享堆中分配一些数据。这不能并行完成,因为所有线程共享相同的堆。相反,它们必须同步。dotnet 库在尽可能并行化堆操作方面做得很好,但是当线程 A 分配堆数据时,它们至少无法避免线程 B 的一些阻塞。所以线程会互相减慢速度。

单独的进程而不是单独的线程是扩展工作负载(例如非预分配的第二个函数)的好方法。每个进程都有自己的堆。