Стоимость параметров по умолчанию в C++
Я наткнулся на пример из "Эффективного C++ во встроенной среде" Скотта Мейерса, где были описаны два способа использования параметров по умолчанию: один был описан как дорогостоящий, а другой - как лучший вариант.
Мне не хватает объяснения того, почему первый вариант может быть более дорогостоящим по сравнению с другим.
void doThat(const std::string& name = "Unnamed"); // Bad
const std::string defaultName = "Unnamed";
void doThat(const std::string& name = defaultName); // Better
3 ответа
В первом временный std::string
инициализируется из буквального "Unnamed"
каждый раз, когда функция вызывается без аргумента.
Во втором случае объект defaultName
инициализируется один раз (для исходного файла) и просто используется при каждом вызове.
void doThat(const std::string& name = "Unnamed"); // Bad
Это "плохо" в том, что новый std::string
с содержанием "Unnamed"
создается каждый раз doThat()
называется.
Я говорю "плохо" и не плохо, потому что небольшая строковая оптимизация в каждом используемом мной компиляторе C++ поместит "Unnamed"
данные во временном std::string
создается на сайте вызова и не выделяет для него никакого хранилища. Таким образом, в данном конкретном случае временные аргументы не требуют больших затрат. Стандарт не требует небольшой строковой оптимизации, но он явно разработан, чтобы разрешить это, и каждая используемая в настоящее время стандартная библиотека реализует его.
Более длинная строка вызовет распределение; Оптимизация небольших строк работает только на коротких строках. Распределения дорогие; Если вы используете эмпирическое правило о том, что одно выделение в 1000+ раз дороже обычной инструкции ( несколько микросекунд!), вы не останетесь в стороне.
const std::string defaultName = "Unnamed";
void doThat(const std::string& name = defaultName); // Better
Здесь мы создаем глобальный defaultName
с содержанием "Unnamed"
, Это создается во время статической инициализации. Здесь есть некоторые риски; если doThat
вызывается во время статической инициализации или уничтожения (до или после main
работает), он может быть вызван без defaultName
или тот, который уже был уничтожен.
С другой стороны, нет риска, что здесь произойдет выделение памяти для каждого вызова.
Теперь правильное решение в современном C++17:
void doThat(std::string_view name = "Unnamed"); // Best
который не будет выделяться, даже если строка длинная; это даже не скопирует строку! Кроме того, в 999/1000 случаях это замена старого doThat
API, и это может даже улучшить производительность, когда вы передаете данные в doThat
и не полагаться на аргумент по умолчанию.
На данный момент, поддержка C++17 во встроенном может отсутствовать, но в некоторых случаях это может произойти в ближайшее время. Строковое представление - это достаточно большое увеличение производительности, так что уже существует множество подобных типов, которые делают то же самое.
Но урок все еще остается; не выполняйте дорогостоящие операции с аргументами по умолчанию. И распределение может быть дорогим в некоторых контекстах (особенно в мире встраиваемых систем).
Может быть, я неверно истолковал "дорогостоящий" (для "правильной" интерпретации см. Другой ответ), но одну вещь, которую следует учитывать с параметрами по умолчанию, это то, что они плохо масштабируются в таких ситуациях:
void foo(int x = 0);
void bar(int x = 0) { foo(x); }
Это становится кошмаром для ошибок, когда вы добавляете больше вложенности, потому что значение по умолчанию должно повторяться в нескольких местах (то есть дорого в том смысле, что одно крошечное изменение требует изменения разных мест в коде). Лучший способ избежать этого, как в вашем примере:
const int foo_default = 0;
void foo(int x = foo_default);
void bar(int x = foo_default) { foo(x); } // no need to repeat the value here