C++ 3d.Комментарии

7b6e9298

Ввод символов


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

Вынужден вас огорчить: определенные стандартом потоки C++ заявленным свойством не обладают. Они всегда работают медленнее C, а в некоторых реализациях -- медленно до смешного (правда, объективности ради стоит отметить, что мне попадались и совершенно отвратительно реализованные FILE* потоки C, в результате чего C++ код вырывался вперед; но это просто недоразумение, если не сказать крепче!). Рассмотрим следующую программу: #include <stdio.h> #include <time.h> #include <io.h> // для open() #include <fcntl.h> #include <iostream> #include <fstream>

using namespace std;

void workc(char*); void workcpp(char*); void work3(char*);

int main(int argc, char **argv) { if (argc==3) switch (*argv[2]-'0') { case 1: { workc(argv[1]); break; } case 2: { workcpp(argv[1]); break; } case 3: { work3(argv[1]); break; } } }

void workc(char* fn) { FILE* fil=fopen(fn, "rb");

if (!fil) return;

time_t t1; time(&t1);

long count=0; while (getc(fil)!=EOF) count++;

time_t t2; time(&t2);

fclose(fil); cout<<count<<" bytes per "<<t2-t1<<" sec.\n" ; }

void workcpp(char* fn) { ifstream fil(fn, ios_base::in|ios_base::binary);

if (!fil) return;

time_t t1; time(&t1);

long count=0; while (fil.get()!=EOF) count++;

time_t t2; time(&t2); cout<<count<<" bytes per "<<t2-t1<<" sec.\n" ; }



class File { int fd; // дескриптор файла unsigned char buf[BUFSIZ]; // буфер стандартного размера unsigned char* gptr; // следующий читаемый символ unsigned char* bend; // конец данных

int uflow(); public: File(char* fn) : gptr(0), bend(0) { fd=open(fn, O_RDONLY|O_BINARY); } ~File() { if (Ok()) close(fd); }


int Ok() { return fd!=-1; }

int gchar() { return (gptr<bend) ? *gptr++ : uflow(); } };

int File::uflow() { if (!Ok()) return EOF;

int rd=read(fd, buf, BUFSIZ); if (rd<=0) { // ошибка или EOF close(fd); fd=-1;

return EOF; }

gptr=buf; bend=buf+rd;

return *gptr++; }

void work3(char* fn) { File fil(fn);

if (!fil.Ok()) return;

time_t t1; time(&t1);

long count=0; while (fil.gchar()!=EOF) count++;

time_t t2; time(&t2);

cout<<count<<" bytes per "<<t2-t1<<" sec.\n" ; }

Ее нужно запускать с двумя параметрами. Первый параметр -- это имя (большого) файла для чтения, а второй -- цифра 1, 2 или 3, выбирающая функцию workc(), workcpp() или work3() соответственно. Только не забудьте про дисковый кэш, т.е. для получения объективных результатов программу нужно запустить несколько раз для каждого из вариантов.

Необычным местом здесь является функция work3() и соответствующий ей класс File. Они написаны специально для проверки "честности" реализации стандартных средств ввода-вывода C -- FILE*. Если вдруг окажется, что workc() работает существенно медленнее work3(), то вы имеете полное право назвать создателей такой библиотеки, как минимум, полными неучами.

А сейчас попробуем получить информацию к размышлению: проведем серию контрольных запусков и посмотрим на результат.

И что же нам говорят безжалостные цифры? Разница в разы! А для одного широко распространенного коммерческого пакета (не будем показывать пальцем) она порой достигала 11 раз!!!

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

Для C с его getc() в типичной реализации мы имеем: #define getc(f) ((--((f)->level) >= 0) ? (unsigned char)(*(f)->curp++) : _fgetc (f))

Т.е. коротенький макрос вместо функции. Как говорится -- всего-ничего. А вот для C++ стандарт требует столько, что очередной раз задаешься вопросом: думали ли господа-комитетчики о том, что горькие плоды их творчества кому-то реально придется применять?!



Ну и ладно: предупрежден -- вооружен! А что, если задать буфер побольше? void workc(char* fn) { // ...

if (setvbuf(fil, 0, _IOFBF, LARGE_BUFSIZ)) return;

// ... }

void workcpp(char* fn) { // ...

char* buf=new char[LARGE_BUFSIZ]; fil.rdbuf()->pubsetbuf(buf, LARGE_BUFSIZ);

// ...

delete [] buf; }

Как ни странно, по сути ничего не изменится! Дело в том, что современные ОС при работе с диском используют очень качественные алгоритмы кэширования, так что еще один уровень буферизации внутри приложения оказывается излишним (в том смысле, что используемые по умолчанию буферы потоков вполне адекватны).

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

Подводя итог, хочется отметить, что если ввод-вывод является узким местом вашего приложения, то следует воздержаться от использования стандартных потоков C++ и использовать проверенные десятилетиями методы.


Содержание раздела