【发布时间】:2021-01-24 18:21:32
【问题描述】:
我编写了一个小解析程序来比较 .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 毫秒,同时分配 366.19 MB
- GetLineNumberUsingPipeAsync - 619.8 毫秒,同时分配 9.28 MB
我在管道代码中做错了吗?
更新:Evk 已经回答了这个问题。应用他的修复后,以下是新的基准数字:
- GetLineNumberWithStreamAsync - 452.2 毫秒,同时分配 366.19 MB
- GetLineNumberWithPipeAsync - 203.8 毫秒,但分配了 9.28 MB
【问题讨论】:
-
进行内存转储并查看您正在创建的对象数量可能是有益的。
-
感谢@IanKemp。我在重复运行时捕获了几个内存转储,并且管道代码在恒定内存中运行。没什么可疑的,主要是 FileStream 和数组池的内部细节。我尝试预先读取 MemoryStream 以避免使用 FileStream,但基准测试结果相似。
-
我猜问题可能出在搜索算法上,string.Contains 使用了高级搜索算法,而 TryReadTo 使用了简单的 o(n*m) 解决方案
-
感谢您使用新的基准更新您的问题!
标签: c# performance .net-core system.io.pipelines