はじめに
Span<T>
が登場した頃、ストリームの読み書きにスパンを引数に取るオーバーロードが追加されました。
スパンを引数に取るということで、既存の byte[]
を引数に取る元々のメソッドよりパフォーマンスがいいのかなと漠然と考えていましたが、実装を見てみるとそうでもなさそうです。
というわけで、今回はこれのパフォーマンスを検証してみます。
各ストリームの実装
System.IO.Stream.Read(Span<byte>)
public virtual int Read(Span<byte> buffer)
{
byte[] sharedBuffer = ArrayPool<byte>.Shared.Rent(buffer.Length);
try
{
int numRead = Read(sharedBuffer, 0, buffer.Length);
if ((uint)numRead > (uint)buffer.Length)
{
throw new IOException(SR.IO_StreamTooLong);
}
new ReadOnlySpan<byte>(sharedBuffer, 0, numRead).CopyTo(buffer);
return numRead;
}
finally
{
ArrayPool<byte>.Shared.Return(sharedBuffer);
}
}
Span<byte>
を引数に取るオーバーロードの基本的な実装は ↑ で、virtual
というのがミソです。つまり派生クラスでオーバーライドされなければ、これが呼び出されます。
- 共有バッファの取得
- バッファのコピー
- 共有バッファの返却
の処理があるため、少しパフォーマンスが低くなりそうです。共有バッファに関しては排他処理が必要ですし、どの程度のコストになるかも気になります。
System.IO.MemoryStream.Read(Span<byte>)
public override int Read(Span<byte> buffer)
{
if (GetType() != typeof(MemoryStream))
{
// MemoryStream is not sealed, and a derived type may have overridden Read(byte[], int, int) prior
// to this Read(Span<byte>) overload being introduced. In that case, this Read(Span<byte>) overload
// should use the behavior of Read(byte[],int,int) overload.
return base.Read(buffer);
}
EnsureNotClosed();
int n = Math.Min(_length - _position, buffer.Length);
if (n <= 0)
return 0;
new Span<byte>(_buffer, _position, n).CopyTo(buffer);
_position += n;
return n;
}
MemoryStream
はメソッドをオーバーライドしてあり、専用の実装になっています。見た感じパフォーマンスは byte[]
と同程度になりそうです。
System.IO.FileStream.Read(Span<byte>)
public override int Read(Span<byte> buffer) => _strategy.Read(buffer);
FileStream
もメソッドをオーバーライドしてあり、専用の実装になっています。見た感じパフォーマンスは byte[]
と同程度になりそうです。
パフォーマンス検証
前段を踏まえてパフォーマンスを検証してみます。
テストコード
using System.IO.Compression;
public class __StreamPerformanceTest
{
private static ReadOnlySpan<byte> Source => """
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>
"""u8;
static void Read256Bytes(Performance p)
{
var buffer = new byte[256];
var memoryStream = new MemoryStream(Source.ToArray());
p.AddTest("MemoryStream:Array", () =>
{
memoryStream.Position = 0;
_ = memoryStream.Read(buffer, 0, 256);
});
p.AddTest("MemoryStream:Span", () =>
{
memoryStream.Position = 0;
_ = memoryStream.Read(buffer.AsSpan());
});
File.WriteAllBytes("__TMP__", Source);
var fileStream = File.OpenRead("__TMP__");
p.AddTest("FileStream:Array", () =>
{
fileStream.Position = 0;
_ = fileStream.Read(buffer, 0, 256);
});
p.AddTest("FileStream:Span", () =>
{
fileStream.Position = 0;
_ = fileStream.Read(buffer.AsSpan());
});
Program.Exit += () =>
{
fileStream.Dispose();
File.Delete("__TMP__");
};
var zipStream = new MemoryStream();
using (var zip = new ZipArchive(zipStream, ZipArchiveMode.Create, true))
{
var entry = zip.CreateEntry("test");
using var entryStream = entry.Open();
entryStream.Write(Source);
}
p.AddTest("ZipStream:Array", () =>
{
zipStream.Position = 0;
using var zip = new ZipArchive(zipStream, ZipArchiveMode.Read, true);
var entry = zip.GetEntry("test")!;
using var entryStream = entry.Open();
_ = entryStream.Read(buffer, 0, 256);
});
p.AddTest("ZipStream:Span", () =>
{
zipStream.Position = 0;
using var zip = new ZipArchive(zipStream, ZipArchiveMode.Read, true);
var entry = zip.GetEntry("test")!;
using var entryStream = entry.Open();
_ = entryStream.Read(buffer.AsSpan());
});
}
static void Write256Bytes(Performance p)
{
var buffer = new byte[256];
Source[..buffer.Length].CopyTo(buffer);
p.AddTest("MemoryStream:Array", () =>
{
using var memoryStream = new MemoryStream();
memoryStream.Write(buffer, 0, 256);
});
p.AddTest("MemoryStream:Span", () =>
{
using var memoryStream = new MemoryStream();
memoryStream.Write(Source[..256]);
});
p.AddTest("FileStream:Array", () =>
{
using var fileStream = new FileStream("__TMP2__", FileMode.Create);
fileStream.Write(buffer, 0, 256);
});
p.AddTest("FileStream:Span", () =>
{
using var fileStream = new FileStream("__TMP3__", FileMode.Create);
fileStream.Write(Source[..256]);
});
p.AddTest("WriteAllBytes:Array", () =>
{
File.WriteAllBytes("__TMP4__", buffer);
});
p.AddTest("WriteAllBytes:Span", () =>
{
File.WriteAllBytes("__TMP5__", Source[..256]);
});
Program.Exit += () =>
{
File.Delete("__TMP2__");
File.Delete("__TMP3__");
File.Delete("__TMP4__");
File.Delete("__TMP5__");
};
p.AddTest("ZipStream:Array", () =>
{
using var zipStream = new MemoryStream();
using var zip = new ZipArchive(zipStream, ZipArchiveMode.Create);
var entry = zip.CreateEntry("test")!;
using var entryStream = entry.Open();
entryStream.Write(buffer, 0, 256);
});
p.AddTest("ZipStream:Span", () =>
{
using var zipStream = new MemoryStream();
using var zip = new ZipArchive(zipStream, ZipArchiveMode.Create);
var entry = zip.CreateEntry("test")!;
using var entryStream = entry.Open();
entryStream.Write(Source[..256]);
});
}
}
Test | Score | % | CG0 |
---|---|---|---|
Read256Bytes (6) | |||
MemoryStream:Array | 2,072,848 | 100.0% | 0 |
MemoryStream:Span | 1,749,351 | 84.4% | 0 |
FileStream:Array | 1,522,456 | 73.4% | 0 |
FileStream:Span | 1,464,853 | 70.7% | 0 |
ZipStream(Memory):Array | 21,206 | 1.0% | 3 |
ZipStream(Memory):Span | 21,520 | 1.0% | 3 |
実行環境: Windows11 x64 .NET Runtime 9.0.0
Score は高いほどパフォーマンスがよいです。
GC0 はガベージコレクション回数を表します(少ないほどパフォーマンスがよい)。
-
MemoryStream
の場合、[]byte
のほうがSpan<byte>
より若干パフォーマンスが良いです。MemoryStream
を使う場面は結構限定的なので、あまり気にする必要はないかもしれません -
FileStream
の場合、[]byte
のほうがSpan<byte>
よりわずかにパフォーマンスが良いです。とはいえ、誤差レベルなのであまり気にする必要はなさそうです - 圧縮ファイルの場合、両者にパフォーマンスの違いはあまりないです
Test | Score | % | CG0 |
---|---|---|---|
Write256Bytes (8) | |||
MemoryStream:Array | 1,187,451 | 100.0% | 48 |
MemoryStream:Span | 1,035,178 | 87.2% | 42 |
FileStream:Array | 392 | 0.0% | 0 |
FileStream:Span | 380 | 0.0% | 0 |
WriteAllBytes:Array | 377 | 0.0% | 0 |
WriteAllBytes:Span | 374 | 0.0% | 0 |
ZipStream(Memory):Array | 9,980 | 0.8% | 2 |
ZipStream(Memory):Span | 9,940 | 0.8% | 2 |
- 概ね読み取りと同じような傾向です
- 書き込みは読み取りに対してかなりコストが大きいです
おわりに
当初の予想通りストリームの読み書きメソッドのオーバーロードは若干パフォーマンスに違いがありましたが、気にするほどではなさそうです。
やはり標準ライブラリはかなりパフォーマンスよく設計されているため、頼もしさを再確認できました。