Каждый раз, когда я упоминаю медленную производительность iostreams стандартной библиотеки С++, я встречаюсь с волной недоверия. Тем не менее, у меня есть результаты профилирования, показывающие большое количество времени, затрачиваемого на библиотечный код iostream (полная оптимизация компилятора), а переход с iostreams на API-интерфейсы ввода-вывода для ОС и управление пользовательским буфером дают порядок улучшения.
Какую дополнительную работу выполняет стандартная библиотека С++, требуется ли она стандартом и полезно ли это на практике? Или некоторые компиляторы обеспечивают реализацию iostreams, которые являются конкурентными с ручным управлением буфером?
Бенчмарки
Чтобы все было в порядке, я написал несколько коротких программ для осуществления внутренней буферизации iostreams:
- вставка двоичных данных в
ostringstream
http://ideone.com/2PPYw - размещение двоичных данных в буфере
char[]
http://ideone.com/Ni5ct - вставка двоичных данных в
vector<char>
с помощьюback_inserter
http://ideone.com/Mj2Fi - NEW:
vector<char>
простой итератор http://ideone.com/9iitv - NEW: размещение двоичных данных непосредственно в
stringbuf
http://ideone.com/qc9QA - NEW:
vector<char>
простой итератор плюс оценки http://ideone.com/YyrKy
Обратите внимание, что версии ostringstream
и stringbuf
запускают меньше итераций, потому что они намного медленнее.
На идеоне ostringstream
примерно в 3 раза медленнее, чем std:copy
+ back_inserter
+ std::vector
, и примерно в 15 раз медленнее, чем memcpy
, в необработанный буфер. Это согласуется с профилированием до и после, когда я переключил свое реальное приложение на пользовательскую буферизацию.
Это все буферы в памяти, поэтому медленность iostreams не может быть обвинена в медленных дисках ввода-вывода, слишком много промывки, синхронизации с stdio или любых других вещей, которые люди используют, чтобы оправдать наблюдаемую медлительность С++ стандартная библиотека iostream.
Было бы неплохо увидеть контрольные показатели в других системах и комментарии к вещам, которые обычно выполняются в обычных реализациях (например, gcc libС++, Visual С++, Intel С++) и о том, какая часть накладных расходов предусмотрена стандартом.
Обоснование для этого теста
Несколько человек правильно указали, что iostreams чаще используются для форматированного вывода. Тем не менее, они также являются единственным современным API, предоставляемым стандартом С++ для доступа к двоичному файлу. Но настоящая причина выполнения тестов производительности внутренней буферизации относится к типичному форматированному вводу-выводу: если iostreams не могут поддерживать контроллер диска с необработанными данными, как они могут идти в ногу с тем, когда они отвечают за форматирование?
Сроки тестирования
Все это за итерацию внешнего цикла (k
).
На ideone (gcc-4.3.4, неизвестная ОС и аппаратное обеспечение):
-
ostringstream
: 53 миллисекунды -
stringbuf
: 27 мс -
vector<char>
иback_inserter
: 17.6 мс -
vector<char>
с обычным итератором: 10.6 мс -
vector<char>
проверка итератора и границ: 11.4 мс -
char[]
: 3,7 мс
На моем ноутбуке (Visual С++ 2010 x86, cl /Ox /EHsc
, Windows 7 Ultimate 64-бит, Intel Core i7, 8 ГБ оперативной памяти):
-
ostringstream
: 73,4 миллисекунды, 71,6 мс -
stringbuf
: 21,7 мс, 21,3 мс -
vector<char>
иback_inserter
: 34,6 мс, 34,4 мс -
vector<char>
с обычным итератором: 1.10 мс, 1.04 мс -
vector<char>
проверка итератора и границ: 1.11 мс, 0.87 мс, 1.12 мс, 0.89 мс, 1.02 мс, 1.14 мс -
char[]
: 1,48 мс, 1,57 мс
Visual С++ 2010 x86 с оптимизацией профиля cl /Ox /EHsc /GL /c
, link /ltcg:pgi
, run, link /ltcg:pgo
, measure:
-
ostringstream
: 61,2 мс, 60,5 мс -
vector<char>
с обычным итератором: 1,04 мс, 1,03 мс
Тот же самый ноутбук, с той же ОС, с помощью cygwin gcc 4.3.4 g++ -O3
:
-
ostringstream
: 62,7 мс, 60,5 мс -
stringbuf
: 44,4 мс, 44,5 мс -
vector<char>
иback_inserter
: 13,5 мс, 13,6 мс -
vector<char>
с обычным итератором: 4.1 мс, 3.9 мс -
vector<char>
проверка итератора и границ: 4.0 мс, 4.0 мс -
char[]
: 3,57 мс, 3,75 мс
Тот же ноутбук, Visual С++ 2008 SP1, cl /Ox /EHsc
:
-
ostringstream
: 88,7 мс, 87,6 мс -
stringbuf
: 23,3 мс, 23,4 мс -
vector<char>
иback_inserter
: 26,1 мс, 24,5 мс -
vector<char>
с обычным итератором: 3,13 мс, 2,48 мс -
vector<char>
проверка итератора и границ: 2,97 мс, 2,53 мс -
char[]
: 1,52 мс, 1,25 мс
Такой же ноутбук, 64-битный компилятор Visual С++ 2010:
-
ostringstream
: 48,6 мс, 45,0 мс -
stringbuf
: 16,2 мс, 16,0 мс -
vector<char>
иback_inserter
: 26,3 мс, 26,5 мс -
vector<char>
с обычным итератором: 0,87 мс, 0,89 мс -
vector<char>
проверка итератора и границ: 0.99 мс, 0.99 мс -
char[]
: 1,25 мс, 1,24 мс
РЕДАКТИРОВАТЬ: Разыграть все дважды, чтобы увидеть, насколько согласованы результаты. Довольно согласованная ИМО.
ПРИМЕЧАНИЕ. На моем ноутбуке, поскольку я могу сэкономить больше времени процессора, чем позволяет ideone, я установил количество итераций до 1000 для всех методов. Это означает, что перераспределение ostringstream
и vector
, которое имеет место только при первом проходе, не должно оказывать незначительного влияния на конечные результаты.
РЕДАКТИРОВАТЬ: Ой, обнаружил ошибку в vector
-with-common-iterator, итератор не был передовым, и поэтому было слишком много хитов кэша. Мне было интересно, как vector<char>
превзошел char[]
. Это не имело большого значения, но vector<char>
все еще быстрее, чем char[]
в VС++ 2010.
Выводы
Буферизация выходных потоков требует трех шагов при каждом добавлении данных:
- Убедитесь, что входящий блок соответствует доступному буферному пространству.
- Скопируйте входящий блок.
- Обновить указатель на конец данных.
Самый последний фрагмент кода, который я опубликовал, "vector<char>
простая проверка итератора плюс оценки" не только это, он также выделяет дополнительное пространство и перемещает существующие данные, когда входящий блок не подходит. Как отметил Клиффорд, буферизация в файле I/O класса не должна была бы этого делать, это просто очистит текущий буфер и повторное его использование. Таким образом, это должно быть верхняя граница стоимости вывода буферизации. И это именно то, что необходимо для создания рабочего буфера в памяти.
Итак, почему stringbuf
на 2,5x медленнее на ideone и, по крайней мере, в 10 раз медленнее, когда я его тестирую? Он не используется полиморфно в этом простом микро-контроле, поэтому это не объясняет.