c# - 如何在简单的 TPL DataFlow 管道中优化性能?
问题描述
鉴于:
- 数百个 .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
对象的数量和要处理的行的数量,这是昂贵的。
我的问题 - 我的实现是最优的吗?我有疑问,因为增加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 !]>
我看到的是:
- 最大改进是
maxDOP1 == 3
-maxDOP2 == 4
14.12 秒 vs 21.17 秒 - 最大投资回报率是
maxDOP1 == 2
-maxDOP2 == 3
15 秒 vs 21.17 秒
总而言之,仅比单线程版本提高了 30%。这有点令人失望,因为所有文件都在 SSD 上,而且我有 12 个逻辑处理器。当然,代码要复杂得多。
我错过了什么吗?也许我没有以最佳方式做到这一点?
解决方案
这种架构不是最优的,因为每个工作块,produceCSFiles
和produceMatchingLines
,都在做混合的 I/O-bound 和 CPU-bound 工作。理想情况下,您希望有一个块专门用于专门执行 I/O-bound,而另一个块专门执行 CPU-bound 工作。通过这种方式,您将能够根据相关硬件组件的功能优化配置每个块的并行度。使用您当前的配置,完全有可能在给定时刻两个块都在进行 I/O 工作,相互竞争 SSD 的注意力,而 CPU 则处于空闲状态。而在另一个时刻,可能会发生完全相反的情况。结果是混乱和不协调的喧嚣。这与使用单片机得到的结果相似Parallel.ForEach
循环,这可能会产生与单线程方法相当的(中等)性能改进。
您应该记住的其他一点是,当从一个块传递到另一个块的消息是大块时,TPL 数据流表现良好。正如介绍性文件所说:“为粗粒度数据流和流水线任务提供进程内消息传递”(强调添加)。如果每条消息的处理都过于轻量级,那么您最终会产生大量开销。如果需要,您可以通过使用BatchBlock<T>
s、Chunk
LINQ 运算符或其他方式对消息进行批处理,从而将工作负载分块。
说了这么多,我的假设是您的工作不成比例地受 I/O 限制,从而降低了 CPU 功能的相关性。老实说,即使使用最复杂的实现,我也不会期望性能大幅提升。
推荐阅读
- javascript - Zapier 操作从 API 保存文件
- bash - 每次循环运行保存输出文件
- automata - 可以在这个最小化的 DFA 中移除不可达状态吗?
- python - 优化 Pyspark 的 Collect_List 函数
- java - 从对象集合中找到最大数量
- c# - C# async await:如何为下面显示的示例编写异步方法?
- typescript - 类型安全从字符串分配给对象的键
- amazon-web-services - 重新处理 AWS SQS 死信队列消息
- java - 动态调整咖啡因缓存的大小
- ios - 无法将本地 html 文件加载到 ios 设备(不是模拟器)上的 WKWebView