1. Вы находитесь в архивной версии форума xaker.name. Здесь собраны темы с 2007 по 2012 год, большинство инструкций и мануалов уже неактуальны.
    Скрыть объявление

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

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

  1. Dr. MefistO

    Dr. MefistO Крывіч Глобальный модератор

    Регистрация:
    3 авг 2008
    Сообщения:
    152
    Симпатии:
    254
    Баллы:
    0
    Всем привет!

    Сегодня я расскажу вам, как можно читать файлы большого объема (до 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, поэтому можно спокойно удалить все остальные - я так и сделаю). Конечный файлик получился вот с таким кодом:
    Код:
    [COLOR="DarkBlue"][B]unit[/B][/COLOR] KOLadd;
    
    [COLOR="DarkBlue"][B]interface[/B][/COLOR]
    
    [COLOR="DarkBlue"][B]uses[/B][/COLOR] Windows, KOL;
    
    [COLOR="DarkBlue"][B]function[/B][/COLOR] MapFileRead( [COLOR="DarkBlue"][B]const[/B][/COLOR] Filename: AnsiString; var hFile, hMap: THandle ): Pointer;
    [COLOR="DarkBlue"][B]procedure[/B][/COLOR] UnmapFile( BasePtr: Pointer; hFile, hMap: THandle );
    
    [COLOR="DarkBlue"][B]implementation[/B][/COLOR]
    
    [COLOR="DarkBlue"][B]function[/B][/COLOR] MapFileRead( [COLOR="DarkBlue"][B]const[/B][/COLOR] Filename: AnsiString; var hFile, hMap: THandle ): Pointer;
    [COLOR="DarkBlue"][B]var[/B][/COLOR] Sz, Hi: DWORD;
    [B][COLOR="Red"]begin[/COLOR][/B]
      Result := [COLOR="DarkBlue"][B]nil[/B][/COLOR];
      hFile := FileCreate( KOLString(Filename), ofOpenRead [COLOR="DarkBlue"][B]or[/B][/COLOR] ofOpenExisting [COLOR="DarkBlue"][B]or[/B][/COLOR] ofShareDenyNone );
      hMap := [COLOR="Blue"]0[/COLOR];
      [B][COLOR="Orange"]if[/COLOR][/B] hFile = INVALID_HANDLE_VALUE [B][COLOR="Orange"]then[/COLOR][/B] Exit;
      Sz := GetFileSize( hFile, @ Hi );
      hMap := CreateFileMapping( hFile, [COLOR="DarkBlue"][B]nil[/B][/COLOR], PAGE_READONLY, Hi, Sz, [COLOR="DarkBlue"][B]nil[/B][/COLOR] );
      [B][COLOR="Orange"]if[/COLOR][/B] hMap = [COLOR="Blue"]0[/COLOR] [B][COLOR="Orange"]then[/COLOR][/B] Exit;
      [COLOR="Green"]//if (Hi <> 0) or (Sz > $0FFFFFFF) then Sz := $0FFFFFFF;[/COLOR]
      [COLOR="Green"]//Здесь я убрал уменьшение до 256 МБ объема проецируемого на память файла[/COLOR]
      Result := MapViewOfFile( hMap, FILE_MAP_READ, [COLOR="Blue"]0[/COLOR], [COLOR="Blue"]0[/COLOR], Sz );
    [B][COLOR="Red"]end[/COLOR][/B];
    
    [COLOR="DarkBlue"][B]procedure[/B][/COLOR] UnmapFile( BasePtr: Pointer; hFile, hMap: THandle );
    [B][COLOR="Red"]begin[/COLOR][/B]
      [B][COLOR="Orange"]if[/COLOR][/B] BasePtr <> [COLOR="DarkBlue"][B]nil[/B][/COLOR] [B][COLOR="Orange"]then[/COLOR][/B]
        UnmapViewOfFile( BasePtr );
      [B][COLOR="Orange"]if[/COLOR][/B] hMap <> [COLOR="Blue"]0[/COLOR] [B][COLOR="Orange"]then[/COLOR][/B]
        CloseHandle( hMap );
      [B][COLOR="Orange"]if[/COLOR][/B] hFile <> INVALID_HANDLE_VALUE [B][COLOR="Orange"]then[/COLOR][/B]
        CloseHandle( hFile );
    [B][COLOR="Red"]end[/COLOR][/B];
    
    [B][COLOR="DarkBlue"]end[/COLOR][/B].
    Собственно код


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

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

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

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

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

    Выводы

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

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

    ---------
    Автор: Владимир Мефисто
    Special for: Xaker.Name
     
    Последнее редактирование: 5 апр 2012
    5 пользователям это понравилось.

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