Стандарт C ++ предписывает низкую производительность для iostreams, или я просто имею дело с плохой реализацией? - PullRequest
192 голосов
/ 03 декабря 2010

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

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

Тесты

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

Обратите внимание, что версии ostringstream и stringbuf выполняют меньше итераций, потому что они намного медленнее.

На ideone ostringstream примерно в 3 раза медленнее, чем std:copy + back_inserter + std::vector, и примерно в 15 раз медленнее, чем memcpy в необработанном буфере. Это похоже на профилирование до и после, когда я переключил свое реальное приложение на пользовательскую буферизацию.

Это все буферы в памяти, поэтому медлительность iostreams не может быть обвинена в медленном вводе-выводе диска, слишком большом сбросе памяти, синхронизации с stdio или любых других вещах, которые люди используют, чтобы оправдать наблюдаемую медлительность C ++ стандартная библиотека iostream.

Было бы неплохо увидеть тесты для других систем и прокомментировать то, что делают обычные реализации (такие как gcc libc ++, 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 /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 C ++ 2010 x86, с оптимизацией по профилю 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 -с обычным-итератором, итератор не продвигался и, следовательно, было слишком много попаданий в кэш. Мне было интересно, как vector<char> превосходит char[]. Это не имело большого значения, хотя, vector<char> все еще быстрее, чем char[] под VC ++ 2010.

Выводы

Буферизация выходных потоков требует трех шагов каждый раз, когда добавляются данные:

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

Последний фрагмент кода, который я опубликовал, «vector<char> простой итератор плюс проверка границ» не только делает это, но также выделяет дополнительное пространство и перемещает существующие данные, когда входящий блок не подходит. Как отметил Клиффорд, буферизация в классе файлового ввода-вывода не должна была бы этого делать, она просто очищает текущий буфер и использует его повторно. Так что это должна быть верхняя граница стоимости буферизации вывода. И это именно то, что нужно для создания рабочего буфера в памяти.

Так почему же stringbuf в 2,5 раза медленнее на идеоне и по крайней мере в 10 раз медленнее, когда я его тестирую? Он не используется полиморфно в этом простом микропроцессоре, так что это не объясняет.

Ответы [ 4 ]

47 голосов
/ 03 декабря 2010

Не отвечая на конкретные вопросы вашего вопроса так же, как на заголовок: Технический отчет о производительности C ++ за 2006 год содержит интересный раздел о IOStreams (с.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-м целым числом, а не с 1-м разом с одним целым. А в реальной жизни можно было бы по достоинству оценить важные функции IOStreams, а именно его безопасный для памяти и безопасный для типов дизайн. Такие преимущества имеют свою цену, и вы написали тест, который делает эти затраты доминирующими во времени выполнения.

27 голосов
/ 04 декабря 2010

Я довольно разочарован в пользователях Visual Studio, у которых, скорее всего, есть такая штука:

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

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

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

8 голосов
/ 03 декабря 2010

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

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

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

1 голос
/ 03 декабря 2010

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

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

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

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

...