1. Теперь за форумную активность начисляются биткоины и другие криптоденьги. Подробнее.
    Скрыть объявление
  2. Появилась архивная версия форума arhiv.xaker.name, где собраны темы с 2007 по 2012 год.
    Скрыть объявление

Быстрое чтение больших файлов в Delphi (Memory Mapped Files)

Тема в разделе "Pascal/Delphi", создана пользователем Dr. MefistO, 5 апр 2012.

  1. Dr. MefistO
    Dr. MefistO Крывіч Глобальный модератор
    Симпатии:
    123
    Всем привет!

    Сегодня я расскажу вам, как можно читать файлы большого объема (до 3 ГБ) очень быстро, особо не напрягая при этом систему.

    Вступление

    Бывает, когда файлы нужно читать байт за байтом (поиск каких-то сигнатур с пропуском каких-то блоков, например). Сложилось так, что обычная операция чтения через файловый поток читает за раз не один байт, а несколько (точно не помню сколько). И, выходит, что при чтении одного байта мы совершаем кучу лишних телодвижений:
    1. Прочитать блок размером 4096 КБ, например;
    2. Выделить из него один байт;
    3. Скопировать этот байт в другой участок памяти для сравнения и т.д.
    Это существенно замедляет скорость работы с файлами. Memory Mapped Files позволяют избежать лишних извращений и напрямую читать нужные данные.

    Memory Mapped Files

    Memory Mapped Files - это такая крутая штука, придуманная фирмой мелкософт для минимизации количества операций чтения/записи в память при работе с файлами на жестком диске.

    Вот небольшая цитата с Педивикии:
    ...и мне вот тоже ничего сначала не понятно было. А потом оказалось, что все куда легче и понятнее (особенно на практике). Вот и приступим...

    Приготовления

    Перед тем, как начать, убедитесь, что у вас установлена Delphi (можно любая версия, но я буду использовать Delphi 7). Теперь нам нужно скачать библиотеку KOL (это ссылка) (и дополнение к ней - KOLAdd). Все это дело распаковываем куда-нибудь в одну папку, и прописываем путь к этому каталогу здесь: Tools->Environment Options...->Вкладка Library->Первая кнопка с многоточием (...)->В поле для ввода пишем путь к каталогу->Жмем Add->OK. Теперь кодинг.

    Кодим тест-сравнение

    Открываем Delphi 7, создаем консольный проект (File->New->Other...>Console Application).
    Мы будем сравнивать обычное чтение через PStream и через Mapped Files. Файл будет размером 400 МБ (например, видеоролик, или маленький фильм).

    Подготовка

    Перед тем, как начать использовать KOLAdd (в нем содержатся функции для работы с Memory Mapped Files), нам нужно его немного подредактировать: убрать все лишнее, и немного подредактировать код (данный модуль писался давно, поэтому требования немного изменились).
    Найдем в файле KOLAdd.pas код функции MapFileRead (собственно, нам понадобится только две функции: эта, да еще UnmapFile, поэтому можно спокойно удалить все остальные - я так и сделаю). Конечный файлик получился вот с таким кодом:
    Код:
    unit KOLadd;
    
    interface
    
    uses Windows, KOL;
    
    function MapFileRead( const Filename: AnsiString; var hFile, hMap: THandle ): Pointer;
    procedure UnmapFile( BasePtr: Pointer; hFile, hMap: THandle );
    
    implementation
    
    function MapFileRead( const Filename: AnsiString; var hFile, hMap: THandle ): Pointer;
    var Sz, Hi: DWORD;
    begin
      Result := nil;
      hFile := FileCreate( KOLString(Filename), ofOpenRead or ofOpenExisting or ofShareDenyNone );
      hMap := 0;
      if hFile = INVALID_HANDLE_VALUE then Exit;
      Sz := GetFileSize( hFile, @ Hi );
      hMap := CreateFileMapping( hFile, nil, PAGE_READONLY, Hi, Sz, nil );
      if hMap = 0 then Exit;
      //if (Hi <> 0) or (Sz > $0FFFFFFF) then Sz := $0FFFFFFF;
      //Здесь я убрал уменьшение до 256 МБ объема проецируемого на память файла
      Result := MapViewOfFile( hMap, FILE_MAP_READ, 0, 0, Sz );
    end;
    
    procedure UnmapFile( BasePtr: Pointer; hFile, hMap: THandle );
    begin
      if BasePtr <> nil then
        UnmapViewOfFile( BasePtr );
      if hMap <> 0 then
        CloseHandle( hMap );
      if hFile <> INVALID_HANDLE_VALUE then
        CloseHandle( hFile );
    end;
    
    end.
    Собственно код


    Давайте сначала определимся, что мы будем делать с этим файлом. Предлагаю посчитать общую сумму всех байт файла, но от результата мы возьмем только младший байт. Делается это простым приведением результата к типу Byte.
    Код:
    a := $AABBCCDD;
    b := byte(a);
    Я приведу полный код со своими комментариями и результатом, который я получил (в виде скриншота).

    Код:
    program memmapfil;
    
    {$APPTYPE CONSOLE}
    
    uses
      KOL, KOLAdd, windows;
    
    var
      sum, buf: byte;
      pFile, pStart: PByte;
      hFile, hMap: THandle;
      stream: PStream;
      time, i, Filesz: Cardinal;
    begin
      //Узнаем размер файла
      //ParamStr(1) - переданный через параметр
      //              командной строки путь к файлу
      Filesz := FileSize(ParamStr(1));
    
      //Откроем файл на чтение
      stream := NewReadFileStream(ParamStr(1));
    
      //До операций чтения узнаем время начала
      time := GetTickCount;
    
      for i:= 1 to Filesz do
      begin
        stream.Read(buf,1);
        sum := byte(sum + buf);
      end;
    
      //И выведем итого время, затраченное на чтение
      Writeln('Elapsed time by PStream: ',GetTickCount-time);
      Writeln(sum);
    
      //Закроем файл
      stream.Free;
      sum := 0;
      time := GetTickCount;
    
      //Создаем Mapped File на наш файл размером 400 МБ
      pFile := MapFileRead(ParamStr(1),hFile,hMap);
    
      //Запоминаем начало файла для последующего закрытия этого файла
      pStart := pFile;
    
      for i:= 1 to Filesz do
      begin
        //pFile^ - это текущий читаемый байт файла
        //Т.к. pFile - это указатель, то, чтобы читать
        //байты, нужно получать текущее значение
        //так называемым "разыменованием" указателя,
        //указав после указателя значок ^
        sum := byte(sum + pFile^);
        Inc(pFile);
      end;
    
      //Закрываем файл
      UnmapFile(pStart, hFile, hMap);
    
      Writeln('Elapsed time by MMF: ',GetTickCount-time);
      Writeln(sum);
    end.
    Результаты:
    Быстрое чтение больших файлов в Delphi (Memory Mapped Files)

    Цифры тут указаны в миллисекундах. Как видим, разница просто огромная. Результат мы получили одинаковый, что говорит о верности обоих методов.

    Важное замечание

    При работе с MMF нужно следить, чтобы читаемый по указателю байт (через ^) не попадал за пределы файла, иначе будет срабатывать исключение Access Violation. Следить за этим просто. Нужно всего лишь знать размер файла, и воспользоваться следующей проверкой:
    Код:
    if (Cardinal(pFile)-Cardinal(pStart))<FileSz then
    //...
    Т.е. если адрес байта текущий минус адрес байта начала меньше размера файла, то читать можно, иначе - закрываем файл и выходим.

    Выводы

    MMF - крутая и полезная штука, если научиться с ними работать. :)

    исходный код моего приложения можно найти во вложении

    ---------
    Автор: Владимир Мефисто
    Special for: Xaker.Name
     
    Последнее редактирование: 5 апр 2012
    5 апр 2012
    5 пользователям это понравилось.
  2. Dr. MefistO
    Dr. MefistO Крывіч Глобальный модератор
    Симпатии:
    123
    Ну уж расскажите нам про буферное чтение, и как его применять в данной ситуации, рассмотренной мной в статье.
     
    8 апр 2012
  3. QAZ
    QAZ Новичок
    Симпатии:
    1
    напрашиваеца вопрос почему до 3х а не до 2х или 4х? :crazy:

    результаты скорости некоректны, после первого прогона файл полностью кэшируется системой

    и как намекал некий robt про буфер, то разница будет в пользу буферного чтения а не ММФ :)
     
    Последнее редактирование: 10 сен 2012
    10 сен 2012
  4. Dr. MefistO
    Dr. MefistO Крывіч Глобальный модератор
    Симпатии:
    123
    QAZ,
    тестировал и на свежесозданных файлах. Скорость явно больше.
     
    10 сен 2012
  5. QAZ
    QAZ Новичок
    Симпатии:
    1
    больше чем у кого с чем? ясен фиг что быстрей чем побайтовое чтение, но не быстрей буферного, да и файлы больше двух гиг не осилит
     
    11 сен 2012
  6. Dr. MefistO
    Dr. MefistO Крывіч Глобальный модератор
    Симпатии:
    123
    Я уже просил привести пример буферного чтения. Чего-то никто не отписался.
     
    12 сен 2012
  7. QAZ
    QAZ Новичок
    Симпатии:
    1
    ну держи...
    для исключения кэширования системой, положи рядом с прогой 2 одинаковых файла приличного размера (1-2гига) с именами test1.dat и test2.dat (для полного задротства можно их дефрагментировать) и после перезагрузки компа запусти

    также буфер ,в отличии от MMF, можно использовать в многопоточке, типа пока считывается следующая порция файла в это время дрючица текущая в памяти
     
    Последнее редактирование: 13 сен 2012
    13 сен 2012
    1 человеку нравится это.
  8. Dr. MefistO
    Dr. MefistO Крывіч Глобальный модератор
    Симпатии:
    123
    Окей. Буду знать)
     
    13 сен 2012
  9. NoFx
    NoFx Новичок
    Симпатии:
    0
    QAZ,
    Было бы интересно узнать, как здесь реализовать многопоточность?
     
    16 мар 2013

Поделиться этой страницей

Загрузка...