Статьи

Со злым умыслом мы можем попробовать еще лучше

Продолжая ту же тему из нашего последнего поста , как мы можем улучшить скорость записи на диск? В частности, в настоящее время я сосредоточен на своем худшем сценарии:

    fill rnd buff 10,000 tx            :    161,812 ms      6,180 ops / sec

Это 10000 сделок все работает один за другим, и принимать действительно слишком долго , чтобы идти о делают свое дело. Теперь мы внесли некоторые улучшения и достигли 6340 операций в секунду, но я думаю, вы согласитесь, что даже эта оптимизация, вероятно, все еще плоха. Мы провели там больше времени, пытаясь выяснить, как именно мы можем выполнить микрооптимизацию, и мы получили до 8078 операций в секунду.

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

 var key = Guid.NewGuid().ToByteArray();
 var buffer = new byte[100];
 new Random().NextBytes(buffer);
  
 using (var fs = new FileStream("test.bin", FileMode.Truncate, FileAccess.ReadWrite))
 {
     fs.SetLength(1024*1024*768);
  
     var sp = Stopwatch.StartNew();
  
     for (int i = 0; i < 10*1000; i++)
    {
         for (int j = 0; j < 100; j++)
         {
             fs.Write(key,0, 16);
             fs.Write(buffer, 0, 100);
         }
         fs.Flush(true);
     }

    Console.WriteLine("{0:#,#} ms for {1:#,#} ops / sec", sp.ElapsedMilliseconds, (1000*1000)/sp.Elapsed.TotalSeconds);
 }

Этот код имитирует абсолютно лучший сценарий, на который мы могли надеяться. Нулевая стоимость для управления данными, чистая последовательная запись. Обратите внимание, что мы вызываем Flush (true) для имитации 10 000 транзакций. Этот код дает мне: 147,201 операций в секунду.

Это интересно, в основном потому, что я думал, что причина, по которой наши случайные записи с 10 000 транзакций были плохими, — это вызовы Flush (), но, похоже, это на самом деле работает очень хорошо. Затем я проверил это с некоторыми случайными записями, добавив следующие строки перед строкой 13:

var next = random.Next(0, 1024*1024*512);
 fs.Position = next - next%4096;

Затем я решил попробовать это с файлами сопоставленной памяти, и я написал:

using (var fs = new FileStream("test.bin", FileMode.Truncate, FileAccess.ReadWrite))
 {
     fs.SetLength(1024 * 1024 * 768);
  
     var memoryMappedFile = MemoryMappedFile.CreateFromFile(fs,
                                     "test", fs.Length, MemoryMappedFileAccess.ReadWrite,
                                     null, HandleInheritability.None, true);
     var memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor();
  
     byte* p = null;
     memoryMappedViewAccessor.SafeMemoryMappedViewHandle.AcquirePointer(ref p);
  
     var sp = Stopwatch.StartNew();
  
     for (int i = 0; i < 10 * 1000; i++)
     {
         var next = random.Next(0, 1024 * 1024 * 512);
         byte* basePtr = p + next;
         using (var ums = new UnmanagedMemoryStream(basePtr, 12 * 1024,12*1024, FileAccess.ReadWrite))
         {
             for (int j = 0; j < 100; j++)
             {
                 ums.Write(key, 0, 16);
                 ums.Write(buffer, 0, 100);
             }
         }
     }
     Console.WriteLine("{0:#,#} ms for {1:#,#} ops / sec", sp.ElapsedMilliseconds, (1000 * 1000) / sp.Elapsed.TotalSeconds);
 }

Вы заметите, что я не делаю здесь никакой промывки. Это намерение на данный момент, используя это, я получаю 5 миллионов + операций в секунду. Но так как я не делаю сброс, это в значительной степени я проверяю, насколько быстро я могу писать в память.

Добавление одного сброса обошлось нам в 1,8 секунды для файла размером 768 МБ. А что делать правильно? Добавление следующего в строку 26 означает, что мы фактически очищаем буферы.

FlushViewOfFile(basePtr, new IntPtr(12 * 1024));

Обратите внимание, что мы не записываем на диск, нам все еще нужно это сделать. Но сейчас давайте попробуем это сделать. Эта единственная строка изменила код с 5 миллионов операций на 170,988 операций в секунду. И это не включает в себя фактическую запись на диск. Когда мы это делаем, мы получаем по-настоящему смешное число: 20 547 операций в секунду. И это многое объясняет, я думаю.

Для справки вот полный код:

 unsafe class Program

   {
     [DllImport("kernel32.dll", SetLastError = true)]
     [return: MarshalAs(UnmanagedType.Bool)]
     extern static bool FlushViewOfFile(byte* lpBaseAddress, IntPtr dwNumberOfBytesToFlush);
  
     static void Main(string[] args)
     {
         var key = Guid.NewGuid().ToByteArray();
         var buffer = new byte[100];
         var random = new Random();
         random.NextBytes(buffer);
  
         using (var fs = new FileStream("test.bin", FileMode.Truncate, FileAccess.ReadWrite))
         {
             fs.SetLength(1024 * 1024 * 768);
  
             var memoryMappedFile = MemoryMappedFile.CreateFromFile(fs,
                                             "test", fs.Length, MemoryMappedFileAccess.ReadWrite,
                                             null, HandleInheritability.None, true);
             var memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor();
  
             byte* p = null;
             memoryMappedViewAccessor.SafeMemoryMappedViewHandle.AcquirePointer(ref p);
  
             var sp = Stopwatch.StartNew();
  
             for (int i = 0; i < 10 * 1000; i++)
             {
                 var next = random.Next(0, 1024 * 1024 * 512);
                 byte* basePtr = p + next;
                 using (var ums = new UnmanagedMemoryStream(basePtr, 12 * 1024, 12 * 1024, FileAccess.ReadWrite))
                 {
                    for (int j = 0; j < 100; j++)
                     {
                        ums.Write(key, 0, 16);
                         ums.Write(buffer, 0, 100);
                     }
                 }
                 FlushViewOfFile(basePtr, new IntPtr(12 * 1024));
                 fs.Flush(true);
             }
             Console.WriteLine("{0:#,#} ms for {1:#,#} ops / sec", sp.ElapsedMilliseconds, (1000 * 1000) / sp.Elapsed.TotalSeconds);
         }
     }
 }

Это примерно так же эффективно, как вы можете получить для записи на диск , используя файлы , отображенные на память , если вам нужно сделать, используя файлы , отображенные на память в транзакционной образом. И это в лучшем случае абсолютно. Где мы точно знаем, что мы написали и где мы это написали, и мы всегда пишем одну запись фиксированного размера и т. Д. В случае с Вороном мы могли бы писать на нескольких страницах за одну транзакцию (на самом деле, мы в значительной степени гарантированно сделаем именно это).

Это означает, что мне нужно подумать о других способах сделать это.