Optymalizacja obliczania funkcji skrótu w wątkach

1

Hej
Miałbym do Was pytanie. Używam sobie w swojej aplikacji kilku funkcji skrótu (sha1, md5, crc32, sha256, sha384, sha512) które obliczam dla danego pliku. Pliki są zazwyczaj dość dużych rozmiarów(powyżej 1GB). Pomyślałem, że używając wątki można nieco przyśpieszyć ich liczenie. Znalazłem wcześniej program HashMyFiles napisany prawdopodobnie w C++ więc w sumie mógł mi się przydać i tak wcześniej obliczałem te skróty. Zależało mi natomiast na możliwości monitorowania aktualnego postępu dla pliku przy liczeniu tych hashy. W tym momencie mój kawałek kodu wygląda następująco:

public static string[] Checksums(FileStream fileStream)
        {
            int percentOfDone = 0, lastPercentOfDone = 0;
            sha256 = new SHA256Managed();
            sha512 = new SHA512Managed();
            sha384 = new SHA384Managed();
            md5 = MD5.Create();
            sha1 = new SHA1Managed();
            crc32 = new CRC32();
            byte[] buff = new byte[BufferLength];
            b1 = new byte[BufferLength];
            b2 = new byte[BufferLength];
            Thread t1 = null;
            Thread t2 = null;
            while (fileStream.Length - fileStream.Position > BufferLength)
            {
                OnEvent(new BlockCountEventArgs((int)(fileStream.Position * 100 / fileStream.Length)));
                fileStream.Read(buff, 0, BufferLength);
                if (t1 != null)
                    t1.Join();
                if (t2 != null)
                    t2.Join();

                buff.CopyTo(b1, 0);
                t1 = new Thread(new ThreadStart(() => {
                    sha256.TransformBlock(HashesCalculator.b1, 0, BufferLength, null, 0);
                    sha512.TransformBlock(HashesCalculator.b1, 0, BufferLength, null, 0);
                    crc32.TransformBlock(HashesCalculator.b1, 0, BufferLength, null, 0);
                }));
                t1.Start();

                buff.CopyTo(b2, 0);
                t2 = new Thread(new ThreadStart(() =>
                {
                    sha1.TransformBlock(HashesCalculator.b2, 0, BufferLength, null, 0);
                    sha384.TransformBlock(HashesCalculator.b2, 0, BufferLength, null, 0);
                }));
                t2.Start();

                md5.TransformBlock(buff, 0, BufferLength, null, 0);
            }
            if (t1 != null)
                t1.Join();
            if (t2 != null)
                t2.Join();
            int last = fileStream.Read(buff, 0, BufferLength);
            sha256.TransformFinalBlock(buff, 0, last);
            sha512.TransformFinalBlock(buff, 0, last);
            sha384.TransformFinalBlock(buff, 0, last);
            md5.TransformFinalBlock(buff, 0, last);
            sha1.TransformFinalBlock(buff, 0, last);
            crc32.TransformFinalBlock(buff, 0, last);
            lastPercentOfDone = (int)((fileStream.Position * 100 / fileStream.Length));
            if (percentOfDone != lastPercentOfDone)
            {
                OnEvent(new BlockCountEventArgs(lastPercentOfDone));
                percentOfDone = last;
            }
            b1 = null;
            b2 = null;
            string[] r = new string[]{
                BytesToStr(md5.Hash),
                BytesToStr(sha1.Hash),
                BytesToStr(crc32.Hash.Reverse().ToArray()),
                BytesToStr(sha256.Hash),
                BytesToStr(sha512.Hash),
                BytesToStr(sha384.Hash)
            };
            sha1.Dispose();
            sha256.Dispose();
            sha384.Dispose();
            sha512.Dispose();
            crc32.Dispose();
            md5.Dispose();
            return r;
        } 

Jak mógłbym przyśpieszyć i zoptymalizować ten kawałek kodu, ponieważ jest on dość niestabilny i przy okazji sporo zapycha mi procesor.

0

Ja bym darował sobie sprawdzanie. Co najwyżej dodać sprawdzanie ile rodzai hashy zostało już obliczonych:

      public static Dictionary<string, string> ComputeHash(string filePath)
      {
         var hashAlgorithms = new List<Tuple<string, HashAlgorithm>>
         {
            new Tuple<string, HashAlgorithm>("sha256", new SHA256Managed()),
            new Tuple<string, HashAlgorithm>("sha512", new SHA512Managed()),
            new Tuple<string, HashAlgorithm>("sha384", new SHA384Managed()),
            new Tuple<string, HashAlgorithm>("md5", MD5.Create()),
            new Tuple<string, HashAlgorithm>("sha1", new SHA1Managed())
         };

         var hashes = new Dictionary<string, string>();

         byte[] fileBytes;
         using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
         {
            fileBytes = new byte[fileStream.Length];
            fileStream.Read(fileBytes, 0, fileBytes.Length);
         };

         Parallel.ForEach(hashAlgorithms, tuple =>
         {
                  hashes.Add(tuple.Item1, BitConverter.ToString(tuple.Item2.ComputeHash(fileBytes), 0));
                  tuple.Item2.Dispose();
         });

         return hashes;
      }
0

Niestety wczytanie na raz całego pliku nie wchodzi w grę. Czasem plik zajmuje po 20GB. I czy to sprawdzanie będzie mi wprowadzało spore opóźnienie np 100 razy na cały plik?

0

To zrób tak:

         var hashes = new Dictionary<string, string>();
         
         Parallel.ForEach(hashAlgorithms, tuple =>
         {
            using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
            {
               hashes.Add(tuple.Item1, BitConverter.ToString(tuple.Item2.ComputeHash(fileStream), 0));
               tuple.Item2.Dispose();
            }
         });

Wtedy plik nie zostanie wczytany cały do pamięci. Jest teraz nawet bardziej elegancko według mnie.
Z ciekawości sprawdziłem dla 270MB pliku liczyło się 10 sekund (i5).

2

Klasa, która napisałem działa na zasadzie przekazania specjalnego obiektu (typu Hash) do metody liczącej. Ten obiekt wystawia nam eventy pod które możemy się podpiąć (OnProgressChanged oraz OnCompleted). Dzięki temu możemy monitorować stan liczenia każdego skrótu oddzielnie. Ponadto możemy anulować hashowanie za pomocą tradycyjnego CancellationToken. Tutaj sposób wykorzystania: http://pastebin.com/ELb6VCeh. Oczywiście w kodzie totalnie brakuje obsługi błędów.

ps. sztuczka z prywatnym interfejsem pozwala nam ukryć poszczególne metody.

public static class Hasher
{
    private interface IHash
    {
        HashAlgorithm CreateHash();
        void ProgressChanged(float progress);
        void Completed(byte[] result);
    }

    public class Hash : IHash
    {
        public event EventHandler<float> OnProgressChanged;
        public event EventHandler<byte[]> OnCompleted;

        private Func<HashAlgorithm> factory;

        public Hash(Func<HashAlgorithm> factory)
        {
            this.factory = factory;
        }

        HashAlgorithm IHash.CreateHash()
        {
            return factory();
        }

        void IHash.ProgressChanged(float progress)
        {
            if (OnProgressChanged != null)
                OnProgressChanged(this, progress);
        }

        void IHash.Completed(byte[] result)
        {
            ((IHash)this).ProgressChanged(1);

            if (OnCompleted != null)
                OnCompleted(this, result);
        }
    }
    public static Hash CreateHash(Func<HashAlgorithm> factory)
    {
        return new Hash(factory);
    }

    public static Hash CreateHash<TAlgorithm>() where TAlgorithm : HashAlgorithm, new()
    {
        return new Hash(() => new TAlgorithm());
    }

    public static Task HashAsync(IEnumerable<Hash> hashes, string filepath, CancellationToken cancellationToken)
    {
        return new Task(() =>
        {
            Parallel.ForEach(hashes, hash => HashSingle(hash, filepath, cancellationToken));
        });
    }

    private static void HashSingle(IHash hash, string filepath, CancellationToken cancellationToken)
    {
        if (cancellationToken.IsCancellationRequested)
            return;

        using (var stream = new FileStream(filepath, FileMode.Open, FileAccess.Read))
        {
            var algorithm = hash.CreateHash();

            const int BlockLength = 1024 * 1024;
            var block = new byte[BlockLength];

            long length = stream.Length;
            long remaining = length;

            while (remaining > 0)
            {
                if (cancellationToken.IsCancellationRequested)
                    return;

                int bytes = stream.Read(block, 0, BlockLength);
                remaining -= algorithm.TransformBlock(block, 0, bytes, null, 0);

                hash.ProgressChanged((float)(length - remaining) / length);
            }

            algorithm.TransformFinalBlock(new byte[0], 0, 0);
            hash.Completed(algorithm.Hash);
        }
    }
}
0

Dzięki za odpowiedzi. Zauważyłem tylko że każde rozwiązanie otwiera osobno plik dla każdego skrótu. Nie spowoduje to skakania po dysku? md5 jest liczone ciut szybciej niż sha512.

1 użytkowników online, w tym zalogowanych: 0, gości: 1