首页 > 解决方案 > 如何在简单的 TPL DataFlow 管道中优化性能?

问题描述

鉴于:

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

完整代码提交在 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对象的数量和要处理的行的数量,这是昂贵的。

我的问题 - 我的实现是最优的吗?我有疑问,因为增加maxDOP1并且maxDOP2不会产生太大的改进:

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 个逻辑处理器。当然,代码要复杂得多。

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

标签: c#task-parallel-librarytpl-dataflow

解决方案


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

您应该记住的其他一点是,当从一个块传递到另一个块的消息是大块时,TPL 数据流表现良好。正如介绍性文件所说:“为粗粒度数据流和流水线任务提供进程内消息传递”(强调添加)。如果每条消息的处理都过于轻量级,那么您最终会产生大量开销。如果需要,您可以通过使用BatchBlock<T>s、ChunkLINQ 运算符或其他方式对消息进行批处理,从而将工作负载分块。

说了这么多,我的假设是您的工作不成比例地受 I/O 限制,从而降低了 CPU 功能的相关性。老实说,即使使用最复杂的实现,我也不会期望性能大幅提升。


推荐阅读