如何在简单的 TPL DataFlow 管道中优化性能?

How to optimize performance in a simple TPL DataFlow pipeline?

给定:

我想在所有项目的所有文件中输出给定文字的所有匹配项。我想使用此示例来了解如何优化简单 TPL 数据流管道的性能。

完整代码提交于github - https://github.com/MarkKharitonov/LearningTPLDataFlow/blob/master/FindStringCmd.cs

管道本身是:

private void Run(string workspaceRoot, string literal, int maxDOP1 = 1, int maxDOP2 = 1)
{
    var projects = (workspaceRoot + "build\projects.yml").YieldAllProjects();

    var produceCSFiles = new TransformManyBlock<ProjectEx, CSFile>(YieldCSFiles, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = maxDOP1 });
    var produceMatchingLines = new TransformManyBlock<CSFile, MatchingLine>(csFile => csFile.YieldMatchingLines(literal), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = maxDOP2 });
    var getMatchingLines = new ActionBlock<MatchingLine>(o => Console.WriteLine(o.ToString(workspaceRoot)));

    var linkOptions = new DataflowLinkOptions { PropagateCompletion = true };

    produceCSFiles.LinkTo(produceMatchingLines, linkOptions);
    produceMatchingLines.LinkTo(getMatchingLines, linkOptions);

    Console.WriteLine($"Locating all the instances of {literal} in the C# code ... ");
    var sw = Stopwatch.StartNew();

    projects.ForEach(p => produceCSFiles.Post(p));
    produceCSFiles.Complete();
    getMatchingLines.Completion.Wait();

    sw.Stop();
    Console.WriteLine(sw.Elapsed);
}

这里有一些注意事项:

  1. 获得ProjectEx个对象非常便宜。
  2. 第一次访问 属性 ProjectEx.MSBuildProject 非常昂贵。这是 Microsoft Build API 评估相应 csproj 文件的地方。
  3. 经过评估得到CS文件列表是很便宜的,但是全部处理它们是相当昂贵的,因为太多了。

我不确定如何在此处以图形方式描述管道,但是:

  1. produceCSFiles 被喂廉价 ProjectEx 个对象并输出大量 CSFile 个对象,由于项目评估,这是昂贵的。
  2. produceMatchingLines 被馈送 CSFile 个对象并输出匹配的行,由于 CSFile 个对象的绝对数量和要处理的行的数量,这是昂贵的。

我的问题 - 我的实施是否最佳?我有疑问,因为增加 maxDOP1maxDOP2 不会产生太大的改进:

C:\work\TPLDataFlow [master ≡ +0 ~2 -0 !]> 1..4 |% { $MaxDOP1 = $_ ; 1..4 } |% { $MaxDOP2 = $_ ; $res = .\bin\Debug\net5.0\TPLDataFlow.exe find-string -d C:\dayforce\tip -l GetClientLegalPromptFlag --maxDOP1 $MaxDOP1 --maxDOP2 $MaxDOP2 -q ; "$MaxDOP1 x $MaxDOP2 --> $res" }
1 x 1 --> Elapsed: 00:00:21.1683002
1 x 2 --> Elapsed: 00:00:19.8194133
1 x 3 --> Elapsed: 00:00:20.2626202
1 x 4 --> Elapsed: 00:00:20.4339065
2 x 1 --> Elapsed: 00:00:17.6475658
2 x 2 --> Elapsed: 00:00:15.4889941
2 x 3 --> Elapsed: 00:00:14.9014116
2 x 4 --> Elapsed: 00:00:14.9254166
3 x 1 --> Elapsed: 00:00:17.6474953
3 x 2 --> Elapsed: 00:00:14.4933295
3 x 3 --> Elapsed: 00:00:14.2419329
3 x 4 --> Elapsed: 00:00:14.1185203
4 x 1 --> Elapsed: 00:00:19.0717189
4 x 2 --> Elapsed: 00:00:15.9069517
4 x 3 --> Elapsed: 00:00:16.3267676
4 x 4 --> Elapsed: 00:00:17.0876474
C:\work\TPLDataFlow [master ≡ +0 ~2 -0 !]>

我看到的是:

总而言之,仅比单线程版本提高了 30%。这有点令人失望,因为所有文件都在 SSD 上,而我有 12 个逻辑处理器。当然,代码要复杂得多。

我错过了什么吗?也许我没有以最佳方式做到这一点?

这个架构不是最优的,因为每个工作块,produceCSFilesproduceMatchingLines,都在做混合的 I/O-bound 和 CPU 绑定工作。理想情况下,您希望有一个块专门用于专门执行 I/O-bound,而另一个专门用于 CPU 绑定工作。这样,您就可以根据相关硬件组件的功能,优化配置每个块的并行度。使用您当前的配置,完全有可能在给定时刻两个块都在做 I/O 工作,相互竞争以获取 SSD 的注意力,而 CPU 空闲。而在另一时刻,可能会发生完全相反的情况。结果是一片混乱和不协调的喧嚣。这与使用整体 Parallel.ForEach 循环时得到的结果类似,与单线程方法相比,这可能会产生可比的(平庸的)性能改进。

您应该记住的另一件事是,当从一个块传递到另一个块的消息是块状的时,TPL 数据流执行良好。作为 introductory document says: "provides in-process message passing for coarse-grained dataflow and pipelining tasks" (emphasis added). If the processing of each individual message is too lightweight, you'll end up with significant overhead. If you need to, you can chunkify your workload by batching the messages, using BatchBlock<T>s, the Chunk LINQ operator, or other means.

说了这么多,我的假设是您的工作不成比例地 I/O 受限,导致您的 CPU 能力的相关性降低。老实说,即使是最复杂的实现,我也不希望性能得到大幅提升。