问题描述
我编写了一个小程序来比较.NET Core中较旧的System.IO.Stream
和较新的system.io.pipelines
。我期望管道代码具有相同的速度或更快的速度。但是,速度要慢40%。
程序很简单:它在100Mb的文本文件中搜索关键字,然后返回关键字的行号。这是Stream版本:
public static async Task<int> GetLineNumberUsingStreamAsync(
string file,string searchWord)
{
using var fileStream = File.OpenRead(file);
using var lines = new StreamReader(fileStream,bufferSize: 4096);
int lineNumber = 1;
// ReadLineAsync returns null on stream end,exiting the loop
while (await lines.ReadLineAsync() is string line)
{
if (line.Contains(searchWord))
return lineNumber;
lineNumber++;
}
return -1;
}
我希望上面的流代码比下面的流水线代码慢,因为流代码将字节编码为StreamReader中的字符串。管道代码通过对字节进行操作来避免这种情况:
public static async Task<int> GetLineNumberUsingPipeAsync(string file,string searchWord)
{
var searchBytes = Encoding.UTF8.GetBytes(searchWord);
using var fileStream = File.OpenRead(file);
var pipe = PipeReader.Create(fileStream,new StreamPipeReaderOptions(bufferSize: 4096));
var lineNumber = 1;
while (true)
{
var readResult = await pipe.ReadAsync().ConfigureAwait(false);
var buffer = readResult.Buffer;
if(TryFindBytesInBuffer(ref buffer,searchBytes,ref lineNumber))
{
return lineNumber;
}
pipe.Advanceto(buffer.End);
if (readResult.IsCompleted) break;
}
await pipe.CompleteAsync();
return -1;
}
以下是相关的辅助方法:
/// <summary>
/// Look for `searchBytes` in `buffer`,incrementing the `lineNumber` every
/// time we find a new line.
/// </summary>
/// <returns>true if we found the searchBytes,false otherwise</returns>
static bool TryFindBytesInBuffer(
ref ReadOnlySequence<byte> buffer,in ReadOnlySpan<byte> searchBytes,ref int lineNumber)
{
var bufferReader = new SequenceReader<byte>(buffer);
while (TryReadLine(ref bufferReader,out var line))
{
if (ContainsBytes(ref line,searchBytes))
return true;
lineNumber++;
}
return false;
}
static bool TryReadLine(
ref SequenceReader<byte> bufferReader,out ReadOnlySequence<byte> line)
{
var foundNewLine = bufferReader.TryReadTo(out line,(byte)'\n',advancePastDelimiter: true);
if (!foundNewLine)
{
line = default;
return false;
}
return true;
}
static bool ContainsBytes(
ref ReadOnlySequence<byte> line,in ReadOnlySpan<byte> searchBytes)
{
return new SequenceReader<byte>(line).TryReadTo(out var _,searchBytes);
}
我在上面使用SequenceReader<byte>
是因为我的理解是,它比ReadOnlySequence<byte>
更智能/更快;当它可以在单个Span<byte>
上运行时,它具有一条快捷路径。
以下是基准测试结果(.NET Core 3.1)。完整的代码和BenchmarkDotNet结果可用in this repo。
- GetLineNumberWithStreamAsync- 435.6 ms ,同时分配366.19 MB
- GetLineNumberUsingPipeAsync- 619.8 ms ,同时分配9.28 MB
我在管道代码中做错了吗?
更新:Evk回答了该问题。应用他的修复后,这是新的基准数字:
- GetLineNumberWithStreamAsync- 452.2毫秒,同时分配366.19 MB
- GetLineNumberWithPipeAsync- 203.8毫秒,而分配了9.28 MB
解决方法
我相信原因是SequenceReader.TryReadTo
的实现。 Here is the source code这种方法。它使用了非常简单的算法(读取到第一个字节的匹配项,然后检查该匹配项之后的所有后续字节,如果不匹配,则前进1个字节并重复),并注意在此实现中有多少方法称为“慢速” (IsNextSlow
,TryReadToSlow
等),因此至少在某些情况下,在某些情况下,它会退回到某些缓慢的路径。它还必须处理可能包含多个段的事实序列并保持位置。
在您的情况下,您可以避免使用SequenceReader
来专门搜索匹配项(但保留它以实际读取行),例如,进行较小的更改(TryReadTo
的重载在这种情况):
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;
}
这将使您的管道代码比流一运行得快。
,这可能不完全是您要寻找的解释,但我希望它能提供一些见解:
浏览一下您所拥有的两种方法,它表明在第二种解决方案中,由于具有两个嵌套循环,因此在计算上比另一种更为复杂。
使用代码概要分析进行更深入的研究表明,第二个(GetLineNumberUsingPipeAsync)的CPU密集度比使用Stream的第二个(请检查屏幕截图)高出21.5%,它与我得到的基准测试结果非常接近:>
-
解决方案#1:683.7 ms,365.84 MB
-
解决方案2:777.5毫秒,9.08 MB