• 不同大小的缓冲区对 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 拖后腿。

  • 相关阅读:
    金南瓜科技SECS/GEM:引领智能制造新潮流
    ZZULIOJ:1191-1199
    CodeForces..构建美丽数组.[简单].[情况判断].[特殊条件下的最小值奇偶问题]
    计算机毕业设计springboot+vue基本安卓/微信小程序的驾校考试预约系统 uniapp
    Linux安装Jenkins
    SpringBoot整合JUnit、MyBatis、MyBatis-plus、Druid详解
    基于SSM的学生信息管理系统
    别的电脑访问不到我本机上的gitea是为什么呀?
    线程间的通讯
    ClickHouse主键索引最佳实践
  • 原文地址:https://www.cnblogs.com/scighost/p/17471908.html