【问题标题】:How to optimize performance in a simple TPL DataFlow pipeline?如何在简单的 TPL DataFlow 管道中优化性能?
【发布时间】: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);
}

这里有一些注意事项:

  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 !]>

我看到的是:

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

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

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

【问题讨论】:

    标签: c# task-parallel-library tpl-dataflow


    【解决方案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 并不容易。到目前为止,我未能从内存中创建项目对象。在这里发布了一个问题 - stackoverflow.com/questions/69454500/…
    • @mark 我希望能帮上忙,但我对 MSBuild 几乎一无所知。
    • 我已更改代码以处理多个字符串文字,更多问题 - stackoverflow.com/questions/69459634/…。谢谢。
    猜你喜欢
    • 1970-01-01
    • 2021-09-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-11
    • 1970-01-01
    相关资源
    最近更新 更多