ByteArrayの比較コード

5文字程度のUtf8のbyte[]のコマンドを、どのコマンドか調べるために比較コードを書いたりします。
Stringにしてみたりとか、switch/ifとか、SequenceEqual使ったりとか。
書きやすさ、読みやすさ、パフォーマンスを全体的に見ていきます。

なお、環境は.net7.0のC#11
X64(AMD Ryzen5),Windows11です。

まずは、ベンチマークの結果を載せておきます。

|                        Method |      Mean |    Error |   StdDev |
|------------------------------ |----------:|---------:|---------:|
|             StringSwitchBench |  83.89 ns | 0.829 ns | 0.776 ns |
|                 StringIfBench |  83.07 ns | 0.559 ns | 0.495 ns |
| StaticBytesSequenceEqualBench | 211.46 ns | 1.032 ns | 0.966 ns |
|   StaticRosSequenceEqualBench |  24.94 ns | 0.085 ns | 0.066 ns |
|      StaticBytesMyEqualsBench |  38.61 ns | 0.315 ns | 0.279 ns |
|StaticBytesMyEqualsInLineBench |  21.66 ns | 0.418 ns | 0.391 ns |
|                  U8BytesBench |  20.97 ns | 0.301 ns | 0.282 ns |

なんか、SequenceEqualで遅いのが一つあります。
他には、やはりStringはちょっと遅そうですね。

ベンチマークは共通コードとして以下を用意しました。

static byte[] _start = "start"u8.ToArray();
static byte[] _stop = "stop"u8.ToArray();
static byte[] _show = "show"u8.ToArray();
static byte[] _quit = "quit"u8.ToArray();

これは、C#11じゃなくて、u8が使えない場合はちょっと遅いけど以下でも同じです。

static byte[] _quit = Encoding.UTF8.GetBytes("quit");

では初めのStringSwitchBenchです。

[Benchmark]
public int StringSwitchBench()
{
    int sum = 0;
    sum += AnalyzeStringSwitch(_start);
    sum += AnalyzeStringSwitch(_show);
    sum += AnalyzeStringSwitch(_stop);
    sum += AnalyzeStringSwitch(_quit);

    return sum;
}

public int AnalyzeStringSwitch(byte[] command)
{
    var commandString = Encoding.UTF8.GetString(command);
    switch(commandString)
    {
        case "start":
            return 1;
        case "show":
            return 2;
        case "stop":
            return 3;
        case "quit":
            return 4;
        default:
            return 0;
    }
}

さて、はじめにベンチマークの内容を書いておきます。
byte[]のUtf8文字列を、startとかquitとかの何かを判定して、intで返してます。
sumしているのはベンチマークの都合だけで、意味は無いです。

StringSwitchBenchは、utf8のbyte[]を、はじめにStringに変換して、それをswitchしているだけですね。
83nsとやや遅いのは、変換作業と、Stringのオブジェクトを1つ作っているためだと思います。
読みやすさ、書きやすさはいいですね。

さて、次は、StringIfBenchです。

[Benchmark]
public int StringIfBench()
{
    int sum = 0;
    sum += AnalyzeStringIf(_start);
    sum += AnalyzeStringIf(_show);
    sum += AnalyzeStringIf(_stop);
    sum += AnalyzeStringIf(_quit);

    return sum;
}

public int AnalyzeStringIf(byte[] command)
{
    var commandString = Encoding.UTF8.GetString(command);

    if(commandString == "start") { return 1; }
    if(commandString == "show") { return 2; }
    if(commandString == "stop") { return 3; }
    if(commandString == "quit") { return 4; }
    return 0;
}

switchだったのを、ifに変えただけですね。
コマンドの種類が30とか100とか増えてくると変わるかもしれませんが、4つくらいだと速度はほぼ変わりません。
読み書きも、switchと大差は無いと思います。

次は、StaticBytesSequenceEqualBenchです。

[Benchmark]
public int StaticBytesSequenceEqualBench()
{
    int sum = 0;
    sum += AnalyzeStaticBytesSequenceEqual(_start);
    sum += AnalyzeStaticBytesSequenceEqual(_show);
    sum += AnalyzeStaticBytesSequenceEqual(_stop);
    sum += AnalyzeStaticBytesSequenceEqual(_quit);

    return sum;
}

public int AnalyzeStaticBytesSequenceEqual(byte[] command)
{
    if (command.SequenceEqual(_start)) { return 1; }
    if (command.SequenceEqual(_show)) { return 2; }
    if (command.SequenceEqual(_stop)) { return 3; }
    if (command.SequenceEqual(_quit)) { return 4; }
    return 0;
}

staticで、事前に用意しておいたbyte[]とSequenceEqualで比較します。
これが今回の一番遅い結果になりました。
理由を調べてみると、Enumerable.SequenceEqualが呼び出されています。
中で、前提チェックしてエラーはいたり、タイプで分岐したりと何かと忙しそうです。
元からbyte[]とわかっているなら不要な処理がたくさんありそうなので、そのせいで遅いのでしょう。
別の場所に、比較用の_startとかを用意しておく必要があるのは、メンテナンス面でマイナスです。

次はStaticRosSequenceEqualBenchです。

[Benchmark]
public int StaticRosSequenceEqualBench()
{
    int sum = 0;
    sum += AnalyzeStaticRosSequenceEqual(_start);
    sum += AnalyzeStaticRosSequenceEqual(_show);
    sum += AnalyzeStaticRosSequenceEqual(_stop);
    sum += AnalyzeStaticRosSequenceEqual(_quit);

    return sum;
}

public int AnalyzeStaticRosSequenceEqual(byte[] command)
{
    var command2 = new ReadOnlySpan<byte>(command);
    if (command2.SequenceEqual(_start)) { return 1; }
    if (command2.SequenceEqual(_show)) { return 2; }
    if (command2.SequenceEqual(_stop)) { return 3; }
    if (command2.SequenceEqual(_quit)) { return 4; }
    return 0;
}

byte[]を、ReadOnlySpanに変えてから、SequenceEqualを呼び出しました。
かなり速いですね。
問題は、byte[]と同じSequenceEqualを使うので、ついうっかりbyte[]のを呼んで遅くなるのが無ければ大丈夫です。
別の場所に、比較用の_startとかを用意しておく必要があるのは、メンテナンス面でマイナスです。

次に、StaticBytesMyEqualsBenchです。

[Benchmark]
public int StaticBytesMyEqualsBench()
{
    int sum = 0;
    sum += AnalyzeStaticBytesMyEquals(_start);
    sum += AnalyzeStaticBytesMyEquals(_show);
    sum += AnalyzeStaticBytesMyEquals(_stop);
    sum += AnalyzeStaticBytesMyEquals(_quit);

    return sum;
}

public int AnalyzeStaticBytesMyEquals(byte[] command)
{
    if (MyEquals1(command,_start)) { return 1; }
    if (MyEquals1(command,_show)) { return 2; }
    if (MyEquals1(command,_stop)) { return 3; }
    if (MyEquals1(command,_quit)) { return 4; }
    return 0;
}

public bool MyEquals1(ReadOnlySpan<byte> a, ReadOnlySpan<byte> b)
{
    if (a.Length != b.Length) { return false; }
    for (int i = 0; i < a.Length; i++)
    {
        if (a[i] != b[i]) { return false; }
    }
    return true;
}

byte[]のSequenceEqualが遅いので、自分で比較コードを書いたケースです。
速度は、まあまあですね。
なんでStaticRosSequenceEqualBenchより遅くなるんでしょうね。
なぞですね。え?きちんと調べろって。

調べてみました。
RosのSequenceEqualには、以下のオプションがついていました。

[MethodImpl(MethodImplOptions.AggressiveInlining)]

インライン化されていたので、速いんでしょうね。
MyEqualsに同じの追加したら、速くなりました。それがStaticBytesMyEqualsInLineBenchです。
なお、上のと同じく、別の場所に、比較用の_startとかを用意しておく必要があるのは、メンテナンス面でマイナスです。

さて最後に、U8BytesBenchです。

[Benchmark]
public int U8BytesBench()
{
    int sum = 0;
    sum += AnalyzeU8Bytes(_start);
    sum += AnalyzeU8Bytes(_show);
    sum += AnalyzeU8Bytes(_stop);
    sum += AnalyzeU8Bytes(_quit);

    return sum;
}

public int AnalyzeU8Bytes(byte[] command)
{
    if ("start"u8.SequenceEqual(command)) { return 1; }
    if ("show"u8.SequenceEqual(command)) { return 2; }
    if ("stop"u8.SequenceEqual(command)) { return 3; }
    if ("quit"u8.SequenceEqual(command)) { return 4; }
    return 0;
}

今回のコードでは、これが最速。
別にbyte[]を用意したりする必要もないので、メモリ的にも、書きやすさ・読みやすさ的にもOKです。
まあ、今回の記事は、これが本当にベストなのか確認したかったということなわけですが。

投稿日:
カテゴリー: C#

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です