4

不同大小的缓冲区对 MD5 计算速度的影响 - Scighost

 1 year ago
source link: https://www.cnblogs.com/scighost/p/17471908.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

不同大小的缓冲区对 MD5 计算速度的影响

最*需要在计算大文件的 MD5 值时显示进度,于是我写了如下的代码:

public long Length {get; private set; }

public long Position { get; private set; }

public async Task ComputeMD5Async(string file, CancellationToken cancellationToken)
{
    using var fs = File.OpenRead(file);
    Length = fs.Length;
    var task = MD5.HashDataAsync(fs, cancellationToken);
    var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(10));
    while (await timer.WaitForNextTickAsync(cancellationToken))
    {
        Position = fs.Position;
        if (task.IsCompleted)
        {
            break;
        }
    }
}

运行的时候发现不对劲儿了,我的校验速度只能跑到 350MB/s,而别人的却能跑到 500MB/s,相同的设备怎么差距有这么大?带这个疑问我去看了看别人的源码,发现是这么写的:

protected long _progressPerFileSizeCurrent;

protected byte[] CheckHash(Stream stream, HashAlgorithm hashProvider, CancellationToken token)
{
    byte[] buffer = new byte[1 << 20];
    int read;
    while ((read = stream.Read(buffer)) > 0)
    {
        token.ThrowIfCancellationRequested();
        hashProvider.TransformBlock(buffer, 0, read, buffer, 0);
        _progressPerFileSizeCurrent += read;
    }
    hashProvider.TransformFinalBlock(buffer, 0, read);
    return hashProvider.Hash;
}

这里使用了 HashAlgorithm.TransformBlock 方法,它能计算输入字节数组指定区域的哈希值,并将中间结果暂时存储起来,最后再调用 HashAlgorithm.TransformFinalBlock 结束计算。上述代码中缓冲区 buffer 大小是 1MB,我敏锐地察觉到 MD5 计算速度可能与这个值有关,接着我又去翻了翻 MD5.HashDataAsync 的源码。

// System.Security.Cryptography.LiteHashProvider
private static async ValueTask<int> ProcessStreamAsync<T>(T hash, Stream source, Memory<byte> destination, CancellationToken cancellationToken) where T : ILiteHash
{
    using (hash)
    {
        byte[] rented = CryptoPool.Rent(4096);

        int maxRead = 0;
        int read;

        try
        {
            while ((read = await source.ReadAsync(rented, cancellationToken).ConfigureAwait(false)) > 0)
            {
                maxRead = Math.Max(maxRead, read);
                hash.Append(rented.AsSpan(0, read));
            }

            return hash.Finalize(destination.Span);
        }
        finally
        {
            CryptoPool.Return(rented, clearSize: maxRead);
        }
    }
}

源码中最关键的是上面这部分,缓冲区 rented 设置为 4KB,与 1MB 相差甚远,原因有可能就在这里。

为了找到最佳的缓冲区值,我跑了一大堆 BenchMark,覆盖了从 32B 到 64MB 的范围。没什么技术含量,但工作量实在不小。测试使用 1GB 的文件,基准测试是对 1GB 大小的数组直接调用 MD5.HashData,实际的测试代码如下,分别使用内存流 MemoryStream 和文件流 FileStream 作为入参 Stream,对比无硬盘 IO 和实际读取文件的速度。

public async Task HashDataAsync(Stream stream)
{
    var hash = MD5.Create();
    byte[] buffer = new byte[1 << size];
    int read = 0;
    while ((read = await stream.ReadAsync(buffer)) != 0)
    {
        hash.TransformBlock(buffer, 0, read, buffer, 0);
    }
    hash.TransformFinalBlock(buffer, 0, read);
    if (!(hash.Hash?.SequenceEqual(fileHash) ?? false))
    {
        throw new Exception("Compute error");
    }
}

img

基准测试是那条红色虚线,是所有测试结果中最快的。橙色的曲线是 MemoryStream 的测试结果,在缓存块的 2KB 处降到了一个较低的位置,后续耗时无明显下降。这证明 .NET 源码中使用 4KB 大小的块是一个合理的选择,但是它没有考虑文件 IO 的延迟影响。蓝色的曲线是最接*显示的测试结果,缓存块大于 32KB 时的测试结果才接*于*稳。

总结一下,MD5.HashDataAsync 过慢的原因是文件 IO 影响到了计算速度。使用文件流进行 MD5 校验的时候,缓冲区至少需要 64KB,总体速度才不会被文件 IO 拖后腿。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK