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


197

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

Яку додаткову роботу виконує стандартна бібліотека C ++, чи вимагає її стандарт і чи корисна вона на практиці? Або деякі компілятори забезпечують реалізацію iostreams, які є конкурентоспроможними ручним управлінням буферами?

Орієнтири

Щоб змінити справи, я написав пару коротких програм для здійснення внутрішнього буферування iostreams:

Зауважте, що ostringstreamі stringbufверсії виконують менше ітерацій, оскільки вони набагато повільніші.

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

Все це буфери в пам'яті, тому повільність іостримів не можна звинувачувати в вводному / вхідному режимі повільного диска, занадто багато промивання, синхронізації з stdio або будь-якого іншого, що люди використовують для виправдання спостережуваної повільності стандартної бібліотеки C ++ іострім.

Було б непогано побачити орієнтири інших систем та коментувати речі, які зазвичай реалізують (наприклад, gcc's libc ++, Visual C ++, Intel C ++) і скільки накладних витрат визначається стандартом.

Обґрунтування цього тесту

Ряд людей правильно вказав, що іотоми частіше використовують для форматованого виводу. Однак вони також є єдиним сучасним 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перерозподіл, який відбувається лише з першого проходу, повинен мало впливати на кінцеві результати.

EDIT: На жаль, знайдено помилку в vector-з звичайним ітератором, ітератор не просунувся, і тому було занадто багато звернень кешу. Мені було цікаво, як vector<char>це перевершує char[]. Це не мало великої різниці, але vector<char>все ж швидше, ніж char[]у VC ++ 2010.

Висновки

Буферування вихідних потоків вимагає трьох кроків кожного разу, коли дані додаються:

  • Переконайтеся, що вхідний блок відповідає доступному буферному простору.
  • Скопіюйте вхідний блок.
  • Оновіть покажчик кінця даних.

Останній фрагмент коду, який я опублікував, " vector<char>простий ітератор плюс перевірка меж", це не тільки робить це, він також виділяє додатковий простір і переміщує наявні дані, коли вхідний блок не підходить. Як зазначав Кліффорд, буферизація у файлі класу вводу-виводу не повинна цього робити, вона просто промиває поточний буфер і повторно використовує його. Отже, це має бути верхньою межею витрат на буферизацію продукції. І саме те, що потрібно для створення робочого буфера пам'яті.

Так чому stringbuf2,5 рази повільніше на ideone, і принаймні в 10 разів повільніше, коли я тестую його? Він не використовується поліморфно в цьому простому мікро-орієнтирі, так що це не пояснює.


24
Ви пишете мільйон символів одночасно і цікавитеся, чому це повільніше, ніж копіювати в попередньо виділений буфер?
Анон.

20
@Anon: Я буферизую чотири мільйони байтів чотири одночасно, і так, мені цікаво, чому це повільно. Якщо std::ostringstreamнедостатньо розумний, щоб експоненціально збільшити розмір буфера так, як std::vectorце робиться, це (А) дурне і (В) те, про що люди повинні думати про продукти вводу / виводу. У будь-якому разі буфер повторно використовується, він не перерозподіляється кожного разу. А std::vectorтакож використовує буфер, що динамічно зростає. Я намагаюся бути тут справедливим.
Ben Voigt

14
Яке завдання ви насправді намагаєтесь орієнтувати? Якщо ви не використовуєте жодну з функцій форматування ostringstreamі ви хочете якомога швидшу продуктивність, то вам слід подумати про те, щоб перейти безпосередньо до цього stringbuf. Заняття ostreamкласів передбачають поєднання функціональних можливостей форматування з урахуванням локальних даних із гнучким вибором буфера (файл, рядок тощо) за допомогою rdbuf()та його віртуального інтерфейсу функцій. Якщо ви не робите жодного форматування, то цей додатковий рівень непрямості, безумовно, буде пропорційно дорогим порівняно з іншими підходами.
CB Bailey

5
+1 для правди оп. Ми отримали збільшення швидкості порядку чи величини, переходячи ofstreamдо, fprintfколи виводить інформацію про реєстрацію, що включає подвійні. MSVC 2008 на WinXPsp3. iostreams просто собачий повільний.
KitsuneYMG

6
Ось якийсь тест на сайті комітету: open-std.org/jtc1/sc22/wg21/docs/D_5.cpp
Йоганнес Шауб - litb

Відповіді:


49

Не відповідаючи на специфіку вашого питання так само, як заголовок: Технічний звіт про ефективність 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м разів на один int. І в реальній ситуації дійсно варто оцінити важливі особливості IOStreams, а саме її безпечну пам’ять та безпечну конструкцію. Такі вигоди приносять свою ціну, і ви написали тест, завдяки якому ці витрати домінують у часі виконання.


Звучить чудовою інформацією для майбутнього запитання про виконання форматованого вставки / вилучення iostreams, яке я, мабуть, скоро запитую. Але я не вірю, що тут є якісь грані ostream::write().
Бен Войгт

4
+1 для профілювання (це я вважаю машиною Linux, яку я вважаю?). Однак я фактично додаю чотири байти одночасно (насправді sizeof i, але всі компілятори, з якими я тестую, мають 4-байтні int). І це здається мені не таким нереальним, які розміри шматки, на вашу думку, проходять при кожному дзвінку xsputnв типовому коді, як stream << "VAR: " << var.x << ", " << var.y << endl;.
Бен Войгт

39
@beldaz: Цей "типовий" приклад коду, який дзвонить лише xsputnп'ять разів, цілком може бути в циклі, який записує 10-мільйонний файл рядка. Передача даних у потоки великих частин - це набагато менше реального сценарію, ніж мій орієнтир-код. Чому я повинен писати в буферний потік з мінімальною кількістю дзвінків? Якщо мені доведеться займатися власним буферизацією, який сенс має бути у потоці iostreams? І з двійковими даними я маю можливість самостійно їх завантажувати, коли записую мільйони чисел у текстовий файл, опція масового просто не існує, МОЖЕ зателефонувати operator <<до кожного.
Бен Войгт

1
@beldaz: Можна визначити, коли введення-виведення починає домінувати за допомогою простого обчислення. При середній швидкості запису 90 Мб / с, характерній для жорстких дисків поточного споживача, промивання буфера 4 Мб займає <45 мс (пропускна здатність, затримка неважлива через кеш запису в ОС). Якщо для заповнення внутрішнього циклу для заповнення буфера потрібно більше часу, тоді обмежуючим фактором буде процесор. Якщо внутрішній цикл працює швидше, то обмежуючим фактором буде введення-виведення або, принаймні, залишилося деякий час процесора, щоб виконати справжню роботу.
Бен Войгт

5
Звичайно, це не означає, що використання iostreams обов'язково означає повільну програму. Якщо введення-виведення - це дуже мала частина програми, то використання бібліотеки вводу-виводу з низькою продуктивністю не матиме загального впливу. Але те, що викликається досить часто, має значення - це не те, що хороша продуктивність, і для важких додатків вводу / виводу це має значення.
Бен Войгт

27

Я дуже розчарований у користувачах Visual Studio там, хто, скоріше, мав приналежність до цього:

  • У візуальної реалізації студії ostream, то sentryоб'єкт (який потрібно стандарт) входить в критичну секцію , що захищає streambuf(який не потрібно). Це здається не обов'язковим, тому ви платите вартість синхронізації потоків навіть за локальний потік, який використовується одним потоком, який не потребує синхронізації.

Це шкодить коду, який використовується ostringstreamдля форматування повідомлень досить сильно. Використання stringbufбезпосередньо уникає використання sentry, але відформатовані оператори вставки не можуть працювати безпосередньо на streambufs. Для Visual C ++ 2010 критичний розділ сповільнюється ostringstream::writeв три рази в порівнянні з базовим stringbuf::sputnвикликом.

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


Чи є ця інформація все ще актуальною? AFAIK, реалізація C ++ 11, що постачається разом із GCC, виконує цей «шалений» замок. Звичайно, VS2010 все ще робить це. Чи може хтось уточнити цю поведінку, і якщо "що не потрібно" все ще втримується в C ++ 11?
mloskot

2
@mloskot: Я не бачу вимог щодо безпеки потоку щодо sentry... " Довідковий клас визначає клас, який відповідає за виконання безпечних операцій з префіксами та суфіксами." та примітка "Конструктор відправлення та деструктор також можуть виконувати додаткові операції, що залежать від реалізації". Можна також припустити, що за принципом C ++ "ти не платиш за те, що не використовуєш", що комітет C ++ ніколи не затвердить таку марну вимогу. Але сміливо задайте питання щодо безпеки потоку iostream.
Бен Войгт

8

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

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

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

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


8
Що ostringstream::write()робити, що vector::push_back()цього не робить? Якщо що-небудь, це має бути швидше, оскільки він передається блоку замість чотирьох окремих елементів. Якщо ostringstreamце повільніше, ніж std::vectorбез надання додаткових функцій, то так, я б назвав це порушеним.
Бен Войгт

1
@Ben Voigt: Навпаки, його щось векторне має робити, що ostringstream НЕ повинен робити, що робить вектор більш ефективним у цьому випадку. Вектор гарантовано залишається в пам’яті, а ostringstream - ні. Vector - це один із класів, призначений для виконання, а ostringstream - ні.
Dragontamer5788

2
@Ben Voigt: Використання stringbufбезпосередньо не збирається видаляти всі виклики функцій, оскільки stringbufзагальнодоступний інтерфейс складається з публічних невіртуальних функцій базового класу, які потім передають захищену віртуальну функцію у похідному класі.
CB Bailey

2
@Charles: На будь-якому пристойному компіляторі це повинно бути, оскільки виклик загальнодоступної функції стане вбудованим у контекст, де динамічний тип відомий компілятору, він може видалити непряме і навіть вбудувати ці виклики.
Бен Войгт

6
@Roddy: Я повинен подумати, що це все вбудований шаблон шаблону, який видно у кожному блоці компіляції. Але я думаю, що це може відрізнятися залежно від впровадження. Напевно, я б очікував, що заклик, що обговорюється, публічна sputnфункція, яка викликає віртуальний захищений xsputn, буде окреслена. Навіть якщо xsputnвін не вбудований, компілятор може, вбудовуючи вказівки sputn, визначити xsputnпотрібне переопределення, необхідне та генерувати прямий дзвінок, не проходячи через vtable.
Ben Voigt

1

Для кращої продуктивності ви повинні зрозуміти, як працюють контейнери, які ви використовуєте. У прикладі масиву char [] масив потрібного розміру виділяється заздалегідь. У вашому прикладі векторного та поточного потоку ви змушуєте об'єкти багаторазово розподіляти та перерозподіляти та, можливо, копіювати дані багато разів під час зростання об'єкта.

З допомогою std :: vector це легко вирішується шляхом ініціалізації розміру вектора до кінцевого розміру, як ви робили масив char; натомість ви досить несправедливо калічите продуктивність, змінюючи розмір до нуля! Це навряд чи справедливе порівняння.

Що стосується потокового потоку, то попереднє розміщення простору неможливо, я б припустив, що це нераціональне використання. Клас має набагато більшу корисність, ніж простий масив char, але якщо ця утиліта вам не потрібна, тоді не використовуйте її, оскільки ви сплатите накладні витрати в будь-якому випадку. Натомість його слід використовувати для того, для чого це добре - форматування даних у рядок. C ++ забезпечує широкий спектр контейнерів, а стрингстрим є одним з найменш підходящих для цієї мети.

У випадку, коли вектор і ostringstream отримують захист від переповнення буфера, ви не отримуєте цього з масивом char, і цей захист не надходить безкоштовно.


1
Виділення, здається, не є проблемою для ostringstream. Він просто прагне повернутись до нуля для наступних ітерацій. Жодного укорочення. Також я спробував, ostringstream.str.reserve(4000000)і це не мало значення.
Родді

Я думаю ostringstream, що ви могли б "резервувати", передаючи манекенну рядок, тобто: " ostringstream str(string(1000000 * sizeof(int), '\0'));З" vector, resizeне розміщується жоден простір, він розширюється лише за потреби.
Нім

1
"вектор .. захист від перекриття буфера". Поширене помилкове уявлення - vector[]оператор, як правило, НЕ перевіряється на наявність помилок меж за замовчуванням. vector.at()Однак.
Родді

2
vector<T>::resize(0)Зазвичай не перерозподіляє пам'ять
Нікі Йошіучі

2
@Roddy: Не використовує operator[], але push_back()(в порядку back_inserter), що, безумовно, НЕ перевіряє на переповнення. Додано ще одну версію, яка не використовується push_back.
Ben Voigt
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.