*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***
Показаны сообщения с ярлыком итераторы. Показать все сообщения
Показаны сообщения с ярлыком итераторы. Показать все сообщения

суббота, 14 февраля 2009 г.

Шестнадцатеричная печать в STL поток

Когда-то очень давно я написал элементарный манипулятор для шестнадцатеричной печати в стандарный поток. Все просто и тривиально. Но тем не менее я заметил, что таскаю этот микрокласс почти в кажный проект, где нужна отладочная печать. Обычно для шестнадцатеричной печати надо указывать сразу несколько итераторов, типа:
std::cout << std::hex << std::uppercase << std::setfill('0') << std::setw(2) << 0xAA;
Причем std::setw() надо повторять для каждого нового выводимого элемента. Я свел все это в один итератор, чтобы можно было просто написать (указав итератору ширину выводимого поля):
std::cout << ext::Hex(2) << 0xAA;
Итак, класс Hex (название пространства имен можно подкрутить по вкусу), файл hex.h:
#ifndef _EXT_HEX_H
#define _EXT_HEX_H

#include <iostream>
#include <iomanip>

namespace ext {

class Hex {
public:
Hex(int width) : __width(width) {}
friend std::ostream& operator<< (std::ostream& os, const Hex& hex);
private:
int __width;
};

inline std::ostream& operator<< (std::ostream& os, const Hex& hex) {
std::hex(os);
std::uppercase(os);
os.width(hex.__width);
os.fill('0');
return os;
}

} // ext

#endif // _EXT_HEX_H
Теперь можно писать так:
std::cout << ext::Hex(0)  << 0x0a << std::endl;
std::cout << ext::Hex(1) << 0x0a << std::endl;
std::cout << ext::Hex(1) << 0xaa << std::endl;
std::cout << ext::Hex(2) << 0xaa << std::endl;
std::cout << ext::Hex(4) << 0xaa << std::endl;
std::cout << ext::Hex(8) << 0x0a << std::endl;
std::cout << ext::Hex(16) << 0x0a << std::endl;
std::cout << ext::Hex(32) << 0x0a << std::endl;
И результатом будет:
A
A
AA
AA
00AA
0000000A
000000000000000A
0000000000000000000000000000000A
На всякий случай, unit-тест. Чтобы не было сюрпризов при обновлении компилятора, STLport или чего-то еще. Тест всегда проверит, работает ли класс так, как вы от него ждете. Вы можете возразить — ну класс-то выеденного яйца не стоит, а тут для него тесты... Соглашусь. А еще я соглашусь, что сотни раз самые казалось бы ненужные на первый взгляд тесты для "очевидных" классов помогали обнаружить глюки на новой версии системных библиотек, новой версии компилятора, использовании "более мощных" параметров оптимизации и т.д. Время на написание тестов всегда окупается сполна, всегда.
Традиционно, для компиляции тестов нам нужна Google Test Framework. Как я уже писал, вы можете скачать мою модификацию этой библиотеки, которая сокращена без потери какой-либо функциональности до двух необходимых файлов gtest/gtest.h и gtest-all.cc.
Файл hex_unittest.cpp:
#include "gtest/gtest.h"
#include "hex.h"
#include <sstream>

void testHex(int n, int w, const std::string& etalon) {
std::stringstream fmt;
fmt << ext::Hex(w) << n;
EXPECT_EQ(etalon, fmt.str());
}

TEST(HexManip, Generic) {
testHex(0x0A, 0, "A");
testHex(0x0A, 1, "A");
testHex(0xAA, 1, "AA");
testHex(0xAA, 2, "AA");
testHex(0xAA, 4, "00AA");
testHex(0xAA, 8, "000000AA");
testHex(0xAA, 16, "00000000000000AA");
testHex(0xAA, 32, "000000000000000000000000000000AA");
}
Ну и головная программа:
#include "gtest/gtest.h"
int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Компилируем.

Visual Studio:

cl /EHsc /I. /Fehex_unittest_vs2008.exe runner.cpp hex_unittest.cpp gtest\gtest-all.cc
Cygwin:
g++ -I. -o hex_unittest_cygwin.exe runner.cpp hex_unittest.cpp gtest/gtest-all.cc
Запускаем:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from HexManip
[ RUN ] HexManip.Generic
[ OK ] HexManip.Generic
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 1 test.
Работает как положено.
При использовании Hex у себя в проекте не забудьте включить файл hex_unittest.cpp в ваш набор unit-тестов. Оберегите себя от ненужной траты времени в будущем.
Под занавес пара слов о производительности. Очевидно, что если вы выводите в поток десятки тысяч шестнадцатеричных чисел подряд, то разумнее будет использовать стандартные итераторы — настроить поток с помощью std::hex, std::uppercase и std::setfill(), а потом вызывать только std::setw() для каждого нового элемента. Но если вы печатаете разнородные данные, что часто требуется при отладке, то тогда итератор Hex будет в самый раз.


Другие посты по теме:

понедельник, 9 февраля 2009 г.

Темные углы C++

Я нашел таки для себя ответ на вопрос про "лишние скобки" вокруг параметра, задающего интервальный итератор (см. "Скоростное чтение файла в STL через итераторы" ). Например:
std::ifstream is("testfile.txt");
std::string val(
(std::istream_iterator<char>(is)),
std::istream_iterator<char>()
);
Обратите внимание на вроде "излишние" скобки вокруг первого параметра конструктора строки val.
Скотт Мейерс в книге "Эффективное использование STL. Библиотека программиста" в Совете 6 "Остерегайтесь странностей лексического разбора C++" (стр. 42, изд. "Питер" 2002) дает исчерпывающее объяснение этого "феномена". Ответ крайне меня опечалил, так как вскрыл некоторую нелогичность и корявость в целом стройного и красивого языка С++ в данном вопросе. Очевидно, что причины этого в сохранения в С++ обратной совместимости с С, но от этого не легче.

Итак, давайте разберемся по порядку (чтобы меня не обвинили в плагиате, сразу скажу, что я буду следовать примерному тексту Мейерса, так как он дал великолепное объяснение с примерами, и изобретать велосипед в данном случае было бы неразумно). Как мы предполагали, код std::istream_iterator<char>(is) создает экземпляр потокового итератора, привязанного к потоку is. И все бы ничего, если такая конструкция используется как самостоятельное объявление. Вся проблема в именно в использовании такого выражения в контексте вызова функции (в данном случае, конструктора), то есть в качестве параметра. Мейерс приводит следующий пример:

int f(double d);
Это команда объявления функции f, которая получает double и возвращает int.

Тоже самое происходит и в следующей строке. Круглые скобки вокруг имени параметра d не нужны, поэтому компилятор их игнорирует:

int f(double (d));     // То же; круглые скобки вокруг d игнорируются
Теперь третий вариант объявления той же функции. В нем имя параметра просто не указано:
int f(double);         // То же; имя параметра не указано
Три такие формы объявления знакомы всем, хотя про возможность заключения параметра в скобки знают не все (может просто потому, что это очевидно лишние по логике вещей скобки).

Теперь можно рассмотреть еще три объявления функции. В первом объявляется функция g с параметром — указателем на функцию, которая вызывается без параметров и возвращает double:

int g(double (*pf)()); // Функции g передается указатель на функцию
То же самое можно сформулировать иначе. Единственное различие заключается в том, что pf объявляется в синтаксисе без указателей (допустимом как в С, так и в С++):
int g(double pf());    // То же; pf неявно интерпретируется как указатель
Как обычно, имена параметров могут опускаться, поэтому возможен и третий вариант объявления g без указания имени pf:
int g(double());       // То же; имя параметра не указано
Обратите внимание на различия между круглыми скобками вокруг имени параметра (например, параметра d во втором объявлении f) и стоящими отдельно (как в этом примере). Круглые скобки, в которые заключено имя параметра, игнорируются, а стоящие отдельно, обозначают присутствие списка параметров; они сообщают о присутствии параметра, который является указателем на функцию.

Теперь вернемся к оригинальному примеру:

std::ifstream is("testfile.txt");
std::string val(
std::istream_iterator<char>(is),
std::istream_iterator<char>()
);
Сейчас я намеренно убрал таинственные "лишние" скобки вокруг первого параметра.
Что же перед нами тут? Совершенно не то, о чем мы думали изначально. Перед нами объявление функции val, возвращающей тип std::string. Функция получает два параметра:
  • Первый параметр, is, относится к типу istream_iterator<char>. Лишние круглые скобки вокруг is игнорируются.
  • Второй параметр не имеет имени. Он относится к типу указателя на функцию, которая вызывается без параметров и возвращает istream_iterator<char>.
А мы то тут ожидали увидеть описание вызова конструктора, которому передаются два потоковых итератора. Такая интерпретация написанного диктуется одним из основных правил C++: все, что может интерпретироваться как указатель функцию, должно интерпретироваться именно так. Так гласит стандарт:
В грамматике имеется неоднозначность, когда инструкция может быть выражением, так и объявлением. Если выражение с явным преобразованием типов в стиле вызова функции (_expr.type.conv_) является крайним слева, то оно может быть неотличимо от объявления, в котором первый оператор объявления начинается с открытой круглой скобки "(". В этом случае инструкция рассматривается как объявление. — [C++03] п.6.8
Так что же делают эти магические скобки вокруг первого параметра конструктора?
std::ifstream is("testfile.txt");
std::string val(
(std::istream_iterator<char>(is)),
std::istream_iterator<char>()
);
А вот что — объявления формальных параметров не могут заключаться в круглые скобки, я вот заключить в круглые скобки аргумент при вызове функции можно. Вот эти круглые скобки и помогают компилятору решить неоднозначность в нужную нам сторону (а не как положено по стандарту по умолчанию) и точно указать, что перед нами именно использование параметра функции при ее вызове, а не при объявлении.

Соглашусь, от этого может слегка заболеть голова, причем совершенно без причины.

Как написал Герб Саттер в книге "Новые сложные задачи на С++" (он тоже посвятил этому вопросу целую главу, “Задача 23. Инициализация ли это?”, стр. 192, изд. “Вильямс”), что такие моменты синтаксиса С++ являются его "темными углами", и их стоит избегать. Рассмотренный пример можно упростить, объявив итератор отдельно, а не прямо в тексте вызова конструктора, тем самым не заходить в "темный угол". Не так элегантно, зато просто и понятно:

std::ifstream is("testfile.txt");
std::istream_iterator<char> begin(is);
std::istream_iterator<char> end;
std::string val(begin, end);
Читал я недавно, как Линус Торвальдс полоскал С++ за неоправданную языковую сложность. "C++ is a horrible language!", — сказал Линус. Может он и прав.
Мыши плакали, кололись, но продолжали грызть С++.


Другие посты по теме:

воскресенье, 8 февраля 2009 г.

Скоростное чтение файла в STL через итераторы

Во многих современных языках программирования считать весь файл в строку можно буквально одним оператором, например, в php это делается так:
$lines = file_get_contents("textfile.txt");
или так, медленно и неэффективно, по-старинке:
$lines = join("", file("textfile.txt"));
Задумался я, как бы это так элегантно одним оператором сделать в С++. Естественно, хочется, чтобы это работало приемлемо быстро для больших файлов (например, от одного мегабайта и больше).

Первое, что сходу приходит в голову (универсальная кондовая классика):

std::ifstream is("testfile.txt");
std::string v;
char buf[N];
while (is) {
is.read(buf, sizeof(buf));
v.append(buf, is.gcount());
}
где с размером буфера N можно еще проиграться, выбрав оптимальный.

Первое улучшение, приходящее в голову — это распределить заранее внутренний буфер для строки, минимизировав количество перераспределений буфера в процессе чтения. Для этого мы, естественно, должны заранее знать размер читаемого файла. Пусть это будет 1 мегабайт (1024*1024).

std::ifstream is("testfile.txt");
std::string v;
// Резервируем внутренний буфер std::string на указанный
// размер в один мегабайт.
v.reserve(1024*1024);
char buf[N];
while (is) {
is.read(buf, sizeof(buf));
v.append(buf, is.gcount());
}
Далее на сцену выходят STL-алгоритмы и потоковые итераторы. Берем типовой пример использования алгоритма stl::copy, который есть практически в любой книге по С++:
std::ifstream is("testfile.txt");
std::string v;
// Сброс данного флага необходим для отключения пропуска пробельных
// символов при форматном вводе через поток.
is.unsetf(std::ios::skipws);
std::copy(
std::istream_iterator<char>(is),
std::istream_iterator<char>(),
std::back_inserter(v)
);
Сразу скажу, это крайне медленный метод. Первым, приходящим в голову улучшением, как всегда, является предварительное распределение буфера приемной строки:
std::ifstream is("testfile.txt");
std::string v;
// Резервируем внутренний буфер std::string на указанный
// размер в один мегабайт.
v.reserve(1024*1024);
is.unsetf(std::ios::skipws);
std::copy(
std::istream_iterator<char>(is),
std::istream_iterator<char>(),
std::back_inserter(v)
);
В рассмотренном методе видно, что сначала данные изымаются из потока потоковым итератором istream_iterator, а потом кладутся через итератор строки back_inserter в саму строку. Двойная работа. Есть метод лучше — класть данные из потока напрямую в строку, используя один из специальных конструкторов класса std::string:
std::ifstream is("testfile.txt");
is.unsetf(std::ios::skipws);
// Самое интересное происходит тут: создается переменная "v"
// через конструктор, работающий напрямую с итераторами потока,
// и данные напрямую поступают во внутренний буфер строки.
std::string v(
(std::istream_iterator<char>(is)),
std::istream_iterator<char>()
);
Опытный читатель заметит казалось бы ненужные обрамляющие скобки вокруг первого параметра. Сразу скажу — без них работать не будет, а будет ошибка компиляции. Тут мы касаемся одного из "темных углов" С++. Это не самый очевидный вопрос, поэтому я посвятил ему отдельную статью.
Уже лучше, но двигаемся дальше. В потоках ввода есть специальные итераторы istreambuf_iterator, которые работают напрямую с внутренними буферами потока в обход всех высокоуровневых функций форматирования и выходного преобразования. Именно по этому для них вызов функции unsetf будет уже не нужен:
std::ifstream is("testfile.txt");
std::string v;
// Опциональное резервирование буфера приемной строки.
v.reserve(1024*1024);
std::copy(
std::istreambuf_iterator<char>(is),
std::istreambuf_iterator<char>(),
std::back_inserter(v)
);
И теперь вариант через конструктор класса std::string:
std::ifstream is("testfile.txt");
std::string v(
(std::istreambuf_iterator<char>(is)),
std::istreambuf_iterator<char>()
);
Мы уже близки к идеалу. Теперь встроим создание объекта std::ifstream прямо в код создания строки:
std::string v(
(std::istreambuf_iterator<char>(
std::ifstream("testfile.txt")
)),
std::istreambuf_iterator<char>()
);
Мы уже в миллиметре от идеала, но в приведенном примере есть одно большое "но". Вызов std::ifstream("testfile.txt") прямо в вызове конструктора создает временный объект, который по стандарту языка всегда является константой, а первый параметр конструктора ожидает принять не константный параметр, поэтому "строгий" компилятор типа gcc скорее всего выдаст ошибку компиляции, а менее "строгий", например cl.exe от Майкрософта на такой вызов не ругается. Но мы не можем принять такое не универсальное решение, поэтому изменим код, чтобы параметр создавался динамически в куче, а для автоматического его удаления будет использоваться std::auto_ptr:
std::string v(
(std::istreambuf_iterator<char>(
*(std::auto_ptr<std::ifstream>(
new std::ifstream("testfile.txt")
)).get()
)),
std::istreambuf_iterator<char>()
);

Этот код должен работать в любом стандартном компиляторе С++.

Оглядимся назад. У нас столько вариантов — какой выбрать? Для начала, скорость. Надо понять, какой вариант работает банально быстрее. Для этого я собрал все эти варианты в тестовую программу (конечно, с использованием Google Test Framework).

Как я уже писал, вы можете скачать мою модификацию этой библиотеки, которая сокращена до двух необходимых файлов gtest/gtest.h и gtest-all.cc.
Файл filereader_unittest.cpp:
#include <gtest/gtest.h>

#include <iostream>
#include <streambuf>
#include <istream>
#include <fstream>
#include <ios>
#include <iomanip>
#include <string>
#include <vector>
#include <memory>
#include <cstdlib>

// Управляющий класс для нашей среды тестирования.
class Env: public testing::Environment {
public:
// Размер тестового файла: 1 мегабайт.
static int testfile_sz() { return 1024 * 1024; }
// Имя тестового файла.
static const char* testfile() { return "testfile"; }

protected:
// Эта функция вызывается один раз в начале тестирования.
// Она создает тестовый файл.
void SetUp() {
std::string dummy(testfile_sz(), 'x');
std::ofstream os(testfile());
os.write(dummy.c_str(), dummy.length());
}

// Эта функция вызывается один раз после всех тестов.
// Она удаляет тестовый файл.
void TearDown() {
std::remove(testfile());
}
};

// Функция, реализующая классический метод чтения файла кусками
// заданной длины N. При необходимости производится предварительное
// распределение буфера приемной строки.
void rawRead(int N, bool reserve) {
std::ifstream is(Env::testfile());

std::string v;
if (reserve)
v.reserve(Env::testfile_sz());

char* buf = new char[N];
while (is) {
is.read(buf, sizeof(buf));
v.append(buf, is.gcount());
}
delete[] buf;

// На всякий случай проверяем размер считанного файла.
EXPECT_EQ(Env::testfile_sz(), v.length());
}

// Классическое чтение с буфером в 100 байт.
TEST(ReaderTest, raw_100) {
rawRead(100, false);
}

// Классическое чтение с буфером в 1 килобайт.
TEST(ReaderTest, raw_1024) {
rawRead(1024, false);
}

// Классическое чтение с буфером в 10 килобайт.
TEST(ReaderTest, raw_10240) {
rawRead(10240, false);
}

// Классическое чтение с буфером в 10 килобайт с предварительным
// распределением буфера приемной строки.
TEST(ReaderTest, raw_reserve_10240) {
rawRead(10240, true);
}

// Функция, реализующая чтение через итератор istream_iterator.
// При необходимости производится предварительное распределение
// буфера приемной строки.
void check_istream_iterator(bool reserve) {
std::ifstream is(Env::testfile());

std::string v;
if (reserve)
v.reserve(Env::testfile_sz());

// Принудительное игнорирование пропуска пробельных символов.
// С этим флагом двоичные данные будут читаться неверно.
is.unsetf(std::ios::skipws);
std::copy(
std::istream_iterator<char>(is),
std::istream_iterator<char>(),
std::back_inserter(v)
);

// На всякий случай проверяем размер считанного файла.
EXPECT_EQ(Env::testfile_sz(), v.length());
}

// Тестируем работу через istream_iterator.
TEST(ReaderTest, istream_iterator) {
check_istream_iterator(false);
}

// Тестируем работу через istream_iterator с предварительным
// распределением буфера приемной строки.
TEST(ReaderTest, istream_iterator_reserve) {
check_istream_iterator(true);
}

// Тестируем работу через istream_iterator при прямом
// вызове конструктора строки, который берет данные напрямую
// из итераторов.
TEST(ReaderTest, istream_iterator_tostring) {
std::ifstream is(Env::testfile());
is.unsetf(std::ios::skipws);

std::string v(
(std::istream_iterator<char>(is)),
std::istream_iterator<char>()
);

// На всякий случай проверяем размер считанного файла.
EXPECT_EQ(Env::testfile_sz(), v.length());
}

// Функция, реализующая чтение через итератор istreambuf_iterator.
// При необходимости производится предварительное распределение
// буфера приемной строки. Для данного метода сброс флага
// std::ios::skipws не нужен, так как этот итератор работает
// на более низком уровне.
void check_istreambuf_iterator(bool reserve) {
std::ifstream is(Env::testfile());

std::string v;
if (reserve)
v.reserve(Env::testfile_sz());

std::copy(
std::istreambuf_iterator<char>(is),
std::istreambuf_iterator<char>(),
std::back_inserter(v)
);

// На всякий случай проверяем размер считанного файла.
EXPECT_EQ(Env::testfile_sz(), v.length());
}

// Тестируем работу через istreambuf_iterator.
TEST(ReaderTest, istreambuf_iterator) {
check_istreambuf_iterator(false);
}

// Тестируем работу через istreambuf_iterator с предварительным
// распределением буфера приемной строки.
TEST(ReaderTest, istreambuf_iterator_reserve) {
check_istreambuf_iterator(true);
}

// Тестируем работу через istreambuf_iterator при прямом
// вызове конструктора строки, который берет данные напрямую
// из итераторов.
TEST(ReaderTest, istreambuf_iterator_tostring) {
std::ifstream is(Env::testfile());

std::string v(
(std::istreambuf_iterator<char>(is)),
std::istreambuf_iterator<char>()
);

// На всякий случай проверяем размер считанного файла.
EXPECT_EQ(Env::testfile_sz(), v.length());
}

#ifdef WIN32

// Этот тест аналогичен тесту istreambuf_iterator_tostring
// за исключение создания объекта потока прямо в вызове
// конструктора строки. Работает только в cl.exe, так как
// "стандартный" компилятор запрещает передавать временные
// объекты по неконстантной ссылке, а cl.exe почему-то это
// разрешает.
TEST(ReaderTest, istreambuf_iterator_tostring_short) {
std::string v(
(std::istreambuf_iterator<char>(
std::ifstream(Env::testfile())
)),
std::istreambuf_iterator<char>()
);

// На всякий случай проверяем размер считанного файла.
EXPECT_EQ(Env::testfile_sz(), v.length());
}

#endif

// Финальный метод. Конструктор строки берет данные напрямую
// из итератора istreambuf_iterator. Объект потока создается
// динамически прямо в коде вызова конструктора строки через
// std::auto_ptr.
TEST(ReaderTest, istreambuf_iterator_tostring_short_auto_ptr) {
std::string v(
(std::istreambuf_iterator<char>(
*(std::auto_ptr<std::ifstream>(
new std::ifstream(Env::testfile())
)).get()
)),
std::istreambuf_iterator<char>()
);

// На всякий случай проверяем размер считанного файла.
EXPECT_EQ(Env::testfile_sz(), v.length());
}

// Запуск тестов.
int main(int argc, char **argv) {
// Инициализация нашей тестовой среды.
testing::AddGlobalTestEnvironment(new Env);
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Для компиляции вам необходимы файлы gtest/gtest.h и gtest-all.cc (см. выше).
Для начала скомпилируем в Visual Studio 2008:
cl /Fefilereader_vs2008.exe /DWIN32 /O2 /arch:SSE2 /I. /EHsc filereader_unittest.cpp gtest-all.cc
Запускаем:
filereader_vs2008.exe --gtest_print_time
Опция --gtest_print_time указывает Google Test выводить время работы каждого теста.
Результат:
[==========] Running 12 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 12 tests from ReaderTest
[ RUN ] ReaderTest.raw_100
[ OK ] ReaderTest.raw_100 (141 ms)
[ RUN ] ReaderTest.raw_1024
[ OK ] ReaderTest.raw_1024 (94 ms)
[ RUN ] ReaderTest.raw_10240
[ OK ] ReaderTest.raw_10240 (109 ms)
[ RUN ] ReaderTest.raw_reserve_10240
[ OK ] ReaderTest.raw_reserve_10240 (94 ms)
[ RUN ] ReaderTest.istream_iterator
[ OK ] ReaderTest.istream_iterator (359 ms)
[ RUN ] ReaderTest.istream_iterator_reserve
[ OK ] ReaderTest.istream_iterator_reserve (344 ms)
[ RUN ] ReaderTest.istream_iterator_tostring
[ OK ] ReaderTest.istream_iterator_tostring (281 ms)
[ RUN ] ReaderTest.istreambuf_iterator
[ OK ] ReaderTest.istreambuf_iterator (141 ms)
[ RUN ] ReaderTest.istreambuf_iterator_reserve
[ OK ] ReaderTest.istreambuf_iterator_reserve (125 ms)
[ RUN ] ReaderTest.istreambuf_iterator_tostring
[ OK ] ReaderTest.istreambuf_iterator_tostring (78 ms)
[ RUN ] ReaderTest.istreambuf_iterator_tostring_short
[ OK ] ReaderTest.istreambuf_iterator_tostring_short (67 ms)
[ RUN ] ReaderTest.istreambuf_iterator_tostring_short_auto_ptr
[ OK ] ReaderTest.istreambuf_iterator_tostring_short_auto_ptr (78 ms)
[----------] 12 tests from ReaderTest (1891 ms total)

[----------] Global test environment tear-down
[==========] 12 tests from 1 test case ran. (1906 ms total)
[ PASSED ] 12 tests.
Теперь в gcc 3.4.4:

g++ -O2 -funroll-all-loops -fomit-frame-pointer -msse2 -mtune=nocona -I. -o filereader_gcc344.exe filereader_unittest.cpp gtest-all.cc
Запускаем:
filereader_gcc344.exe --gtest_print_time
Результат:
[==========] Running 11 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 11 tests from ReaderTest
[ RUN ] ReaderTest.raw_100
[ OK ] ReaderTest.raw_100 (647 ms)
[ RUN ] ReaderTest.raw_1024
[ OK ] ReaderTest.raw_1024 (649 ms)
[ RUN ] ReaderTest.raw_10240
[ OK ] ReaderTest.raw_10240 (647 ms)
[ RUN ] ReaderTest.raw_reserve_10240
[ OK ] ReaderTest.raw_reserve_10240 (644 ms)
[ RUN ] ReaderTest.istream_iterator
[ OK ] ReaderTest.istream_iterator (2141 ms)
[ RUN ] ReaderTest.istream_iterator_reserve
[ OK ] ReaderTest.istream_iterator_reserve (2131 ms)
[ RUN ] ReaderTest.istream_iterator_tostring
[ OK ] ReaderTest.istream_iterator_tostring (1115 ms)
[ RUN ] ReaderTest.istreambuf_iterator
[ OK ] ReaderTest.istreambuf_iterator (1124 ms)
[ RUN ] ReaderTest.istreambuf_iterator_reserve
[ OK ] ReaderTest.istreambuf_iterator_reserve (1120 ms)
[ RUN ] ReaderTest.istreambuf_iterator_tostring
[ OK ] ReaderTest.istreambuf_iterator_tostring (76 ms)
[ RUN ] ReaderTest.istreambuf_iterator_tostring_short_auto_ptr
[ OK ] ReaderTest.istreambuf_iterator_tostring_short_auto_ptr (74 ms)
[----------] 11 tests from ReaderTest (10371 ms total)

[----------] Global test environment tear-down
[==========] 11 tests from 1 test case ran. (10396 ms total)
[ PASSED ] 11 tests.

Давайте проанализируем результаты.

Мы не будем сравнивать абсолютные времена компиляторов друг против друга. Сейчас не об этом.

Вывод первый. Предварительное резервирование буфера приемной строки (метод reserve()) не дает никакого эффекта в нашем случае. Может это из-за того, что стратегия расширения буфера при простом линейном добавлении данных итак весьма эффективна в классе std::string.

Вывод второй. Размер буфера чтения в методе чтения файла явными кусками установленного размера не дал четкой картины. Не очевидно, какой размер буфера может быть потенциально оптимальным. Тут может и дисковый кэш повлиял, может внутреннее буферизирование в классе std::ifstream, может что-то еще.

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

Вывод четвертый. Использование конструктора класса std::string, работающего напрямую с итераторами потока, заметно быстрее, чем использование алгоритма std::copy (ReaderTest.istream_iterator заметно медленнее ReaderTest.istream_iterator_tostring и ReaderTest.istreambuf_iterator заметно медленнее ReaderTest.istreambuf_iterator_tostring). И понятно почему — данные напрямую поступают в буфер строки без ненужного промежуточного копирования.

Вывод пятый (основной). Метод чтения через итератор istreambuf_iterator с использованием конструктора строки, работающего напрямую с итераторами потока (тест ReaderTest.istreambuf_iterator_tostring для "нестрогого" компилятора и тест ReaderTest.istreambuf_iterator_tostring_auto_ptr для компилятора, следующего стандартам), является весьма эффективным и может конкурировать с ручным блочным чтением. Конечно, текст данного метода весьма непрост и может запутан для понимания на первый взгляд, особенно для начинающих, а блочное чтения прозрачно и ясно, но при почти равной эффективности этих методов нет причин отказываться от работы через итераторы, так как данный метод весь фактически предоставляется библиотекой STL, а значит быть может оптимизирован независимо, без затрагивания кода уже использующей его программы.


Другие посты по теме: