Идея буферизации

 

1. Идея буферизации

Девушке Гали надо несколько стаканов воды, чтобы полить цветы на окне. Но она почему-то идет к колодцу с двумя огромными ведрами. Путь туда не совсем короткий, ведра тяжелые, но принеся их, Галя не пойдет к колодцу по следующим стаканами, пока не исчерпает из ведер всю воду.

Такие же соображения лежат в основе организации обмена данных между физическими файлами и переменными программы. Физический файл можно сравнить с колодцем. Добыча воды из него требует много времени, и выгодно это делать не слишком часто и не совсем малыми дозами. В роли ведра во время выполнения программы выступает буфер — специальная участок пам 'памяти программы, предоставляемой каждой файловой переменной при ее зв. Связывание.

Сначала рассмотрим физические файлы на диске. Дисковая память разбивается на блоки — участки фиксированного размера (чаще всего, по 512 байт). Устройства обмена (дисководы) создан так, что именно блоками данные копируются на диск или с диска.

Блок является единицей физического обмена между диском и оперативной памяти 'яттю. Когда выполняется вызов подпрограммы чтения, в буфер копируется целый блок или несколько блоков данных из файла, и к переменным значения попадают не из файла, а из буфера. При следующих чтений никаких перемещений данных между диском и оперативной памяти 'яттю нет, а значения берутся из буфера.

Размер буфера определяется операционной системой. Как правило, это 2, 8, 16 или даже больше блоков.

С каждым буфером свя связана дополнительная переменная, которая указывает на его текущий элемент. Именно его значение копируется при чтении, а текущим становится следующий за ним. Можно считать, что текущий элемент в буфере выступает представителем доступного элемента файла. Если весь буфер прочитано, то при очередной попытке чтения следующие несколько блоков файла копируются в буфер.

Подчеркнем, что буфер, как и указатель текущего элемента, в Паскаль-программе явно не обозначается и не используется.

Буфер можно рассматривать как своего рода окно, сквозь которое из программы видно файл. Если физический файл меньше буфера по размеру, то есть весь файл "умещается в окне", то все чтения с него требуют лишь одного обращения к файлу.

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

Заметим, что организация буферов текстов в Турбо Паскале имеет дополнительные особенности, которые рассматриваются далее. Например, о текстах нельзя утверждать, что незаполненный буфер сбрасывается по окончании программы. Но буферы файлов всех типов сбрасываются при выполнении процедуры закрытия close. Вот почему стоит работу с файлом описывать в "скобках", образованных вызовами процедур открывания и закрывания.

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

2. Буферизация текстов

С текстом свя связано не один, а два буфера. Первый, внешний, обрабатывается согласно написанного в предыдущем подразделе. Работа со вторым, внутренним, ведется иначе. При чтении данные копируются из текста во внешний буфер, а оттуда часть их копируется во внутренней. Какая именно часть, зависит от размера внутреннего буфера. При чтении символы текста на самом деле берутся из внутреннего буфера, а когда он иссякает, то у него копируется следующая часть внешнего буфера (возможно, с обращением к физическому файла), и чтение продолжается.

Размер внутреннего буфера текстов устанавливается в системе программирования в 128 байт. Программист имеет возможность изменить его в пределах от 1 до 65536 байт вызовом процедуры SETTEXTBUF вида

settextbuf (f, Buf, Bufsize)

или

settextbuf (f, Buf),

где f — им 'я файловой переменной типа text, Buf — им' я переменной, тип которой несущественный, а Bufsize — выражение целого типа со значением в пределах 1 ... 65535. Такой вызов следует записывать после зв. Связывание файла перед установкой его в исходное состояние (чтения или записи).

Переменная Buf используется как внутренний буфер, поэтому целесообразно, чтобы ее длина была кратной длине блока. Если размер буфера (в байтах) Bufsize в вызове не указано, то он определяется длиной переменной Buf. Если Bufsize указано и меньше длины переменной Buf, то оно задает длину буфера в пределах переменной Buf. Но если Bufsize больше длины Buf, то переменные, расположенные в памяти 'памяти напрямую Buf, используются во буфер, и это может привести к непредсказуемым последствиям.

Пример 15.1. Рассмотрим программу

program GreatBufferManager;

var f: text; Hugebuf: array [1 .. 2] of char;

x, y: char; s: string [4];

begin

assign (f, 'huge.dat'); settextbuf (f, hugebuf, 4);

x: = 'x'; y: = 'y';

reset (f); readln (f, s);

writeln ( 'x =', x,; y = ', y);

readln;

end.

Если первой строкой текста в файле huge.dat есть 'qwer', то за выполнение этой программы на экране 'появится совсем не ожидаемое

x = x; y = y,

а на первый взгляд довольно странное

x = e; y = r.

Дело в том, что переменные x и y физически расположены непосредственно за массивом Hugebuf, и чтение четырех символов строки файла в этот буфер приводит к заполнению не только массива, но и переменных за ним. Если сделать строка немного длиннее, то, уверяем читателя, результаты будут еще неожиданными. Но не увлекайтесь, это может стать опасным для программы GreatBufferManager.

При выводе в текст символы накапливаются во внутреннем буфере, который сбрасывается во внешний буфер в случае заполнения или выполнения процедуры writeln или close. Можно также задать принудительное сброса внутреннего буфера текста f вызовом процедуры FLUSH (f). Его следует записывать для периодического выполнения, а также после всех выводов в файл. Подчеркнем, что при вызове процедуры flush лишь напоминает внутренний буфер во внешней. Сброс внешнего буфера при этом происходит только в случае его заполнения.

Если в конце работы с файлами не указать вызовов flush или close, то содержание внутреннего буфера так и не попадет во внешней буфер и в файл.

Пример 2. Кажется, следующая программа задает копирования текстов:

program wrongcpy;

var f, g: text; c: char;

begin

assign (f, ...); assign (g, ...); reset (f); rewrite (g);

while not eof (f) do

begin read (f, c); write (g, c) end;

(здесь не хватает close (g)! Хотя и close (f) не помешает ...)

end.

Попробуйте эту программу запустить, и увидите, что если исходный файл — песня, то файл-"копия" — тоже песня, но Недопетая. А все потому, что "конец песни" так и остается во внутреннем буфере.

Переменную во внутренний буфер стоит означать глобальным в программе. Если обозначить и свя связать файловую переменную в программе, а ее внутренний буфер обозначить в подпрограмме, то по окончании вызова подпрограммы файловая переменная будет доступной, а ее буфер — нет. Попытка сброса с такого буфера после окончания программы может привести к непредсказуемым последствиям. Но если вся работа с файлом, от assign к close, описанная в подпрограмме, и буфер вполне естественно обозначить в ней же.

Пример 3. Рассмотрим программу процедуре spoilbuf, т.е. "испортить буфер", при вызове которой изменяется буфер, который остается в локальной памяти 'памяти после окончания предыдущей процедуры fillbuf.

program foolish;

var f: text;

procedure fillbuf;

var buf: array [1 .. 5] of char;

begin

settextbuf (f, buf); rewrite (f); write (f, 'abcdefgh');

end;

procedure spoilbuf;

begin end;

begin

assign (f, 'boo.dat'); fillbuf; spoilbuf;

close (f)

end.

При выполнении вызова fillbuf символы abcde заполняют внутренний буфер и сбрасываются во внешней. Затем, уже при исполнении close (f) они с появляются в файле boo.dat. Но символы fgh остаются во внутреннем буфере после окончания fillbuf и портятся при исполнении spoilbuf. Испорченный буфер сбрасывается при закрытии файла f, и затем вместо fgh мы видим в файле то совсем на них не похоже.

Задачи

15.1. Написать процедуру копирования текстов с собственными внутренними буферами размером в 16 блоков, то есть 8192 байта, или 8K.

15.2. Написать процедуру побайтового сравнение текстов с собственными внутренними буферами.

3. Буферизация экрана и клавиатуры

Экран и клавиатура являются текстами, свя связанными с файловыми переменными output и input. Для работы с ними также употребляются буферы.

Сначала рассмотрим экран. С ним свя связан буфер, но символы, попав в него, сразу копируются на экран. Если бы этого не было, информация на экране 'являлась бы с нежелательными задержками.

При выполнении процедуры WRITE по значению ее каждый аргумент вычисляется стала, то есть последовательность символов, которые через буфер сразу выводятся на экран. На самом деле, вызов

write (E1, E2, ..., EN)

выполняется как последовательность вызовов

write (E1); write (E2); ...; write (EN).

Выполнение writeln отличается тем, что в буфер экрана "добавляется eol", и курсор переводится в следующую строку.

Организация работы с клавиатурой намного сложнее. Символы, образованные нажатием клавиш, накапливаются в буфере клавиатуры. Он вмещает 15 символов. В этом можно убедиться, запустив программу

uses crt;

begin delay (16000) end.

При ее выполнении в течение 16 секунд нетрудно успеть нажать какую клавишу 16 раз и по последнего нажатия услышать звуковой сигнал комп 'ютера, что свидетельствует о переполнении буфера клавиатуры. Буфер переполняется, поскольку за выполнение этой программы символы из него не переносятся во внутренний буфер. Кроме того, набранные далее символы не отображаются на экране и вообще "исчезают".

Перенос символов во внутренний буфер происходит за выполнение процедур чтения readln и read. Как и для других текстов, его размер 128 байт. В этом можно убедиться, запустив программу

begin readln end.

По ее выполнения комп 'ютер начинает ждать нажатий на клавиши. Каждое нажатие на клавишу (кроме Enter) приводит к появлению соответствующего символа в буфере клавиатуры. Этот символ сразу переносится в ее внутренний буфер и через экранный буфер без задержки отображается на экране. После того, как набрано 128 символов, следующий символ во внутренний буфер не переносится и на экране не с 'является. Вместо этого можно услышать звуковой сигнал, свидетельствующий о переполнения внутреннего буфера.

Нажатие на клавишу Enter ведет к появлению соответствующего символа в буфере клавиатуры и перевода курсора в новую строку экрана. Когда этот символ 'является во внутреннем буфере, строчку в нем рассматривается как "завершен символом eol".

Завершен строку во внутреннем буфере анализируется и по устоявшимся в нем вычисляются значения базовых типов и присваиваются переменным, указанному в вызове read (readln). Если постоянных меньше, чем переменных в вызове, то выполнение продолжается, то есть внутренний буфер спорожнюеться и начинается ожидание новых символов с клавиатуры.

Когда в очередном строке проанализированы последнюю постоянную, текущим становится следующий за ней символ во внутреннем буфере. Выполнение процедуры read на этом заканчивается. По следующего выполнения процедуры чтения новые символы будут добавляться к содержанию внутреннего буфера, но поиск и анализ постоянных начнется от текущего символа буфера, оставшийся от предыдущего вызова read. Особенность процедуры readln заключается в том, что после анализа последней устойчивой остальные символы во внутреннем буфере пропускаются вместе с ближайшим eol, то есть фактически буфер сбрасывается.

По вызова функции EOF анализируется внутренний буфер клавиатуры. Если он пуст, то выполнение программы приостанавливается до ближайшего нажатия клавиш. Тогда символы из буфера клавиатуры переносятся во внутренний к появлению Enter. При наличии символов во внутреннем буфере анализируется первых, текущий символ. Если он соответствует сочетанию клавиш Ctrl-Z, которым задается конец файла на клавиатуре, то с вызова eof возвращается значение true. По другой первых символа, т.е. при нажатии клавиши, отличной от Ctrl-Z, возвращается false.

Пример. Пусть действует определение var V: integer, а клавиши не нажимались до начала выполнения такого фрагмента программы:

V: = 0;

while not eof do

begin

write ( 'Введите целое число>'); read (V)

end;

writeln ( 'V =', V: 1)

При выполнении eof компьютер ждет нажатий на клавиши. Приглашение к вводу числа на экране еще не с 'появилось. Если нажать Ctrl-Z и Enter, то с вызова eof возвращается false, и выполнение фрагмента заканчивается печатанием текста V = 0. По нажатий цифровых клавиш цифры отображаются на экране и накапливаются во внутреннем буфере клавиатуры. После нажатия на Enter выполнения eof заканчивается и возвращается значение false. После этого, т.е. лишь после набора на клавиатуре первые постоянной (.) Выполняется тело цикла и появляется сообщение 'input number>'.

При выполнении read лишь анализируются символы, накопленные во внутреннем буфере за выполнение вызова eof. Если они образуют постоянную, то соответствующее значение присваивается переменной V, после чего повторяется вызов eof т.д. Итак, ввод символов с клавиатуры в таком цикле происходит за вызовов eof, а не read! Таким образом, чтобы приглашение печаталось до начала введения первой постоянной, стоит перед циклом добавить вызов write ( 'input number>').

Еще раз вернемся к употребления процедуры readln вместо read. Если при выполнении приведенного цикла за очередной постоянной после пропуска случайно набрать непустой символы, не задают постоянную, то они останутся во внутреннем буфере. Далее с вызова eof вернется false, и анализ этих символов за выполнение read приведет к аварийному завершению программы. Если же вместо read записать readln, то после обработки постоянной эти символы пропускаются, так как набираются перед Enter, и программа выполняется нормально.

15.4. Тип безтипових файлов

Рассмотрим программу посимвольно копирования файлов:

program StupidCopy;

var f, g: file of char; c: char; s: string;

begin

writeln ( 'Задайте имя исходного файла');

readln (s); assign (f, s);

writeln ( 'Задайте имя целевого файла');

readln (s); assign (g, s);

reset (f); rewrite (g);

while not eof (f) do

begin

read (f, c); write (g, c);

end;

close (f); close (g);

end.

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

Попробуйте запустить ее на исполнение, указав входным файл размером в несколько сотен килобайт — выполнение займет секунды и десятки секунд. Напрашивается вывод, что при ее выполнении несколько осуществляется не лучшим образом. Рассмотрим один из способов ускорения работы с файлами.

Система Турбо Паскаль позволяет создать дополнительный собственный буфер и собственноручно описать его применения. Это оказывается гораздо более эффективным от использования буферов, обеспечиваемых системой. А реализуется это с помощью безтипових файлов.

Тип безтипових файлов задается словом file. Файловую переменную этого типа, как и всех других файловых типов, надо сначала свя связать с физическим файлом и открыть, установив в исходное состояние для чтения или записи. Процедуры открывания RESET и REWRITE здесь имеют по 2 параметры. Кроме имени файловой переменной, в их вызова указывается размер "внешнего буфера" в байтах. Этот буфер еще называется блоком и явно в Паскаль-программе не сказывается. Через него данные копируются по физическому файле "внутреннего буфера". Размер блока может быть в пределах от 1 до 65535. Как ни странно, лучше всего установить его равным 1:

ReSet (f, 1) или ReWrite (g, 1).

Почему именно так, мы скажем ниже.

Вся дальнейшая обработка безтипового файла описывается совсем другими средствами.

Чтение безтипових файлов задает процедура BLOCKREAD с четырьмя параметрами. Все они, кроме третьего — параметры-переменные. В вызове процедуры первым аргументом является им 'я файловой переменной, второй задает место в памяти' памяти, с которого начинается "внутренний буфер", третья — количество блоков, которые надо прочитать из файла, а в четвертом, типа Word, возвращается количество блоков, которая на самом деле читается за выполнение вызова. Например, с определениями

var f: file;

inbuf: array [1 .. 100] of char;

blsz: Longint; numbl, numblre: Longint

и операторами и вызовами

blsz: = 4; numbl: = 25;

reset (f, blsz); blockread (f, inbuf, numbl, numblre)

размер блока устанавливается равным 4 байта, и 25 таких блоков надо прочитать с начала файла. Если размер файла на самом деле не менее 4 * 25 = 100 байт, и никаких ошибок при чтении не было, то значением переменной numblre также будет 25. После чтения массив inbuf будет заполнен до конца, и надо будет обработать его в зависимости от конкретной задачи. Кроме того, при выполнении следующего вызова этой процедуры файл f будет читаться со 101-го байта.

Следовательно, для безтипових файлов понятие "доступен элемент" заменяется на "доступен байт".

Возможно, в файле меньше 100 байт или при чтении то случилось, и в действительности прочитано меньше, чем указанные 25 блоков. Тогда значение переменной numblre будет не равным 25. После вызова можно задать проверку numblre = numbl и соответствующие действия в случае неравенства.

Если задать чтения количества блоков, меньшей 25, то массив inbuf будет заполнен не до конца, а если большей — то заполнится массив inbuf и соответствующее количество переменных, расположенных за ним в пам 'памяти программы. Поскольку переменные располагаются там в порядке определения, первыми "жертвами" в данном случае станут переменные blsz, numbl, numblre. Они имеют тип Longint и занимают по 4 байта, поэтому за выполнение blockread (f, inbuf, 26, numblre) будет испорчена лишь первый из них, по blockread (f, inbuf, 27, numblre) — первые два т.д. Значит, надо быть особенно внимательным при записи вызова.

Если блок, или "внешний буфер" не заполняется до конца, то количество блоков, реально прочитанных, будет меньше заданного количества. Таким образом, во избежание неприятностей надо обеспечить, чтобы размер файла делился на размер блока. Поскольку размер блока на самом деле не влияет на скорость чтения, лучше предоставлять ему значение 1. Тогда проблем не будет за любого размера файла.

Понятно, что когда обрабатывается файл записей фиксированного размера, то этот размер можно задавать и для блока. Например, записи типа

Student = record

Sname, Name: string [20];

Ball: real

end

имеют размер 21 +21 +6 = 48 (байт). Именно это значение возвращается из вызова функции

SizeOf (Student).

И вообще, с вызова вида SizeOf (им 'я-типа) возвращается количество байтов, занимающихся значениями этого типа, например,

SizeOf (char) = 1, SizeOf (integer) = 2

и т.д. Итак, файл f записей типа Student можно открыть вызовом

ReSet (f, SizeOf (Student)).

После этого вызов вида

BlockRead (f, Buf, n, nreal)

задает чтение n блоков по 48 байт в память переменной Buf.

Главную роль в скорости чтения безтипових файлов играет размер "внутреннего буфера". Чем он больше, тем меньше обращений к внешнего носителя и быстрее обработка файла. Но все хорошо в меру.

Можете проверить утверждение, что за размеров буфера, кратных 512 байтам и больших 8K байт, скорость чтения файлов практически стала.

Процедура блочного вывода BLOCKWRITE также имеет 4 аналогичные параметры. Отличие ее в том, что данные из "внутреннего буфера" через блок записываются в конец файла. Разумеется, сначала для файла надо установить размер "внешнего" буфера вызовом вида ReWrite (f, m).

Вернемся к задаче копирования и напишем программу, выполнение которой в сотни (!) Раз быстрее программы StupidCopy. В роли "внутреннего буфера" выступает массив символов Buf размером в Bufsz = 32K байтов. Сначала по вызову FileSize определяется размер входного файла в байтах, а затем файл читается в массив порциями по Bufsz байтов. Обработка этого буфера в данном случае заключается в блочном копировании в выходной файл. Последняя порция может содержать меньше, чем Bufsz байт — массив заполняется и переписывается в файл не до конца.

program QuickCop;

const Bufsz = 32768;

var f, g: file;

Buf: array [1 .. Bufsz] of char;

restfil, portion: Longint;

rdin, wrou: word; s: string;

begin

writeln ( 'Задайте имя файла-источника:');

readln (s); assign (f, s);

writeln ( 'Задайте имя целевого файла:');

readln (s); assign (g, s);

reset (f, 1); rewrite (g, 1);

restfil: = filesize (f);

while restfil> 0 do

begin

if restfil> Bufsz then portion: = Bufsz

else portion: = restfil;

dec (restfil, portion);

Blockread (f, Buf, portion, rdin);

if rdin <> portion then

begin

writeln ( 'Ошибка чтения файла'); halt

end;

Blockwrite (g, Buf, portion, wrou);

if wrou <> portion then

begin

writeln ( 'Ошибка записи файла'); halt

end;

end;

close (g); close (f);

end.

Два замечания относительно этой программы. Во-первых, в нее можно добавить исчисления времени, которое занимает обработка файла. Для этого следует задать в начале программы подключения модуля Dos и воспользоваться его процедурой GETTIME. Следует обозначить 4 переменные типа Word, например,

th, tm, ts, tms: word.

Можно записать вызов

Gettime (th, tm, ts, tms)

еще в начале тела программы, например, перед открытием файлов. По его выполнения переменным присваиваются соответственно часы, минуты, секунды те миллисекунды от встроенного в комп 'ютер часов.

Обработка значений этих переменных зависит от вкусов программиста. Например, по ним можно вычислить время в сотых долях секунды. Определите переменную tim типа longint:

tim: = ((th * 60 + tm) * 60 + ts) * 100 + tms div 10;

В конце программы запишем

gettime (th, tm, ts, tms);

tim: = ((th * 60 + tm) * 60 + ts) * 100 + tms div 10 — tim;

writeln ( 'Израсходовано времени:', (tim div 100): 1, '',

(tim mod 100 div 10): 1,

(tim mod 100 mod 10): 1, 'sec'

)

Тогда печатается время выполнения в секундах вроде 3.62 или 0.01.

Второе замечание касается способа задания имен файлов при выполнении программы. Заставлять пользователя набирать их каждый раз на клавиатуре — не лучший вариант. Система Турбо Паскаль позволяет задавать имена файлов в командной строке вызова программы и читать их отсюда с помощью функции PARAMSTR. Например, если вызов программы QuickCop записать в виде

QuickCop file.in file.out

то строка 'file.in' является значением, возвращающегося с вызова ParamStr (1), 'file.out' — ParamStr (2). В таком случае зв. Связывание файлов можно задать так:

assign (f, ParamStr (1));

assign (g, ParamStr (2)).

И вообще, пусть словом считается последовательность символов, отличных от пропуска. Слова после названия программы в командной строке есть строками, возвращающихся из вызовов ParamStr с соответствующими номерами. Количество слов возвращается из вызова функции PARAMCOUNT (без аргументов).

Следовательно, если пользователь программы QuickCop не задал имена файлов в командной строке, можно заставить его задать их с клавиатуры, написав в начале программы то вроде:

case ParamCount of

0: begin

writeln ( 'Задайте имя входного файла');

readln (s); assign (f, s);

writeln ( 'Задайте имя целевого файла');

readln (s); assign (g, s);

end;

1: begin

assign (f, ParamStr (1));

writeln ( 'Задайте имя целевого файла');

readln (s); assign (g, s);

end

else

begin

assign (f, ParamStr (1)); assign (g, ParamStr (2));

end

end.