web-dev-qa-db-fra.com

Comment lire rapidement un fichier binaire en c #? (ReadOnlySpan vs MemoryStream)

J'essaie d'analyser un fichier binaire le plus rapidement possible. Voici donc ce que j'ai d'abord essayé de faire:

using (FileStream filestream = path.OpenRead()) {
   using (var d = new GZipStream(filestream, CompressionMode.Decompress)) {
      using (MemoryStream m = new MemoryStream()) {
         d.CopyTo(m);
         m.Position = 0;

         using (BinaryReaderBigEndian b = new BinaryReaderBigEndian(m)) {
            while (b.BaseStream.Position != b.BaseStream.Length) {
               UInt32 value = b.ReadUInt32();
}  }  }  }  }

Où la classe BinaryReaderBigEndian est implémentée comme suit:

public static class BinaryReaderBigEndian {
   public BinaryReaderBigEndian(Stream stream) : base(stream) { }

   public override UInt32 ReadUInt32() {
      var x = base.ReadBytes(4);
      Array.Reverse(x);
      return BitConverter.ToUInt32(x, 0);
}  }

Ensuite, j'ai essayé d'obtenir une amélioration des performances en utilisant ReadOnlySpan au lieu de MemoryStream. J'ai donc essayé de faire:

using (FileStream filestream = path.OpenRead()) {
   using (var d = new GZipStream(filestream, CompressionMode.Decompress)) {
      using (MemoryStream m = new MemoryStream()) {
         d.CopyTo(m);
         int position = 0;
         ReadOnlySpan<byte> stream = new ReadOnlySpan<byte>(m.ToArray());

         while (position != stream.Length) {
            UInt32 value = stream.ReadUInt32(position);
            position += 4;
}  }  }  }

BinaryReaderBigEndian classe a changé dans:

public static class BinaryReaderBigEndian {
   public override UInt32 ReadUInt32(this ReadOnlySpan<byte> stream, int start) {
      var data = stream.Slice(start, 4).ToArray();
      Array.Reverse(x);
      return BitConverter.ToUInt32(x, 0);
}  }

Mais, malheureusement, je n'ai remarqué aucune amélioration. Alors, où est-ce que je fais mal?

13
heliosophist

J'ai fait une mesure de votre code sur mon ordinateur ( Intel Q9400, 8 GiB RAM, disque SSD, Win10 x64 Home, .NET Framework 4/7/2, testé avec 15 Fichier MB (une fois décompressé) avec les résultats suivants:

Version No-Span: 520 ms
Version étendue: 720 ms

Donc, la version Span est en fait plus lente! Pourquoi? Parce que new ReadOnlySpan<byte>(m.ToArray()) effectue une copie supplémentaire de tout le fichier et ReadUInt32() effectue de nombreux découpages de Span (le découpage est bon marché, mais pas gratuit). Puisque vous avez effectué plus de travail, vous ne pouvez pas vous attendre à ce que les performances soient meilleures simplement parce que vous avez utilisé Span.

Pouvons-nous faire mieux? Oui. Il s'avère que la partie la plus lente de votre code est en fait un garbage collection provoqué par l'allocation répétée de Arrays sur 4 octets créés par le .ToArray() appelle dans la méthode ReadUInt32(). Vous pouvez l'éviter en implémentant vous-même ReadUInt32(). C'est assez facile et élimine également le besoin de découpage Span. Vous pouvez également remplacer new ReadOnlySpan<byte>(m.ToArray()) par new ReadOnlySpan<byte>(m.GetBuffer()).Slice(0, (int)m.Length);, qui effectue un découpage bon marché au lieu de copier le fichier entier. Alors maintenant, le code ressemble à ceci:

public static void Read(FileInfo path)
{
    using (FileStream filestream = path.OpenRead())
    {
        using (var d = new GZipStream(filestream, CompressionMode.Decompress))
        {
            using (MemoryStream m = new MemoryStream())
            {
                d.CopyTo(m);
                int position = 0;

                ReadOnlySpan<byte> stream = new ReadOnlySpan<byte>(m.GetBuffer()).Slice(0, (int)m.Length);

                while (position != stream.Length)
                {
                    UInt32 value = stream.ReadUInt32(position);
                    position += 4;
                }
            }
        }
    }
}

public static class BinaryReaderBigEndian
{
    public static UInt32 ReadUInt32(this ReadOnlySpan<byte> stream, int start)
    {
        UInt32 res = 0;
        for (int i = 0; i < 4; i++)
            {
                res = (res << 8) | (((UInt32)stream[start + i]) & 0xff);
        }
        return res;
    }
}

Avec ces changements, je passe de 720 ms à 165 ms (4x plus rapide). Sonne bien, non? Mais nous pouvons faire encore mieux. Nous pouvons complètement éviter MemoryStream copier et incorporer et optimiser encore ReadUInt32():

public static void Read(FileInfo path)
{
    using (FileStream filestream = path.OpenRead())
    {
        using (var d = new GZipStream(filestream, CompressionMode.Decompress))
        {
            var buffer = new byte[64 * 1024];

            do
            {
                int bufferDataLength = FillBuffer(d, buffer);

                if (bufferDataLength % 4 != 0)
                    throw new Exception("Stream length not divisible by 4");

                if (bufferDataLength == 0)
                    break;

                for (int i = 0; i < bufferDataLength; i += 4)
                {
                    uint value = unchecked(
                        (((uint)buffer[i]) << 24)
                        | (((uint)buffer[i + 1]) << 16)
                        | (((uint)buffer[i + 2]) << 8)
                        | (((uint)buffer[i + 3]) << 0));
                }

            } while (true);
        }
    }
}

private static int FillBuffer(Stream stream, byte[] buffer)
{
    int read = 0;
    int totalRead = 0;
    do
    {
        read = stream.Read(buffer, totalRead, buffer.Length - totalRead);
        totalRead += read;

    } while (read > 0 && totalRead < buffer.Length);

    return totalRead;
}

Et maintenant, cela prend moins de 90 ms (8x plus rapide que l'original!). Et sans Span! Span est idéal dans les situations où il permet d'effectuer un découpage et d'éviter la copie de tableau, mais il n'améliorera pas les performances simplement en l'utilisant aveuglément. Après tout, Span est conçu pour avoir caractéristiques de performances comparables à Array , mais pas mieux (et uniquement sur les environnements d'exécution qui ont un support spécial pour cela, tels que .NET Core 2.1).

8
Ňuf