🌊

System.IO.HashingのxxHashを使っててハマった話

2024/04/15に公開

概要

ファイルコピーにおけるチェックサムとしてMD5を使っていたのですが、xxHashを使ったらどれくらい速くなるのか試してみたくなり使ってみた所、進捗をレポートする処理を入れようとするとハッシュが不正になってしまう現象に遭遇しました。色々調査した結果解決できたので共有します。

結論

https://stackoverflow.com/questions/75750329/how-to-calculate-the-progress-of-hashing-system-io-hashing
やりたかったのはこれなのですがコメントを見て「読み込みデータだけじゃなくてバッファを丸ごと送っている事が原因」である事が分かったので、AsSpanを使って読み込んだデータだけ送るようにすれば直りました。

private byte[] GetXxHash3(string filename)
{
    var hashAlgorithm = new XxHash3();
    var bufferSize = 1024 * 1024; // 1MB
    var buffer = new byte[bufferSize];
    using (var entryStream = System.IO.File.OpenRead(filename))
    {
        var bytesRead = entryStream.Read(buffer, 0, buffer.Length);
        while (bytesRead > 0)
        {
            // AsSpanを使って読み込んだデータだけAppendする。
            hashAlgorithm.Append(buffer.AsSpan(0, bytesRead));
            // 元々は以下のように書いてた。
            // hashAlgorithm.Append(buffer);
        }
    }
    return hashAlgorithm.GetHashAndReset());
}

おそらくStream.Readの仕様が変わった事が原因です。
https://learn.microsoft.com/ja-jp/dotnet/core/compatibility/core-libraries/6.0/partial-byte-reads-in-streams

これ結構影響のある変更なんじゃないかなと思うんですが、つまりバッファの中身が埋まる保証が無くなっています。
entryStream.Read(buffer)を実行してバッファが埋まる前に読み取りが終わってしまった状態で、hashAlgorithm.Append(buffer);みたいにバッファを全部渡してしまうと、間違ったデータを含んだバッファを渡す事になるのでハッシュが不正確になります。
なのでhashAlgorithm.Append(buffer.AsSpan(0, bytesRead));という感じで、読み込んだデータだけを指定すれば正確なハッシュが計算されます。

経緯

元々MD5でチェックサム機能を実装していた時はTransformBlockTransformFinalBlockを使って進捗レポートを送りながらハッシュを計算しつつ、プログレスバーを伸ばしたり転送速度を表示していました。まぁ普通の処理です。
ですがストレージ速度が上がってるのにMD5の影響で速度が活かせていない事に気づき、高速なxxHashに切り替えたらどれくらい速くなるのかが気になってやってみようと思いました。まずは簡単なCLIで検証してみた所、わずか150MBほどのファイルのハッシュ値を計算するだけでも、xxhashはMD5の10分の1の速度で計算が終わりました。
これは期待できると思い、いざxxHashを組み込む為にライブラリをいろいろ探っていた時、.NET Platform Extensionsで公式から配布されている事が分かり、導入してみました。NuGetで配布されています。
https://www.nuget.org/packages/System.IO.Hashing/
netstandard2.0で実装されているので幅広く導入できます。

これを導入して色々使い方を調べたら、どうやらAppendメソッドを使えば進捗レポートを送りつつハッシュ計算ができるのでは?と思って作っていました。
ただこれだと全然違うハッシュが生成されてしまうので更に調べまくった結果、バッファをそのままAppendメソッドに送ってはいけないという事にたどり着きました。

注意点

このxxHashをMAUIで使うと極端に遅くなるという現象が起きます。issueで報告したところどうやらコード生成の部分に原因があるようで、.NET 10で改善されるみたいです。
https://github.com/dotnet/runtime/issues/103539

Discussion