3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】Stream.Read/Write(Span<byte>) のパフォーマンス

Last updated at Posted at 2025-01-26

はじめに

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
  • 概ね読み取りと同じような傾向です
  • 書き込みは読み取りに対してかなりコストが大きいです

おわりに

当初の予想通りストリームの読み書きメソッドのオーバーロードは若干パフォーマンスに違いがありましたが、気にするほどではなさそうです。

やはり標準ライブラリはかなりパフォーマンスよく設計されているため、頼もしさを再確認できました。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?