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

Posted

技术标签:

【中文标题】如何在简单的 TPL DataFlow 管道中优化性能?【英文标题】:How to optimize performance in a simple TPL DataFlow pipeline? 【发布时间】:2021-11-25 07:14:58 【问题描述】:

给定:

数百个 .NET 项目 所有项目中的数千个 C# 文件 字符串文字

我想在所有项目的所有文件中输出给定文字的所有匹配项。我想使用这个示例来了解如何优化简单 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);

这里有一些注意事项:

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

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

    produceCSFiles 被提供廉价的 ProjectEx 对象并输出大量 CSFile 对象,由于项目评估,这很昂贵。 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 !]>

我看到的是:

最大改进是 maxDOP1 == 3maxDOP2 == 4 - 14.12 秒 vs 21.17 秒 最大投资回报率是 maxDOP1 == 2maxDOP2 == 3 - 15 秒 vs 21.17 秒

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

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

【问题讨论】:

【参考方案1】:

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

您应该记住的其他一点是,当从一个块传递到另一个块的消息是大块时,TPL 数据流会很好地执行。正如introductory document 所说:“为粗粒度数据流和流水线任务提供进程内消息传递”(强调)。如果每条消息的处理都过于轻量级,那么您最终会产生大量开销。如果需要,您可以通过使用BatchBlock&lt;T&gt;s、Chunk LINQ 运算符或other means 将消息批处理来分块您的工作负载。

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

【讨论】:

听起来你建议有单独的工作负载将文件内容读入线阵列,然后有单独的工作负载来处理成批的线阵列。这对应于在produceMatchingLines 中将 IO 与 CPU 分离。如果我单独阅读项目文件并使用Project(XMLReader xmlReader) 而不是Project(string projectFile),也可以在produceCSFiles 中这样做。值得检查。虽然它会生成 IO --> CPU --> IO --> CPU。我将无法将所有 IO 与所有 CPU 隔离开来 @mark yeap,这就是我的建议。如果您最终得到多个 I/O 块,则可能值得尝试使用相同的 ConcurrentScheduler 配置所有这些块,以实施通用 I/O 并发策略。从我对 SSD 的小经验来看,这些设备对一点点并行性 (~2) 有积极的反应,而经典的硬盘驱动器在一次只做一件事时表现最好。 不幸的是,msbuild 并不容易。到目前为止,我未能从内存中创建项目对象。在这里发布了一个问题 - ***.com/questions/69454500/… @mark 我希望能帮上忙,但我对 MSBuild 几乎一无所知。 我已更改代码以处理多个字符串文字,更多问题 - ***.com/questions/69459634/…。谢谢。

以上是关于如何在简单的 TPL DataFlow 管道中优化性能?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 TPL/Dataflow 中发出笛卡尔积?

TPL Dataflow 如何与“全局”数据同步

TPL-Dataflow 是不是适用于高并发应用程序?

TPL Dataflow BufferBlock 线程安全吗?

使用 TPL-Dataflow 进行聚合和连接(内、外、左……)?

IO读写操作的TPL Dataflow实现中的内存问题