ПАСКАЛЬ: БУФЕРИЗАЦІЯ ВВЕДЕННЯ-ВИВЕДЕННЯ
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.