Недавно читал статью, в которой рассказывается про тормоза исключений в C++ и про то, что можно с этим сделать. Ниже будет пересказ с моими дополнениями.
Начнем с причины, почему бросание исключений медленное. Дело в том, что для этого необходимо раскрутить стек до ближайшего подходящего
catch. Но раскрутка стека реализована через глобальные таблицы, которые защищены mutex'ом (например, чтобы обработать случай, когда в программу динамически подгружается so'шка и хочет, чтобы ее исключения тоже обрабатывались). Очевидно, что в многопоточном коде при интенсивном использовании исключений этот mutex быстро станет bottleneck'ом и будет драматически замедлять программу.В статье указывается, что попытки починить вышеописанную проблему (например, заменив mutex на rwlock) приведут либо к гонкам, либо к поломке ABI. А, как известно, C++ по странным историческим причинам могут использовать вместе с древними, собранными в виде so'шек библиотеками, из-за чего ABI никто ломать не хочет. И теперь все вынуждены жить вот с такими вот медленными исключениями :(
Указывается еще две проблемы с исключениями в текущем виде: 1) они нелокальны из-за
std::current_exception() и 2) они требуют аллокации на куче и RTTI. Все это, в числе прочих, мешает оптимизациям компилятора, когда тот понимает, что вот в этой ветке выкинется вот это исключение, и мог бы здесь попытаться срезать углы на создании и выбрасывании.Стоит отдельно упомянуть, что исключения на самом деле не zero cost, даже если они не кидаются: поддержка раскрутки стека раздувает размер бинаря и иногда заставляет компилятор генерить менее оптимальный код (потому что оптимизации помешают правильной раскрутке стека).
Авторы пишут, что если оставить семантику исключений в текущем виде, но ускорить раскрутку, то
Even with fully lock-free unwinding, we encountered some scalability issues with very high threads counts and high error rates (256 threads, 10% failure). These were far less severe than with current single-threaded unwinding, but nevertheless it is clear that the other parts of traditional exception handling do not scale either due to global state.что как будто бы частично решает проблему, но не до конца.
Далее предлагаются разные способы ускорить механизм обработки ошибок, все без раскрутки стека. Удивительно, что
std::expected<> показал себя не очень хорошо по скорости, хотя все остальные методы (boost::LEAF и throwing values) работают на аналогичных принципах.Я бы хотел отдельно рассказать про throwing values, но про это читайте пост ниже :)