为什么 System/mscorlib 代码如此快?特别是对于循环?
-
23-08-2019 - |
题
这只是我一直在研究的一个个人项目。基本上,我使用 StreamReader 解析一个文本文件(比如从 20mb 到大约 1gb)。性能相当稳定,但仍然...我一直渴望看看如果我用二进制解析它会发生什么。别误会,我并不是过早地优化。我绝对是故意进行微观优化,只是为了“看看”。
因此,我正在使用字节数组读取文本文件。来看看,新行可以是(Windows)标准的 CR/LF 或 CR 或 LF...相当混乱。我本来希望能够在 CR 上使用 Array.IndexOf,然后跳过 LF。相反,我发现自己编写的代码与 IndexOf 非常相似,但检查其中任何一个并根据需要返回一个数组。
所以关键是:使用与 IndexOf 非常相似的代码,我的代码最终仍然慢得要命。使用 800mb 文件来透视它:
- 使用 IndexOf 并寻找 CR:〜320mb/s
- 使用 StreamReader 和 ReadLine:〜180MB/秒
- for 循环复制 IndexOf:〜150mb/s
这是带有 for 循环的代码(~150mb/s):
IEnumerator<byte[]> IEnumerable<byte[]>.GetEnumerator() {
using(FileStream fs = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, _bufferSize)) {
byte[] buffer = new byte[_bufferSize];
int bytesRead;
int overflowCount = 0;
while((bytesRead = fs.Read(buffer, overflowCount, buffer.Length - overflowCount)) > 0) {
int bufferLength = bytesRead + overflowCount;
int lastPos = 0;
for(int i = 0; i < bufferLength; i++) {
if(buffer[i] == 13 || buffer[i] == 10) {
int length = i - lastPos;
if(length > 0) {
byte[] line = new byte[length];
Array.Copy(buffer, lastPos, line, 0, length);
yield return line;
}
lastPos = i + 1;
}
}
if(lastPos > 0) {
overflowCount = bufferLength - lastPos;
Array.Copy(buffer, lastPos, buffer, 0, overflowCount);
}
}
}
}
这是更快的代码块(~320mb/s):
while((bytesRead = fs.Read(buffer, overflowCount, buffer.Length - overflowCount)) > 0) {
int bufferLength = bytesRead + overflowCount;
int pos = 0;
int lastPos = 0;
while(pos < bufferLength && (pos = Array.IndexOf<byte>(buffer, 13, pos)) != -1) {
int length = pos - lastPos;
if(length > 0) {
byte[] line = new byte[length];
Array.Copy(buffer, lastPos, line, 0, length);
yield return line;
}
if(pos < bufferLength - 1 && buffer[pos + 1] == 10)
pos++;
lastPos = ++pos;
}
if(lastPos > 0) {
overflowCount = bufferLength - lastPos;
Array.Copy(buffer, lastPos, buffer, 0, overflowCount);
}
}
(不,它还没有准备好生产,某些情况下会导致它爆炸;我使用 128kb 大小的缓冲区来忽略其中的大部分。)
所以我的大问题是...为什么 Array.IndexOf 工作得这么快?它本质上是相同的,都是一个遍历数组的 for 循环。mscorlib 代码的执行方式有什么问题吗?即使更改上面的代码以真正复制 IndexOf 并仅查找 CR,然后像我一样跳过 LF(如果使用 IndexOf 没有帮助的话)。呃……我一直在经历各种排列,但已经太晚了,也许我遗漏了一些明显的错误?
顺便说一句,我研究了 ReadLine 并注意到它使用 switch 块而不是 if 块......当我做类似的事情时,奇怪的是它确实将性能提高了大约 15mb/s。这是另一个问题(为什么 switch 比 if 更快?),但我想我应该指出我确实看过它。
另外,我正在 VS 之外测试发布版本,因此没有进行任何调试。
解决方案
这是个好问题。简而言之,这一切都归结为 IndexOf 将使用的 IEqualityComparer 的实现。让我们看下面的一段代码:
using System;
using System.Collections.Generic;
using System.Diagnostics;
class Program {
static int [] buffer = new int [1024];
const byte mark = 42;
const int iterations = 10000;
static void Main ()
{
buffer [buffer.Length -1] = mark;
Console.WriteLine (EqualityComparer<int>.Default.GetType ());
Console.WriteLine ("Custom: {0}", Time (CustomIndexOf));
Console.WriteLine ("Builtin: {0}", Time (ArrayIndexOf));
}
static TimeSpan Time (Action action)
{
var watch = new Stopwatch ();
watch.Start ();
for (int i = 0; i < iterations; i++)
action ();
watch.Stop ();
return watch.Elapsed;
}
static void CustomIndexOf ()
{
for (int i = 0; i < buffer.Length; i++)
if (buffer [i] == mark)
break;
}
static void ArrayIndexOf ()
{
Array.IndexOf (buffer, mark);
}
}
你需要编译它 csc /优化+.
这是我得到的结果:
C:\Tmp>test
System.Collections.Generic.GenericEqualityComparer`1[System.Int32]
Custom: 00:00:00.0386403
Builtin: 00:00:00.0427903
现在,将数组和 EqualityComparer 的类型更改为字节,这是我得到的结果:
C:\Tmp>test
System.Collections.Generic.ByteEqualityComparer
Custom: 00:00:00.0387158
Builtin: 00:00:00.0165881
正如您所看到的,字节数组是特殊情况,它可能经过优化以在字节数组中查找字节。由于我无法反编译.net框架,所以我在这里停止了分析,但我想这是一个很好的线索。
其他提示
mscorlib 文件在安装过程中生成。尝试使用 Ngen.exe 实用程序对文件进行 ngen 处理(我想是与 .NET 框架一起提供的)...然后检查基准。可以稍微快一点。
为了使您的 .NET 代码以接近本机的速度运行,Microsoft 建议您在应用程序安装过程中“Ngen”您的代码...