Главная » Delphi » Оптимизация чтения через memory mapped files

0

Ах да, вы же, наверное, с нетерпением ждете, когда я приступлю к оптимизации доступа к файлу — нельзя же дальше терпеть это любительство с многократным чтением с диска, притом побайтно! На самом деле это очень просто, нам даже не придется вносить капитальных изменений в программу. Мы, кстати, уже умеем создавать отображения файлов в память — см. главу 7. Но здесь все еще проще — никаких структур ведь не надо, требуется только перевести содержимое файла в строку. Нам нужно осуществить такую последовательность операций: получить дескриптор дискового файла (CreateFile), создать файл в памяти (знакомая функция CreateFiieMapping) и получить указатель на этот файл (также знакомая нам MapViewOfFiie). Потом мы считаем значения по этому указателю в строку и в обратном порядке все уничтожим

(UnmapViewOfFile И CloseHandle).

Заметки на полях

Для того чтобы ускорить процесс чтения файлов, есть, вообще говоря, несколько механизмов. Это потоковое чтение с использованием TFiieStream (см. [3]), простое применение API-функций CreateFile и ReadFile с созданием вручную (через механизм Allocate) буфера в куче, чтение нетипизироввнных файлов в массивы по указателю, в динамические массивы или просто сразу в строку (подробности см. в главе 21). Механизм отображения фвйлов в память (file mapping) хорош тем, что прост, и в то же время все заботы о кэшировании система берет на себя. Формально говоря, с точки зрения системы в данном случав, когда мы фвйл читаем последовательно байт за байтом, лучшим способом было бы использование потокового чтения— при нем оперативная память меньше загружена и больше ресурсов остается другим приложениям. Но наше ограничение читаемого файла размером 500 Кбайт фактически сводит эту разницу к минимуму — если она вообще будет заметной. Отметим, что при отображении файла в память, естественно, его когда-то в любом случае придется прочесть с диска. Разница та, что теперь это делает за нас система — обращение с файлами через mapping в принципе ничем не отличается от обращения с любыми другими данными — например, массивом или строкой — в памяти, для которой система сама решает, когда и какой объем подгрузить в оперативную память, а какой — оставить на диске, причем сделает это максимально быстро и "прозрачно" для пользователя. В таких системах, как у меня, где оперативной памяти достаточно много (512 Мбайт), это должно сильно ускорять работу — хотя, что означают слова "достаточно много памяти" применительно к современным ОС, точно сказать никто не может.

Перенесем проект Trace в новую папку (Glaval4\2), придадим ему новый номер версии (I.10, не забудем исправить начальный заголовок формы) и объявим две новых глобальных переменных типа integer: fmax и fsize. Переменные xw и fd можно удалить — они нам больше не понадобятся. Замечу в скобках, что удалять ненужные переменные при переделке программы следует всегда — это дает дополнительную гарантию, что ошибки "вылезут" сразу при компиляции и вам не придется долго давить на <F8>, чтобы обнаружить присвоение значения переменной, которая уже вовсе не используется. Кстати, еще одно замечание— иллюстрация к положению, что по мере возможности программу нужно писать сразу по плану. Так. отладка этой программы после переделки (а вы увидите, что изменения не так уж и велики) заняла у меня ровно столько же времени, сколько написание предыдущего варианта. В данном случае я шел на это сознательно (чтобы продемонстрировать, насколько могут различаться по времени работы профаммы, выполненные "по- старому" и "по-новому"), а вообще-то поступать так — пустая трата времени.

В основном цикле, после оператора naii:=nail+i;, добавим следующие две строки:

if sf.Size>?max then continue; (eann больше fMax – не рассматриваем) fsize:=sf.Size;

Они нам сохранят размер текущего файла, который понадобится позже, и заменят проверку размера файла, которая стоит в процедуре ReadFileFormat— нам придется от нес избавиться. Переменная fMax также требует инициализации, и мы создадим обработчик события onCreate формы, в который временно поместим такую строку (потом мы fMax будем инициализировать через настройки):

procedure TForml. Forir,Create (Sender: TObject) ; begin

fmax: =500*102’!; /500 Кбайт} end;

Теперь создадим нашу главную новую функцию для получения строки и памяти — ReadMapFile.

function ReadMapFile:boolean; var

hfile,hMap:THandle; pFile:pointer; pb:"byte; i:integer;

begin (читаем файл в строку sLFile)

result:=True;

try

hFile:=CreateFile (Pchar(fnaiie) .GENERICJREftD, O.nil,

QPENJEXISTING,FILE_ATTRIBUTE_NQRMAL, 0); hMap:=CreateFileMapping(hFile,nil,PAGE_KEADONLY,0,0,nil!; pFile:-MapViewOfFile (hMap, FILE_MAP_FEAD, 0, 0, 0) ; stFiie:-‘1;

for i:=0 to fsize-1 do begin

pb:=pointer(integer(pFile)+i);

(Избавляемся от символов <32, заменяя их на пробелы: ) if рЬЛ>31 then

stFile:=stFile+chr (рЬЛ) else stFUe:-st.File+’ ‘; end;

UnmapViewOfFile(pFi]e); CloseHandle(hMap);

CloseHandle(hFile); (удалили все объекты) except

result:=False; end; end;

Заметьте, что мы не стали разбираться с возможными ошибками по отдельности — нас интересует только общий результат: прочли — отлично, не прочли (вдруг файл занят)— и бог с ним. Процедура формирования строки stFile здесь также далека от совершенства (почему— вы узнаете в главе 21), но нам так в данном случае удобнее— заодно мы избавляемся от лишних символов.

Заметки на полях

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

var pst: Pchar; stFiie: string; pFile: pointer;

pst:=pFile; {Pchar, как указатель) stFile=pst; (Pchar, как строка)

И компилятор, заметьте, не сделает вам ни одного замечания! Мало того, если вы такую вещь проделаете, то даже можете получить при трассировке правильный результат: обе строки будут содержать а себе весь текст файла. Но если вы попробуете поработать всерьез, то скоро программа начнет рушиться в самые непредсказуемые моменты — и это относится не только к данному случаю, например, в предотвращении повторного запуска мы тоже используем не Pchar, а массив символоа, хотя они формально вроде бы совместимы. Анализ показал, что в нашем случае ошибки обусловланы следующим обстоятельством: файловый массиа в памяти, на который будет нацелен указатель pFi]~, соаершенно необязательно заканчивается нулем — это будет так, например, если память а этом месте была до запуска программы заполнена нулями. Но стоит разок-другой попользоваться памятью, как она замусорится, и хотя при присвоении значения указателя на массив в памяти Fchar-строке длина массива формально будет присвоена длине строки (т. е. под нее будет зарезервирована память), но это не будет настоящая строка типа Pchar, т. е. заканчивающаяся нулем. Дело еще и а том, что здесь никто не гарантирует отсутствия нулевого символа внутри файла, и тогда мы просто потеряем всю оставшуюся его часть (см. на эту тему также главу 21). Резюме такое— с Pchar надо обращаться с осторожностью, использовать тип Pchar безоговорочно можно только, если он изначельно создается именно как Pchar, а не через присвоение значения, как указателю. Эту тему мы еща будем более подробно разбирать в главе 21.

Теперь приступим к внесению изменений в остальной части программы и начнем с функции ReadFiieFormat. Внесем в ее заголовок переменную i: integer для перебора элементов строки. После того как мы удалим все, относящееся к чтению из файла, и заменим его на анализ строки (в том числе и фильтрацию по условию (xs=0) or (xofs=0), она нам также не потребуется, все нули отсеяли заранее), эта функция будет выглядеть следующим образом:

function ReadFileFormat: boolean;

var i:integer;

begin

result:=ReadMapFile; Iв stFiie – содержимое файла) if result=False then exit; altn:=0; {в эти переменные будем накапливать статистику) winn:=0; koi8:=0; i: =1;

while i<length(stFiie)-1 do (на 2 меньше длины строки) begin

xofs:=ord(stFile(i]); i:=i+l;

xs:=ord(st Fi le [ i !); i:=i+l; (номер для следующего раза) if (xo?s>127) and (xs>127) then (только русские двухбуквенные сочетания}

………..  (все, как Сило)

end;

nx:=MaxIntValue([altn,winn,koi81); (максимальное из полученного)

……..  (все, как было)

result:=FindString; Iпоиск строки) end;

Оператор ciosefiie(fd) мы в конце также удалили. Теперь внесем аналогичные изменения в функцию FindString, причем необходимые локальные переменные уже имеются:

function FindString:boolean; (поиск строки в файле! var i,j,n:integer;

var ast: array [Q..991 of string; (до 100 слов no CR)

var sttemp:string;

begin

result:=False; (и не надейтесь, что найдем) j:=l;

if (st=’ KOI-8′) or (st=’ cp866′) then (перекодировка) begin

while j<length(stFile) do (на 1 меньше длины строки) begin xs:=ord(stFile[j)) ; if xs>127 then begin

if nx=koi8 then nsl:=koi[xs]; (номер символа в KOI) if nx=altn then nsl:=alt[xs]; (номер символа в Alt) for i:=128 to 255 do (обратная кодировка, ищем символ) if win[i]=nsl then break; (наши его код no Wlnl251) xs:=i; end;

stFile[j1:=chr(xs); j:=j+l; (номер для следующего раза) end;

end; (конец перекодировки)

if Forml.RadioButtonAND.Checked then {ищем no AND)

……..  (все, как было/

end;

Кстати, удалим из строки отображения времени поиска разряды часов, сейчас вы увидите, что они нам больше не понадобятся:

st: = Format:Па 1;йТime t’ hh; nn: ss’, Time-ttold) ;

aeleue (sl, 1, 3); Iудаляем часы – cms нам больше на понадобятся) Forml.Label2.Caption: =1 Время поиска ‘+st;

Рис. 14.3. Результаты работы программы Trace при чтении файлов из памяти

Вот и все доделки: если вы запустите программу теперь, то изумитесь результатам (рис. 14.3): поиск, аналогичный тому, что мы делали в старом варианте, занял всего I минуту 8 секунд — в сорок раз быстрее! Признаюсь, для пущего эффекта я закрыл все посторонние программы, запустив, правда. Trace в отладочном режиме из среды Delphi. Если вы поэкспериментируете, то увидите, что время теперь очень зависит от количества свободной памяти, даже с моими 512 Мбайтами достаточно запустить еще пару-другую "монстров" вроде Word или Photoshop, как время поиска увеличится в разы. Но, конечно, таких умопомрачительных "тормозов", как при прямом чтении с диска, мы уже не получим. Кстати, на фоне общего увеличения скорости поиска теперь будет очень заметно, как тормозит браузер при загрузке странички — особенно в конце, когда нужно перейти на метку. Но тут я ничего поделать не могу — к тому же эффект этот заметен только в Windows 98, в ХР все перезагружается значительно быстрее.

Заметки на полях

Казалось бы, при перекодировке нам логично, как мы делаем это в той же процедуре далее, воспользоваться парой процедур:

delete(stFiie, j, 1); insert(chr(xs!,StFiie,j);

Предостерегаю — если вы так поступите, то вместо того, чтобы ускорить процесс. вы его резко замедлите. Представьте себе, как работают процедуры delete и insert: для того чтобы удалить единственный символ, надо "перелопатить" всю строку, сдвинув все, что было после удаленного символа, а затем повторить это в обратную сторону для insert, и так каждый символ — а если их полмиллиона? Отметим заодно, что операция конкатенации (складывания строк), в отличие от delete и insert, выполняется бопее быстро, хотя там тоже есть свои "тараканы" (см. на эту тему главу 21). Но вы спросите — а как же двльше, там ведь мы используем delete и insert для удаления HTML-тегов? Во-лервых, там строки однозначно короче, во-вторых, мы делаем это всего один-два раза, а не с каждым байтом строки, так что тут на замедление можно не обращать внимвния. А вот повторение разбора строки поиска каждый раз при поиске по "OR" — не здорово, конечно, и тут кроется резерв для небольшого увеличения быстродействия, который в данном случае, в отличие от варианта с чтением с диска, стоит принять во внимание. Увеличение это, однако, будет настолько гомеопатическим (можете проверить), что можно и не усложнять программу, особенно учитывая то, что время поиска значительно в большей степени зависит от наличия свободной памяти. Да и про ProgressBar не забудем.

На рис. 14.3 вы уже можете видеть меню с пунктами Настройки, Справка и Загрузить результаты. Справкой мы займемся в главе 16, а сейчас давайте решим вопрос с настройками и результатами поиска.

Настройки

Составим полный список того, что именно мы хотим сохранить в настройках.

1.    Состояние кнопок RadioButtonAND и RadioButtonl (достаточно двух веду- щих).

2.      Максимальный размер файла для поиска (шах).

3.      Последнюю папку (возможно, с маской имени файла), в которой осуществлялся поиск.

4.      Строку с заданием запрещенных расширений файлов.

Исключительно ради последнего пункта нам следует здесь ввести кнопочку Вернуть установки по умолчанию — строка длинная и специфическая, и пользователь может ее так "заредактировать", что сам забудет, что к чему. Сохранять ли строку поиска — вопрос дискуссионный, и мы его еще обсудим чуть ниже.

Итак, добавим модуль IniFiles в uses, создадим переменную iniFiie:TiniFi.le и сделаем следующий фокус: создадим строковую переменную stExt, а существующую константу stExt назовем stDefExt, в тексте программы при этом ничего менять не придется. Перепишем уже существующий у нас обработчик onCreate формы таким образом:

procedure TForml.FormCreate(Sender: TObject); begin

IniFile:=TIniFile.Create(ChangeFileExt(ParamStr(0),’.ini’)); (если не было – создаем, иначе открываем) with IniFile do begin

if SectionExists(‘Main’) then

(если файл и секция в нем уже есть)

begin

if ReadBool(‘Main’,’AND’,True) then RadioButtonAND.Checked:=True else RadioButtonOR.Checked:=True; if ReadBool (‘Main’, ‘ ReadDirOr.ly’,True) then RadioButtonl.Checked:=True else RadioButton2.Checked:=True; Editl.Text:=ReadString(‘Main’,’Path’,’C:\’); fMax:=ReadInteger(‘Main’,’MaxSizeOfFile’,500*1024); stExt:=ReadString(‘Main’,’Taboo extension’,stDefExt); end else /если секция еще не создана) begin

WriteBool(‘Main’,’AND’,True); WriteBool(‘Main’,’ReadDirOnly’,True); WriteString(‘Main’,’Path’, ‘C:\’); Writelnteger(‘Main’,’MaxSizeOfFile’,500*1024); WriteString(‘Main’,’Taboo extension’,stDefExt); stExt:=stDefExt; fMax:=500*1024; end;

Destroy; end; end;

Отлично, теперь реализуем запоминание текущих установок. В процедуре Forml. Des troy запишем:

procedure TForml.FormDestroy(Sender: TObject); begin

IniFile:=TIniFile.Create(ChangeFileExt{ParamStr(0), ‘.ini’)); I открываем INI)

with IniFile do

begin

WriteBool(‘Main1,’AND’,RadioButtonAND.Checked); WriteBool(‘Main’,’ReadDirOniy’.RadioButtonl.Checked); WriteString(‘Main’,’Path’,Editl.Text) ; Writelr.teger (‘Main’, ‘MaxSizeOfFile’, fMax); WriteString(‘Main’,’Taboo extension’,stExt); Destroy; end;

if FileExists(ftempname) then

erase (fteirphtm); {уничтожаем временный файл)

end;

Вернемся тут к вопросу — сохранять ли строку поиска? Я не стал этого делать, ориентируясь на то, что в большинстве случаев пользователь не настолько рассеян, чтобы закрывать программу с нужной ему строкой, и при начальной загрузке она будет только мешаться. Но если читатель решит, что сохранять надо— тогда имеет смысл не уничтожать временный файл ftempname, содержащий результаты поиска по этой строке, а также загружать его при начальном запуске программы — это будет и логичное и красивое решение. Подобным образом поступают многие программы. Самым грамотным решением было бы даже ввести отдельную установку — сохранять строку/не сохранять.

Теперь реализуем установки для fMax и stExt по ходу работы с программой. Я еще раньше, как вы видели на рис. 14.3, создал меню с пунктом Настройки. Данный пункт я назвал seti и присвоил ему горячую клавишу <F2> (хотя в главном меню это и не отображается, и даже всплывающую подсказку (свойство Hint), которая тут как раз позарез нужна, для MainMenu создать просто так не удастся). Поместим на форму, как и в главе 9, компонент panel (он получит имя Panelз), и сразу установим для него свойство visible в False. После того как мы расположим на этой панели нужные компоненты, она приобретет вид, показанный на рис. 14.4. Тут для разнообразия мы использовали для ввода чисел упоминавшийся ранее компонент SpinEdit (закладка Samples: у него надо установить свойство MinValue в 1, a Maxvalue в 100 ООО — больше, чем 100-мегабайтный файл, вряд ли кто-то захочет просматривать).

Рис. 14.4. Панель настроек программы Trace

Далее напишем четыре обработчика событий:

procedure TFormL.SetlClick(Sender: TObject); begin (пункт маню Настройки) Edit3.Text:=stExt; (установки на панели настроек} SpinEditl.Value:=fMax div 1024; Panel3.Visible:=True; (панель с настройками) SpinEditl.Set Focus; end;

procedure TForml.SpinEditIKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);

begin

(нажатие на Enter s SpinEdit и Edit3 после очередного ввода)

if key=vk_Retutn then

begin

FindNextControl(Sender as TWinControl, true, true,

false).SetFocus; Edit3.SelStart:=length(Edit3.Text); Edit3.SelText:=’1; (в Edit3 курсор – в конец текста) end; end;

procedure TForml.Button4Click(Sender: TObject); begin (Ok на панели настроек) stExt:=Edit3.Text; fMax:=SpinEdi 11.Vnlue*1024 ;

Pane 13.Visible:=I;‘a 1 se; (прячем панель с настройками) end;

procedure TForml.ButtonbClick(Sender: TObject); begin I вернуть умолчания) Edit 3.Text:=stDefExt; SpinEdi 11.Va1ue:=500; end;

По первому из них (щелчок на пункте меню Seti Настройки) панель показывается, и фокус устанавливается в spinEditi. Второй обработчик— общий для обоих редакторов при событии onKeyDown, если нажата клавиша <Enter> (для Edit3 надо объявить эту процедуру для того же события через закладку Events). В нем не только передается фокус ввода, но и курсор в Edit3 устанавливается в конец строки — так меньше вероятность что-то испортить и удобнее строку разглядывать. Третий— щелчок на кнопке Button’. Ok, закрывает панель и устанавливает величины. В результате, если мы откроем панель (<F2>), при троекратном нажатии <Enter> она закроется без каких-то изменений в величинах. Наконец, четвертая процедура — обработчик события нажатия на кнопку Buttons Вернуть умолчания — устанавливает наши "умолчательные" значения. Программа эта не такая "попсовая", как Slide- Show, поэтому возиться с закрытием панели по щелчку вне ее я не стал — если читатель желает, он может доделать и это самостоятельно (образец см. в главе 9).

Кроме этого, в меню уже имеется и пункт Загрузить результаты (p.esi), который должен делать то, что в предыдущем варианте делала кнопка Button-i. Кнопку мы удалим, для Labei2 в свойстве constraints.Maxwidi.il опять запишем 0, чтобы она могла растягиваться до конца формы. Заменим в двух местах Button4 на Real. Выводить диалог "Сохранить результаты" каждый раз неудобно— лишнее действие для пользователя, поэтому мы изменим концовку процедуры поиска, удалив из нее вызов MessageBox, и запишем вместо него такой оператор:

if nfile>0 then fесли нашли больше одного1

Resi.Caption:=’Сохранить результаты’ else Res1.Caption:=’Загрузить результаты';

Теперь создадим обработчик обращения к пункту меню Загрузить результаты Resiciick, перенесем в него содержимое обработчика нажатия на кнопку Button4 и дополним его вызовом диалога по условию, что заголовок в меню предлагает сохранение:

procedure TForrnl .ReslClick (Sender: TObject); begin {результаты}

if Resl.Caption=’Сохранить результаты’ then {если поиск закончилсяI

begin

Resl.Caption:=’Загрузить результаты'; if Application.MessageBox(‘Сохранить результаты

поиска?’, ‘ ‘ ,snb_OKCANCEL) = idOK then Savehtmlfile; exit; (сохраняем и выходим! end;

{загрузить результата:J WebBrowserl.0nBeforeNavigate2:=nil; If OpenDialogl.Execute then

WebBrowserl.Navigate(Pchar(OpenDialogl.FileName)); Application.ProcessMessages;

WebBrowserl.OnBeforeNavigate2:=WebBrowserlBeforeNavigate2; end;

И чтобы уж совсем довести программу до ума, создадим еще и такой обработчик события onciose формы, чтобы пользователь не забыл сохранить результаты:

procedure TForml.E’ormClose(Sender: TObject; var Action: TCloseAction);

begin {при закрытии формы)

if Resl.Caption=’Сохранить результаты’ then

{если поиск закончился) if Application.MessageBox(‘Сохранить результаты

поиска?1. " ,rab_OKCANCEL) = idOK then Savehtmlfile; end;

Справку мы. как я уже говорил, сделаем в главе 16 (вместе с другими справками), а здесь я только отмечу, что программа у нас получилась ничуть не хуже фирменных, особенно если довести до ума некоторые детали, до которых здесь просто руки не дошли. На некоторые я обращал внимание по ход\ дела, но вот еще, например: при поиске программа найдет не только файлы i отдельными словами, но и все те, в которых заданное слово содержится в составе более длинных слов. Так что если вы зададите, скажем, поиск фразы "про Delphi" с условием "любое из слов", то получите фантастическое количество мусора (сочетание "про" статистически самое распространенное из трехбуквенных сочетаний). И однозначно решить эту проблему нельзя: в равных случаях может потребоваться как поиск отдельного слова (режим "только слово целиком"), так и поиск фрагмента в составе более сложных ело». В плане интеллектуальных способностей программу можно совершенствовать до бесконечности, главное только — не переборщить.

Источник: Ревнч Ю. В.  Нестандартные приемы программирования на Delphi. — СПб.: БХВ-Петербург, 2005. — 560 е.: ил.

По теме:

  • Комментарии