Подтвердить что ты не робот

API Windows кажется намного быстрее, чем BinaryWriter - мой тест правильный?

[EDIT]

Благодаря @VilleKrumlinde я исправил ошибку, которую я случайно представил ранее, пытаясь избежать предупреждения анализа кода. Я случайно включил "перекрывающуюся" обработку файлов, которая продолжала сбросить длину файла. Это теперь исправлено, и вы можете вызывать FastWrite() несколько раз для одного и того же потока без проблем.

[Редактирование конца]


Обзор

Я делаю несколько тестов времени, чтобы сравнить два разных способа записи массивов структур на диск. Я считаю, что воспринимаемая мудрость заключается в том, что затраты на ввод-вывод настолько велики по сравнению с другими вещами, что не стоит тратить слишком много времени на оптимизацию других вещей.

Однако мои тесты времени, похоже, указывают на другое. Либо я делаю ошибку (что вполне возможно), либо моя оптимизация действительно значительна.

История

Первая история: этот метод FastWrite() был изначально написан много лет назад для поддержки написания структур в файл, который был использован унаследованной программой на С++, и мы все еще используем его для этой цели. (Также есть соответствующий метод FastRead().) Он был написан в основном для упрощения записи массивов blittable structs в файл, и его скорость была вторичной проблемой.

Мне говорили несколько человек, что оптимизация, как это, не намного быстрее, чем просто использование BinaryWriter, поэтому я, наконец, укусил пулю и выполнил некоторые временные тесты. Результаты меня удивили...

Похоже, что мой метод FastWrite() в 30-50 раз быстрее, чем эквивалент, используя BinaryWriter. Это кажется смешным, поэтому я отправляю свой код здесь, чтобы узнать, смогут ли найти ошибки.

Спецификация системы

  • Протестирована сборка x86 RELEASE, запустите от OUTSIDE отладчика.
  • Работает в Windows 8, x64, 16 Гб памяти.
  • Запуск на обычном жестком диске (а не на SSD).
  • Использование .Net 4 с Visual Studio 2012 (поэтому установлен .Net 4.5)

Результаты

Мои результаты:

SlowWrite() took 00:00:02.0747141
FastWrite() took 00:00:00.0318139
SlowWrite() took 00:00:01.9205158
FastWrite() took 00:00:00.0327242
SlowWrite() took 00:00:01.9289878
FastWrite() took 00:00:00.0321100
SlowWrite() took 00:00:01.9374454
FastWrite() took 00:00:00.0316074

Как вы можете видеть, это похоже на то, что в этом прогоне FastWrite() в 50 раз быстрее.

Вот мой тестовый код. После запуска теста я выполнил двоичное сравнение двух файлов, чтобы убедиться, что они действительно идентичны (т.е. FastWrite() и SlowWrite() созданы идентичные файлы).

Посмотрите, что вы можете сделать.:)

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using Microsoft.Win32.SafeHandles;

namespace ConsoleApplication1
{
    internal class Program
    {

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        struct TestStruct
        {
            public byte   ByteValue;
            public short  ShortValue;
            public int    IntValue;
            public long   LongValue;
            public float  FloatValue;
            public double DoubleValue;
        }

        static void Main()
        {
            Directory.CreateDirectory("C:\\TEST");
            string filename1 = "C:\\TEST\\TEST1.BIN";
            string filename2 = "C:\\TEST\\TEST2.BIN";

            int count = 1000;
            var array = new TestStruct[10000];

            for (int i = 0; i < array.Length; ++i)
                array[i].IntValue = i;

            var sw = new Stopwatch();

            for (int trial = 0; trial < 4; ++trial)
            {
                sw.Restart();

                using (var output = new FileStream(filename1, FileMode.Create))
                using (var writer = new BinaryWriter(output, Encoding.Default, true))
                {
                    for (int i = 0; i < count; ++i)
                    {
                        output.Position = 0;
                        SlowWrite(writer, array, 0, array.Length);
                    }
                }

                Console.WriteLine("SlowWrite() took " + sw.Elapsed);
                sw.Restart();

                using (var output = new FileStream(filename2, FileMode.Create))
                {
                    for (int i = 0; i < count; ++i)
                    {
                        output.Position = 0;
                        FastWrite(output, array, 0, array.Length);
                    }
                }

                Console.WriteLine("FastWrite() took " + sw.Elapsed);
            }
        }

        static void SlowWrite(BinaryWriter writer, TestStruct[] array, int offset, int count)
        {
            for (int i = offset; i < offset + count; ++i)
            {
                var item = array[i];  // I also tried just writing from array[i] directly with similar results.
                writer.Write(item.ByteValue);
                writer.Write(item.ShortValue);
                writer.Write(item.IntValue);
                writer.Write(item.LongValue);
                writer.Write(item.FloatValue);
                writer.Write(item.DoubleValue);
            }
        }

        static void FastWrite<T>(FileStream fs, T[] array, int offset, int count) where T: struct
        {
            int sizeOfT = Marshal.SizeOf(typeof(T));
            GCHandle gcHandle = GCHandle.Alloc(array, GCHandleType.Pinned);

            try
            {
                uint bytesWritten;
                uint bytesToWrite = (uint)(count * sizeOfT);

                if
                (
                    !WriteFile
                    (
                        fs.SafeFileHandle,
                        new IntPtr(gcHandle.AddrOfPinnedObject().ToInt64() + (offset*sizeOfT)),
                        bytesToWrite,
                        out bytesWritten,
                        IntPtr.Zero
                    )
                )
                {
                    throw new IOException("Unable to write file.", new Win32Exception(Marshal.GetLastWin32Error()));
                }

                Debug.Assert(bytesWritten == bytesToWrite);
            }

            finally
            {
                gcHandle.Free();
            }
        }

        [DllImport("kernel32.dll", SetLastError=true)]
        [return: MarshalAs(UnmanagedType.Bool)]

        private static extern bool WriteFile
        (
            SafeFileHandle hFile,
            IntPtr         lpBuffer,
            uint           nNumberOfBytesToWrite,
            out uint       lpNumberOfBytesWritten,
            IntPtr         lpOverlapped
        );
    }
}

Follow Up

Я также проверил код, предложенный @ErenErsönmez, следующим образом (и я подтвердил, что все три файла идентичны в конце теста):

static void ErenWrite<T>(FileStream fs, T[] array, int offset, int count) where T : struct
{
    // Note: This doesn't use 'offset' or 'count', but it could easily be changed to do so,
    // and it doesn't change the results of this particular test program.

    int size = Marshal.SizeOf(typeof(TestStruct)) * array.Length;
    var bytes = new byte[size];
    GCHandle gcHandle = GCHandle.Alloc(array, GCHandleType.Pinned);

    try
    {
        var ptr = new IntPtr(gcHandle.AddrOfPinnedObject().ToInt64());
        Marshal.Copy(ptr, bytes, 0, size);
        fs.Write(bytes, 0, size);
    }

    finally
    {
        gcHandle.Free();
    }
}

Я добавил тест для этого кода и в то же время удалил строки output.Position = 0;, чтобы файлы теперь выросли до 263K (это разумный размер).

С этими изменениями результаты:

ПРИМЕЧАНИЕ Посмотрите, насколько медленнее время FastWrite(), когда вы не возвращаете указатель файла обратно к нулю!:

SlowWrite() took 00:00:01.9929327
FastWrite() took 00:00:00.1152534
ErenWrite() took 00:00:00.2185131
SlowWrite() took 00:00:01.8877979
FastWrite() took 00:00:00.2087977
ErenWrite() took 00:00:00.2191266
SlowWrite() took 00:00:01.9279477
FastWrite() took 00:00:00.2096208
ErenWrite() took 00:00:00.2102270
SlowWrite() took 00:00:01.7823760
FastWrite() took 00:00:00.1137891
ErenWrite() took 00:00:00.3028128

Таким образом, похоже, что вы можете достичь почти той же скорости, используя Marshaling, не используя вообще API Windows. Единственным недостатком является то, что метод Eren должен сделать копию всего массива структур, что может быть проблемой, если память ограничена.

4b9b3361

Ответ 1

Я не думаю, что разница имеет отношение к BinaryWriter. Я думаю, это связано с тем, что вы делаете несколько файлов IO в SlowWrite (10000 * 6) против одного ввода-вывода в FastWrite. Преимущество вашего FastWrite состоит в том, что для записи в файл требуется один бит байтов. С другой стороны, вы делаете попытку преобразования структур в байтовые массивы один за другим в SlowWrite.

Чтобы проверить эту теорию, я написал небольшой метод, который предварительно создает большой массив байтов всех структур, а затем использовал этот массив байтов в SlowWrite:

static byte[] bytes;
static void Prep(TestStruct[] array)
{
    int size = Marshal.SizeOf(typeof(TestStruct)) * array.Length;
    bytes = new byte[size];
    GCHandle gcHandle = GCHandle.Alloc(array, GCHandleType.Pinned);
    var ptr = gcHandle.AddrOfPinnedObject();
    Marshal.Copy(ptr, bytes, 0, size);
    gcHandle.Free();
}

static void SlowWrite(BinaryWriter writer)
{
    writer.Write(bytes);
}

Результаты:

SlowWrite() took 00:00:00.0360392
FastWrite() took 00:00:00.0385015
SlowWrite() took 00:00:00.0358703
FastWrite() took 00:00:00.0381371
SlowWrite() took 00:00:00.0373875
FastWrite() took 00:00:00.0367692
SlowWrite() took 00:00:00.0348295
FastWrite() took 00:00:00.0373931

Обратите внимание, что SlowWrite теперь очень похоже на FastWrite, и я думаю, что это показывает, что разница в производительности не связана с фактической производительностью ввода-вывода, а больше связана с процессом двоичного преобразования.