Каждый раз, когда я упоминаю о низкой производительности iostreams стандартной библиотеки C ++, меня встречает волна недоверия. Тем не менее, у меня есть результаты профилировщика, показывающие большое количество времени, проведенного в коде библиотеки iostream (полная оптимизация компилятора), и переключение с iostreams на специфичные для ОС API-интерфейсы ввода-вывода и настраиваемое управление буфером дает улучшение порядка.
Какую дополнительную работу выполняет стандартная библиотека C ++, требуется ли она по стандарту и полезна ли она на практике? Или некоторые компиляторы предоставляют реализации iostreams, которые конкурируют с ручным управлением буфером?
Ориентиры
Чтобы начать работу, я написал несколько коротких программ для внутренней буферизации iostreams:
- положить двоичные данные в
ostringstreamhttp://ideone.com/2PPYw - размещение двоичных данных в
char[]буфере http://ideone.com/Ni5ct - положить двоичные данные в
vector<char>использованиеback_inserterhttp://ideone.com/Mj2Fi - NEW :
vector<char>простой итератор http://ideone.com/9iitv - NEW : размещение двоичных данных непосредственно в
stringbufhttp://ideone.com/qc9QA - НОВИНКА :
vector<char>простой итератор и проверка границ http://ideone.com/YyrKy
Обратите внимание , что ostringstreamи stringbufверсии работать меньше итераций , потому что они намного медленнее.
На ideone, то ostringstreamесть примерно в 3 раза медленнее , чем std:copy+ back_inserter+ std::vector, и примерно в 15 раз медленнее , чем memcpyв сырьевой буфер. Это похоже на профилирование до и после, когда я переключил свое реальное приложение на пользовательскую буферизацию.
Все это буферы в памяти, поэтому медлительность iostreams не может быть обвинена в медленном вводе-выводе диска, слишком большом сбрасывании, синхронизации с stdio или любых других вещах, которые люди используют, чтобы оправдать наблюдаемую медлительность стандартной библиотеки C ++ iostream.
Было бы неплохо увидеть тесты для других систем и прокомментировать то, что делают обычные реализации (такие как libc ++ gcc, Visual C ++, Intel C ++) и сколько накладных расходов предусмотрено стандартом.
Обоснование этого теста
Многие люди правильно отметили, что iostreams чаще используются для форматированного вывода. Однако они также являются единственным современным API, предоставляемым стандартом C ++ для доступа к двоичным файлам. Но настоящая причина для выполнения тестов производительности внутренней буферизации заключается в типичном форматированном вводе / выводе: если 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 C ++ 2010 x86, cl /Ox /EHscWindows 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 C ++ 2010 x86, с профилем Guided Optimization cl /Ox /EHsc /GL /c, link /ltcg:pgi, бег, link /ltcg:pgo, меры:
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 C ++ 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 C ++ 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-обычно-итераторе, итератор не был продвинут, и поэтому было слишком много попаданий в кэш. Мне было интересно, как это vector<char>было лучше char[]. Это не имело большого значения, но vector<char>все же быстрее, чем char[]под VC ++ 2010.
Выводы
Буферизация выходных потоков требует трех шагов каждый раз, когда добавляются данные:
- Убедитесь, что входящий блок соответствует доступному буферному пространству.
- Скопируйте входящий блок.
- Обновите указатель конца данных.
Последний фрагмент кода, который я опубликовал, « vector<char>простой итератор плюс проверка границ», не только делает это, но также выделяет дополнительное пространство и перемещает существующие данные, когда входящий блок не подходит. Как отметил Клиффорд, буферизация в классе файлового ввода-вывода не должна была бы этого делать, она просто очищает текущий буфер и использует его повторно. Так что это должна быть верхняя граница стоимости буферизации вывода. И это именно то, что нужно для создания рабочего буфера в памяти.
Так почему же в stringbuf2,5 раза медленнее на идеоне и как минимум в 10 раз медленнее, когда я его тестирую? Он не используется полиморфно в этом простом микропроцессоре, поэтому это не объясняется.
std::ostringstreamон недостаточно умен, чтобы экспоненциально увеличивать размер буфера, как это std::vectorпроисходит, это (A) глупо и (B) то, о чем должны думать люди, думающие о производительности ввода-вывода. В любом случае, буфер используется повторно, он не перераспределяется каждый раз. И std::vectorтакже использует динамически растущий буфер. Я пытаюсь быть честным здесь.
ostringstreamи хотите максимально возможную производительность, то вам следует перейти прямо к этому stringbuf. Предполагается, что ostreamклассы связывают воедино функциональность форматирования с учетом локали с гибким выбором буфера (файл, строка и т. Д.) rdbuf()И его интерфейс виртуальной функции. Если вы не выполняете никакого форматирования, тогда этот дополнительный уровень косвенности будет выглядеть пропорционально дороже по сравнению с другими подходами.
ofstreamк fprintfпри выводе информации регистрации, включающей удвоения. MSVC 2008 на WinXPsp3. Iostreams это просто собака медленно.