C# 为什么这个System.IO.Pipelines代码比基于流的代码慢得多?
我编写了一个小的解析程序来比较.NETCore中较旧的C# 为什么这个System.IO.Pipelines代码比基于流的代码慢得多?,c#,performance,.net-core,system.io.pipelines,C#,Performance,.net Core,System.io.pipelines,我编写了一个小的解析程序来比较.NETCore中较旧的System.IO.Stream和较新的System.IO.Pipelines。我希望管道代码具有相同的速度或更快。但是,速度要慢40%左右 这个程序很简单:它在100Mb的文本文件中搜索关键字,并返回关键字的行号。以下是流版本: 公共静态异步任务GetLineNumberUsingStreamAsync( 字符串文件, 字符串(搜索词) { 使用var fileStream=File.OpenRead(文件); 使用var lines=ne
System.IO.Stream
和较新的System.IO.Pipelines
。我希望管道代码具有相同的速度或更快。但是,速度要慢40%左右
这个程序很简单:它在100Mb的文本文件中搜索关键字,并返回关键字的行号。以下是流版本:
公共静态异步任务GetLineNumberUsingStreamAsync(
字符串文件,
字符串(搜索词)
{
使用var fileStream=File.OpenRead(文件);
使用var lines=newstreamreader(fileStream,bufferSize:4096);
int lineNumber=1;
//ReadLineAsync在流结束时返回null,退出循环
while(等待行。ReadLineAsync()是字符串行)
{
if(行包含(搜索词))
返回行号;
lineNumber++;
}
返回-1;
}
我希望上面的流代码比下面的管道代码慢,因为流代码在StreamReader中将字节编码为字符串。管道代码通过对字节进行操作来避免这种情况:
公共静态异步任务GetLineNumberSingPipeAsync(字符串文件,字符串搜索字)
{
var searchBytes=Encoding.UTF8.GetBytes(searchWord);
使用var fileStream=File.OpenRead(文件);
var pipe=PipeReader.Create(fileStream,newstreampipereaderoptions(bufferSize:4096));
变量lineNumber=1;
while(true)
{
var readResult=await pipe.ReadAsync().ConfigureAwait(false);
var buffer=readResult.buffer;
if(TryFindBytesInBuffer(ref buffer、searchBytes、ref lineNumber))
{
返回行号;
}
管道推进(缓冲器端);
如果(readResult.IsCompleted)中断;
}
wait pipe.CompleteAsync();
返回-1;
}
以下是相关的帮助器方法:
//
///在'buffer'中查找'searchBytes',每隔一天递增'lineNumber'
///我们该换一条新路线了。
///
///如果我们找到searchBytes,则为true,否则为false
静态布尔函数TryFindBytesInBuffer(
ref只读序列缓冲区,
在ReadOnlySpan searchBytes中,
参考整数(行号)
{
var bufferReader=新的SequenceReader(缓冲区);
while(TryReadLine(ref bufferReader,out var line))
{
if(包含字节(参考行,搜索字节))
返回true;
lineNumber++;
}
返回false;
}
静态布尔传输线(
参考SequenceReader bufferReader,
输出只读序列行)
{
var foundNewLine=bufferReader.TryReadTo(输出行,(字节)'\n',AdvanceCastDelimiter:true);
如果(!foundNewLine)
{
行=默认值;
返回false;
}
返回true;
}
静态布尔包含字节(
ref ref ReadOnlySequence行,
在ReadOnlySpan searchBytes中)
{
返回新的SequenceReader(line).TryReadTo(out var u,searchBytes);
}
我使用上面的SequenceReader
,因为我的理解是它比ReadOnlySequence
更智能/更快;当它可以在单个Span
上操作时,它有一个快速路径
以下是基准测试结果(.NET Core 3.1)。完整的代码和BenchmarkDotNet结果可用
- GetLineNumberWithStreamAsync-435.6毫秒,同时分配366.19 MB
- GetLineNumberUsingPipeAsync-619.8 ms,同时分配9.28 MB
- GetLineNumberWithStreamAsync-452.2毫秒,同时分配366.19 MB
- GetLineNumberWithPipeAsync-203.8毫秒,分配9.28 MB
- 解决方案#1:683.7毫秒,365.84 MB
- 溶液#2:777.5毫秒,9.08 MB
- 这也许不是您想要的解释,但我希望它能提供一些见解:
浏览一下这里的两种方法,第二种解决方案中的两个嵌套循环在计算上比另一种更复杂
使用代码评测进行更深入的挖掘表明,第二个(GetLineNumberSingPipeAsync)比使用流的(请查看屏幕截图)高出近21.5%的CPU密集度,并且与我得到的基准测试结果非常接近:
我认为原因在于
SequenceReader.TryReadTo的实现。这种方法的优点。它使用非常简单的算法(读取第一个字节的匹配,然后检查匹配后的所有后续字节,如果不匹配,则向前推进1个字节并重复),并注意在这个实现中有相当多的方法称为“慢”(IsNextSlow
,TryReadToSlow
等等),因此,至少在某些情况下,在某些情况下,它会回到某种缓慢的道路上。它还必须处理序列可能包含多个段的事实,并保持位置
在您的情况下,您可以避免专门使用SequenceReader
来搜索匹配项(但将其留给实际读取行),例如,通过这种细微的更改(在这种情况下,TryReadTo
的过载也更有效):
private static bool TryReadLine(ref SequenceReader bufferReader,out ReadOnlySpan行){
//请注意,“match”和“line”现在都是“ReadOnlySpan”,而不是“ReadOnlySequence”`
var foundNewLine=bufferReader.TryReadTo(out ReadOnlySpan match,(byte)'\n',AdvanceCastDelimiter:true);
如果(!foundNewLine){
行=默认值;
返回false;
}
直线=匹配;
返回true;
}
然后:
private static bool包含字节(ref ref ReadOnlySpan line,在ReadOnlySpan searchBytes中){
//行现在是'ReadOnlySpan',所以我们可以使用有效的'IndexOf'方法
private static bool TryReadLine(ref SequenceReader<byte> bufferReader, out ReadOnlySpan<byte> line) {
// note that both `match` and `line` are now `ReadOnlySpan` and not `ReadOnlySequence`
var foundNewLine = bufferReader.TryReadTo(out ReadOnlySpan<byte> match, (byte) '\n', advancePastDelimiter: true);
if (!foundNewLine) {
line = default;
return false;
}
line = match;
return true;
}
private static bool ContainsBytes(ref ReadOnlySpan<byte> line, in ReadOnlySpan<byte> searchBytes) {
// line is now `ReadOnlySpan` so we can use efficient `IndexOf` method
return line.IndexOf(searchBytes) >= 0;
}