Исключения в C ++ действительно медленные

99

Я смотрел систематическую обработку ошибок в C ++ - Андрей Александреску утверждает, что исключения в C ++ очень медленные.

Верно ли это для C ++ 98?

Авинаш
источник
44
Нет смысла спрашивать, работают ли «исключения C ++ 98» быстрее / медленнее, чем «исключения C ++ 03» или «исключения C ++ 11». Их производительность зависит от того, как компилятор реализует их в ваших программах, а стандарт C ++ ничего не говорит о том, как они должны быть реализованы; единственное требование - их поведение должно соответствовать стандарту (правило «как если бы»).
In silico
Связанный (но не повторяющийся) вопрос: stackoverflow.com/questions/691168/…
Филипп
2
да, это очень медленно, но их не следует выбрасывать для нормальной работы или использовать в качестве ветки
BЈовић
Я нашел похожий вопрос .
PaperBirdMaster
Чтобы прояснить то, что сказал Бёовин, не стоит бояться использования исключений. Когда возникает исключение, вы сталкиваетесь с (потенциально) трудоемкими операциями. Мне также любопытно, почему вы хотите знать конкретно о C ++ 89 ... эта последняя версия - C ++ 11, и время, необходимое для запуска исключений, определяется реализацией, поэтому мои «потенциально» трудоемкие .
thecoshman

Ответы:

163

Основная модель, используемая сегодня для исключений (Itanium ABI, VC ++ 64 бит), - это исключения модели с нулевой стоимостью.

Идея состоит в том, что вместо того, чтобы терять время, настраивая защиту и явно проверяя наличие исключений повсюду, компилятор генерирует дополнительную таблицу, которая отображает любую точку, которая может вызвать исключение (счетчик программ), в список обработчиков. Когда генерируется исключение, к этому списку обращаются для выбора правильного обработчика (если есть), и стек раскручивается.

По сравнению с типичной if (error)стратегией:

  • модель с нулевой стоимостью, как следует из названия, бесплатна, когда не происходит исключений
  • это стоит около 10x / 20x if когда возникает исключение

Стоимость, однако, измерить нетривиально:

  • Приставной столик вообще холодный , поэтому извлечение его из памяти занимает много времени.
  • Определение правильного обработчика включает RTTI: множество дескрипторов RTTI для выборки, разбросанных по памяти и выполнение сложных операций (в основном, dynamic_castтест для каждого обработчика)

Таким образом, в основном промахи в кеше, и поэтому нетривиально по сравнению с чистым кодом ЦП.

Примечание: для получения дополнительной информации прочтите отчет TR18015, глава 5.4 Обработка исключений (pdf)

Итак, да, исключения выполняются медленно на исключительном пути , но в остальном они быстрее, чем явные проверки ( ifстратегия) в целом.

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


Это имеет значение ?

Я бы сказал, что это не так. Программа должна быть написана с учетом удобочитаемости , а не производительности (по крайней мере, не в качестве первого критерия). Исключения следует использовать, когда ожидается, что вызывающий не может или не захочет обработать сбой на месте и передать его вверх по стеку. Бонус: в C ++ 11 исключения можно упорядочивать между потоками с помощью стандартной библиотеки.

Это тонко, хотя я утверждаю, что он map::findне должен бросать, но я нормально отношусь к map::findвозврату, checked_ptrкоторый выдает, если попытка разыменования не удалась, потому что он равен нулю: в последнем случае, как и в случае класса, который представил Александреску, вызывающий выбирает между явной проверкой и использованием исключений. Расширение прав и возможностей звонящего, не возлагая на него большую ответственность, обычно является признаком хорошего дизайна.

Матье М.
источник
3
+1 Я бы добавил только четыре вещи: (0) о добавленной в C ++ 11 поддержке переброса; (1) ссылка на отчет комитета по эффективности C ++; (2) некоторые замечания по поводу правильности (как превосходящей даже удобочитаемости); и (3) о производительности, замечания об ее измерении в сравнении со случаем
неиспользования
2
@ Cheersandhth.-Alf: (0), (1) и (3) готово: спасибо. Что касается правильности (2), хотя она превосходит удобочитаемость, я не уверен, что исключения приводят к более правильному коду, чем другие стратегии обработки ошибок (так легко забыть о многих невидимых путях создания исключений выполнения).
Matthieu M.
2
Описание может быть локально правильным, но стоит отметить, что наличие исключений имеет глобальные последствия для предположений и оптимизаций, которые может сделать компилятор. Эти импликации страдают той проблемой, что у них нет "тривиальных контрпримеров", поскольку компилятор всегда может видеть насквозь небольшую программу. Профилирование на реалистичной, большой базе кода с исключениями и без них может быть хорошей идеей.
Kerrek SB
4
> Модель с нулевой стоимостью, как следует из названия, бесплатна, если не происходит никаких исключений, что на самом деле неверно вплоть до мельчайших уровней детализации. создание большего количества кода всегда влияет на производительность, даже если оно небольшое и незаметное ... ОС может потребоваться немного больше времени для загрузки исполняемого файла, или вы получите больше промахов i-cache. а как насчет кода разматывания стека? Кроме того, как насчет экспериментов, которые вы можете провести для измерения эффектов, вместо того, чтобы пытаться понять это с помощью рационального мышления?
jheriko
2
@jheriko: На самом деле, я думаю, что уже ответил на большинство ваших вопросов. Это не должно влиять на время загрузки (холодный код не должен загружаться), на i-cache не должно влиять (холодный код не должен попадать в i-cache), ... так что для ответа на один недостающий вопрос: «как измерить» => замена любого генерируемого исключения вызовом abortпозволит вам измерить двоичный размер и проверить, что время загрузки / i-cache ведет себя аналогичным образом. Конечно, лучше не попадать ни в одну из abort...
Матье М.
61

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


В C ++ исключения особенно медленны по сравнению с другими языками?

Повторная претензия

«Я наблюдал за обработкой систематических ошибок в C ++ - Андрей Александреску утверждает, что исключения в C ++ работают очень медленно».

Если это буквально то, что утверждает Андрей, то на этот раз он очень вводит в заблуждение, если не совсем неправ. Ибо возникшие / выданные исключения всегда выполняются медленно по сравнению с другими базовыми операциями на языке, независимо от языка программирования . Не только в C ++ или в большей степени в C ++, чем в других языках, как указывает предполагаемое утверждение.

В общем, в основном независимо от языка, две основные языковые функции, которые на порядки медленнее остальных, потому что они переводятся в вызовы подпрограмм, которые обрабатывают сложные структуры данных, являются

  • выброс исключения и

  • распределение динамической памяти.

К счастью, в C ++ часто можно избежать того и другого в критичном ко времени коде.

К сожалению, нет такой вещи, как бесплатный обед , даже если эффективность C ++ по умолчанию довольно близка. :-) Эффективность, получаемая за счет исключения генерации исключений и распределения динамической памяти, обычно достигается за счет кодирования на более низком уровне абстракции с использованием C ++ как «лучшего C». А более низкая абстракция означает большую «сложность».

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


Существуют ли какие-либо объективные показатели производительности исключения исключений C ++?

Да, международный комитет по стандартизации C ++ опубликовал Технический отчет о производительности C ++, TR18015 .


Что значит «медленные» исключения?

В основном это означает, что выполнение throwможет занять очень много времени ™ по сравнению, например, с intназначением, из-за поиска обработчика.

Как TR18015 обсуждает в разделе 5.4 «Исключения», существуют две основные стратегии реализации обработки исключений:

  • подход, при котором каждый try-block динамически устанавливает перехват исключений, так что поиск по динамической цепочке обработчиков выполняется при возникновении исключения, и

  • подход, при котором компилятор создает статические таблицы поиска, которые используются для определения обработчика возникшего исключения.

Первый очень гибкий и общий подход почти принудительно применяется в 32-битной Windows, тогда как в 64-битной среде и в * nix-land обычно используется второй, гораздо более эффективный подход.

Также, как обсуждается в этом отчете, для каждого подхода есть три основных области, в которых обработка исключений влияет на эффективность:

  • try-блоки,

  • обычные функции (возможности оптимизации), и

  • throw-выражения.

В основном, при использовании подхода с динамическим обработчиком (32-разрядная версия Windows) обработка исключений влияет на tryблоки, в основном независимо от языка (потому что это вызвано схемой обработки структурированных исключений Windows ), в то время как подход со статической таблицей имеет примерно нулевую стоимость для try- блоки. Обсуждение этого займет гораздо больше места и исследований, чем это практически возможно для ответа SO. Итак, подробности смотрите в отчете.

К сожалению, отчет за 2006 год уже немного датирован концом 2012 года, и, насколько мне известно, нет ничего более нового сравнимого.

Еще одна важная точка зрения заключается в том, что влияние использования исключений на производительность сильно отличается от изолированной эффективности поддерживающих языковых функций, поскольку, как отмечается в отчете,

«При рассмотрении обработки исключений ее следует противопоставлять альтернативным способам работы с ошибками».

Например:

  • Затраты на обслуживание из-за разных стилей программирования (корректность)

  • Резервная ifпроверка отказов сайта вызова по сравнению с централизованнойtry

  • Проблемы с кешированием (например, более короткий код может поместиться в кеш)

В отчете есть другой список аспектов, которые необходимо учитывать, но в любом случае единственный практический способ получить достоверные факты об эффективности выполнения - это, вероятно, реализовать ту же программу с использованием исключения, а не с использованием исключений, в пределах установленного ограничения времени разработки и с разработчиками. знакомы с каждым способом, а затем ИЗМЕРИТЬ .


Как хорошо избежать накладных расходов, связанных с исключениями?

Правильность почти всегда важнее эффективности.

Без исключений легко может произойти следующее:

  1. Некоторый код P предназначен для получения ресурса или вычисления некоторой информации.

  2. Вызывающий код C должен был проверить на успех / неудачу, но этого не происходит.

  3. Несуществующий ресурс или недопустимая информация используются в коде, следующем за C, что вызывает общий хаос.

Основная проблема - это пункт (2), где при обычной схеме кода возврата вызывающий код C не вынужден проверять.

Есть два основных подхода, которые заставляют такую ​​проверку:

  • Где P напрямую выдает исключение в случае сбоя.

  • Где P возвращает объект, который C должен проверить перед использованием своего основного значения (в противном случае исключение или завершение).

Второй подход, AFAIK, впервые был описан Бартоном и Накманом в их книге « Научный и инженерный C ++: введение с расширенными методами и примерами» , где они представили класс, вызывающий Fallow«возможный» результат функции. Аналогичный класс optionalтеперь предлагается библиотекой Boost. И вы можете легко реализовать Optionalкласс самостоятельно, используя в std::vectorкачестве носителя значения для случая результата, отличного от POD.

При первом подходе вызывающий код C не имеет другого выбора, кроме как использовать методы обработки исключений. Однако при втором подходе вызывающий код C может сам решать, выполнять ли ifпроверку на основе или общую обработку исключений. Таким образом, второй подход поддерживает компромисс между программистом и эффективностью времени выполнения.


Какое влияние оказывают различные стандарты C ++ на производительность исключений?

«Я хочу знать, верно ли это для C ++ 98»

C ++ 98 был первым стандартом C ++. Для исключений он ввел стандартную иерархию классов исключений (к сожалению, весьма несовершенную). Основное влияние на производительность оказала возможность спецификаций исключений (удаленных в C ++ 11), которые, однако, никогда не были полностью реализованы основным компилятором Windows C ++ Visual C ++: Visual C ++ принимает синтаксис спецификации исключений C ++ 98, но просто игнорирует спецификации исключения.

C ++ 03 был просто техническим исправлением C ++ 98. Единственным действительно новым в C ++ 03 была инициализация значений . Что не имеет ничего общего с исключениями.

В стандарте C ++ 11 общие спецификации исключений были удалены и заменены noexceptключевым словом.

Стандарт C ++ 11 также добавил поддержку для хранения и повторного генерирования исключений, что отлично подходит для распространения исключений C ++ через обратные вызовы языка C. Эта поддержка эффективно ограничивает способ сохранения текущего исключения. Однако, насколько мне известно, это не влияет на производительность, за исключением той степени, в которой обработка исключений в новом коде может быть более легко использована с обеих сторон обратного вызова языка C.

Приветствия и hth. - Альф
источник
7
"исключения всегда медленны по сравнению с другими базовыми операциями на языке, независимо от языка программирования" ... за исключением языков, предназначенных для компиляции использования исключений в обычное управление потоком.
Бен Фойгт,
5
«генерирование исключения включает как выделение, так и раскручивание стека». Это также явно неверно в целом, и, опять же, OCaml является противоположным примером. В языках со сборкой мусора нет необходимости раскручивать стек, потому что нет деструкторов, поэтому вы просто longjmpобращаетесь к обработчику.
JD
2
@JonHarrop: по-видимому, вы не знаете, что Pyhon имеет предложение finally для обработки исключений. это означает, что реализация Python либо имеет раскрутку стека, либо не является Python. Вы, кажется, совершенно не осведомлены о предметах, о которых вы делаете (фантазии) утверждения. извиняюсь.
Приветствия и hth. - Alf
2
@ Cheersandhth.-Alf: «Pyhon имеет предложение finally для обработки исключений. Это означает, что реализация Python либо имеет раскручивание стека, либо не является Python». try..finallyКонструкция может быть реализована без стека раскручивания. F #, C # и Java реализуются try..finallyбез использования раскрутки стека. Вы просто longjmpк обработчику (как я уже объяснил).
JD
4
@JonHarrop: вы звучать как создает дилемму. но это не имеет отношения к чему-либо, что обсуждалось до сих пор, и до сих пор вы опубликовали длинную последовательность негативно звучащей чепухи . Мне пришлось бы доверять вам, чтобы согласиться или не согласиться с какой-то расплывчатой ​​формулировкой, потому что как антагонист вы выбираете то, что вы раскроете, что это «означает», и я, конечно же, не доверяю вам после всей этой бессмысленной чепухи, голосов против и т.
Приветствия и hth. - Alf
14

Вы никогда не сможете утверждать о производительности, если не конвертируете код в сборку или не тестируете ее.

Вот что вы видите: (быстрая скамья)

Код ошибки не зависит от процента появления. Исключения имеют небольшие накладные расходы, если они никогда не генерируются. Как только вы их бросите, начнутся страдания. В этом примере он выбрасывается для 0%, 1%, 10%, 50% и 90% случаев. Когда исключения генерируются в 90% случаев, код в 8 раз медленнее, чем в случае, когда исключения генерируются в 10% случаев. Как видите, исключения очень медленные. Не используйте их, если их часто бросают. Если ваше приложение не требует работы в реальном времени, не стесняйтесь бросать их, если они возникают очень редко.

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

Тест производительности исключений C ++

Сыпь
источник
13

Это зависит от компилятора.

GCC, например, был известен очень низкой производительностью при обработке исключений, но за последние несколько лет ситуация стала значительно лучше.

Но обратите внимание, что обработка исключений должна - как следует из названия - быть исключением, а не правилом в вашей разработке программного обеспечения. Когда у вас есть приложение, которое выдает столько исключений в секунду, что влияет на производительность, и это по-прежнему считается нормальной работой, вам лучше подумать о том, чтобы действовать по-другому.

Исключения - отличный способ сделать код более читабельным, убрав весь этот неуклюжий код обработки ошибок, но как только они становятся частью обычного потока программы, за ними становится действительно трудно следить. Помните, что a throwв значительной степени goto catchзамаскировано.

Филипп
источник
-1 в том виде, в каком он стоит сейчас, «верно ли это для C ++ 98», который определенно не зависит от компилятора. кроме того, этот ответ throw new Exception- Java-ism. как правило, никогда не следует бросать указатели.
Приветствия и hth. - Alf
1
диктует ли стандарт 98, как именно должны быть реализованы исключения?
thecoshman
6
C ++ 98 - это стандарт ISO, а не компилятор. Есть много компиляторов, которые его реализуют.
Филипп
3
@thecoshman: Нет. Стандарт C ++ ничего не говорит о том, как что-либо должно быть реализовано (за возможным исключением части стандарта «Пределы реализации»).
In silico
2
@Insilico, тогда я могу сделать только логический вывод, что (шокирующе) это определяется реализацией (чтение, специфично для компилятора), как работают исключения.
thecoshman
4

Да, но это не важно. Зачем?
Прочтите это:
https://blogs.msdn.com/b/ericlippert/archive/2008/09/10/vexing-exceptions.aspx

По сути, это говорит о том, что использование исключений, подобных описанному Александреску (50-кратное замедление, потому что они используют catchas else), просто неправильно. Это сказано для тех, кто любит делать это так, как я хочу, чтобы С ++ 22 :) добавил что-то вроде:
(обратите внимание, что это должен быть базовый язык, так как это в основном компилятор, генерирующий код из существующего)

result = attempt<lexical_cast<int>>("12345");  //lexical_cast is boost function, 'attempt'
//... is the language construct that pretty much generates function from lexical_cast, generated function is the same as the original one except that fact that throws are replaced by return(and exception type that was in place of the return is placed in a result, but NO exception is thrown)...     
//... By default std::exception is replaced, ofc precise configuration is possible
if (result)
{
     int x = result.get(); // or result.result;
}
else 
{
     // even possible to see what is the exception that would have happened in original function
     switch (result.exception_type())
     //...

}

PS также обратите внимание, что даже если исключения настолько медленные ... это не проблема, если вы не проводите много времени в этой части кода во время выполнения ... Например, если деление с плавающей запятой выполняется медленно, и вы делаете его в 4 раза быстрее, что не имеет значения, если вы тратите 0,3% своего времени на деление FP ...

NoSenseEtAl
источник
0

Как и в Silico, сказано, что его реализация зависит от реализации, но в целом исключения считаются медленными для любой реализации и не должны использоваться в коде с высокой производительностью.

РЕДАКТИРОВАТЬ: Я не говорю, что не используйте их вообще, но для кода с высокой производительностью лучше их избегать.

Крис МакКейб
источник
9
В лучшем случае это очень упрощенный взгляд на производительность исключений. Например, GCC использует реализацию с «нулевой стоимостью», при которой вы не снижаете производительность, если не генерируются исключения. И исключения предназначены для исключительных (то есть редких) обстоятельств, поэтому, даже если они медленные по какой-либо метрике, это все еще не достаточная причина не использовать их.
In silico
@insilico, если вы посмотрите, почему я сказал, я не говорил не использовать точку исключения исключений. Я указал код с высокой производительностью, это точная оценка, я в основном работаю с gpgpus, и меня не расстреляют, если я использую исключения.
Крис МакКейб,