[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 должен сделать копию всего массива структур, что может быть проблемой, если память ограничена.