Является ли стандарт С++ низкой эффективностью для iostreams, или я просто имею дело с плохой реализацией?

Каждый раз, когда я упоминаю медленную производительность iostreams стандартной библиотеки С++, я встречаюсь с волной недоверия. Тем не менее, у меня есть результаты профилирования, показывающие большое количество времени, затрачиваемого на библиотечный код iostream (полная оптимизация компилятора), а переход с iostreams на API-интерфейсы ввода-вывода для ОС и управление пользовательским буфером дают порядок улучшения.

Какую дополнительную работу выполняет стандартная библиотека С++, требуется ли она стандартом и полезно ли это на практике? Или некоторые компиляторы обеспечивают реализацию iostreams, которые являются конкурентными с ручным управлением буфером?

Бенчмарки

Чтобы все было в порядке, я написал несколько коротких программ для осуществления внутренней буферизации iostreams:

Обратите внимание, что версии 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 раз медленнее, когда я его тестирую? Он не используется полиморфно в этом простом микро-контроле, поэтому это не объясняет.

Ответ 1

Не отвечающий специфике вашего вопроса так сильно, как заголовок: 2006 Технический отчет о производительности С++ содержит интересный раздел о IOStreams (p 0,68). Наиболее актуальным для вашего вопроса является раздел 6.1.2 ( "Скорость выполнения" ):

Поскольку некоторые аспекты обработки IOStreams распределен по нескольким граням, он что Стандарт предусматривает неэффективная реализация. Но это это не так - с использованием какой-то формы предварительной обработки, большая часть работы может избегать. С немного умнее чем обычно используется, это можно удалить некоторые из этих Неэффективность. Это обсуждается в §6.2.3 и §6.2.5.

Поскольку отчет был написан в 2006 году, можно было бы надеяться, что многие из рекомендаций были бы включены в состав современных компиляторов, но, возможно, это не так.

Как вы говорите, фасеты могут не отображаться в write() (но я бы не предполагал, что слепо). Так что же особенность? Выполнение GProf в коде ostringstream, скомпилированном с помощью GCC, дает следующую разбивку:

  • 44,23% в std::basic_streambuf<char>::xsputn(char const*, int)
  • 34,62% ​​в std::ostream::write(char const*, int)
  • 12.50% в main
  • 6.73% в std::ostream::sentry::sentry(std::ostream&)
  • 0,96% в std::string::_M_replace_safe(unsigned int, unsigned int, char const*, unsigned int)
  • 0,96% в std::basic_ostringstream<char>::basic_ostringstream(std::_Ios_Openmode)
  • 0.00% в std::fpos<int>::fpos(long long)

Таким образом, основная часть времени тратится на xsputn, который в итоге вызывает std::copy() после многих проверок и обновлений позиций и буферов курсора (посмотрите подробности c++\bits\streambuf.tcc).

Я считаю, что вы сосредоточились на худшем случае. Вся проверка, которая выполняется, будет небольшой частью общей работы, если вы имеете дело с достаточно большими кусками данных. Но ваш код перемещает данные по четыре байта за раз и каждый раз приносит все дополнительные затраты. Ясно, что можно было бы избежать этого в реальной ситуации - подумайте о том, насколько пренебрежимо было бы наказание, если бы write был вызван в массиве из 1 м int вместо 1 м раз на одном int. И в реальной ситуации вы действительно оцените важные функции IOStreams, а именно его безопасный и безопасный по типу дизайн. Такие льготы приходят по цене, и вы написали тест, который заставляет эти затраты доминировать над временем выполнения.

Ответ 2

Я довольно разочарован в том, что пользователи Visual Studio там, у кого скорее всего было это:

  • В реализации Visual Studio ostream объект sentry (который требуется стандартом) входит в критический раздел, защищающий streambuf (который не требуется). Это не является необязательным, поэтому вы платите стоимость синхронизации потоков даже для локального потока, используемого одним потоком, который не нуждается в синхронизации.

Это повреждает код, который использует ostringstream для форматирования сообщений довольно серьезно. Использование stringbuf позволяет избежать использования sentry, но форматированные операторы вставки не могут работать непосредственно на streambuf s. Для Visual С++ 2010 критический раздел замедляет ostringstream::write в три раза по сравнению с базовым вызовом stringbuf::sputn.

Рассматривая данные профилировщика beldaz о newlib, кажется очевидным, что gcc sentry не делает ничего такого сумасшедшего. ostringstream::write под gcc занимает примерно 50% больше, чем stringbuf::sputn, но stringbuf сам по себе намного медленнее, чем в VС++. И оба по-прежнему очень неблагоприятно относятся к использованию vector<char> для буферизации ввода-вывода, хотя и не того же поля, что и в VС++.

Ответ 3

Проблема, которую вы видите, связана с накладными расходами на каждый вызов write(). Каждый уровень абстракции, который вы добавляете (char [] → vector → string → ostringstream), добавляет еще несколько вызовов/возвратов функций и других хостов, которые - если вы называете это миллион раз - складывается.

Я изменил два примера на ideone, чтобы писать по десять ints за раз. Время ostringstream прошло от 53 до 6 мс (почти 10-кратное улучшение), а цикл char улучшился (3,7 до 1,5) - полезен, но только в два раза.

Если вы заинтересованы в производительности, вам нужно выбрать правильный инструмент для работы. ostringstream полезен и гибкий, но существует штраф за его использование так, как вы пытаетесь. char [] труднее работать, но прирост производительности может быть большим (помните, что gcc, вероятно, встроит memcpys для вас).

Короче говоря, ostringstream не нарушается, но чем ближе вы дойдете до металла, тем быстрее будет работать ваш код. У ассемблера все еще есть преимущества для некоторых людей.

Ответ 4

Чтобы получить лучшую производительность, вы должны понимать, как работают контейнеры. В примере массива char [] массив требуемого размера выделяется заранее. В вашем векторе и примере ostringstream вы вынуждаете объекты многократно выделять и перераспределять и, возможно, копировать данные многократно по мере увеличения объекта.

С std::vector это легко разрешается путем инициализации размера вектора до конечного размера, как вы делали массив char; вместо этого вы скорее несправедливо калечите производительность, изменяя размер до нуля! Это вряд ли справедливое сравнение.

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

В случае вектора и ostringstream вы получаете защиту от переполнения буфера, вы не получаете этого с массивом char, и эта защита не приходит бесплатно.