🌊

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

に公開

概要

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

結論

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

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/

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

注意点

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

Discussion