Передача аргументов как ссылок на const преждевременная оптимизация?

20

«Преждевременная оптимизация - корень всего зла»

Я думаю, что с этим мы все можем согласиться. И я очень стараюсь избегать этого.

Но в последнее время меня интересует практика передачи параметров по константной ссылке, а не по значению . Меня учили / учили, что аргументы нетривиальных функций (то есть большинство не примитивных типов) желательно передавать по константной ссылке - довольно много книг, которые я прочитал, рекомендуют это как «лучшую практику».

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

void fooByValue(SomeDataStruct data);   

и

void fooByReference(const SomeDataStruct& data);

Является ли практика, которую я изучил - передача константных ссылок (по умолчанию для нетривиальных типов) - преждевременная оптимизация?

CharonX
источник
1
См. Также F.call в C ++ Core Guidelines для обсуждения различных стратегий передачи параметров.
Амон
1
@DocBrown Принятый ответ на этот вопрос относится к принципу наименьшего удивления , который также может применяться здесь (т. Е. Использование константных ссылок является отраслевым стандартом и т. Д. И т. Д.). Тем не менее, я не согласен с тем, что вопрос является дубликатом: вопрос, на который вы ссылаетесь, спрашивает, является ли плохой практикой (в общем случае) полагаться на оптимизацию компилятора. Этот вопрос задает обратное: является ли передача константных ссылок (преждевременной) оптимизацией?
CharonX
@CharonX: если здесь можно положиться на оптимизацию компилятора, ответ на ваш вопрос однозначно: «Да, ручная оптимизация не нужна, она преждевременна». Если на него нельзя положиться (возможно, из-за того, что вы заранее не знаете, какие компиляторы будут когда-либо использоваться для кода), ответ таков: «для более крупных объектов это, вероятно, не преждевременно». Таким образом, даже если эти два вопроса не являются буквально равными, они ИМХО кажутся достаточно похожими, чтобы связать их вместе как дубликаты.
Док Браун
1
@DocBrown: Итак, прежде чем вы сможете объявить это обманом, укажите, где в вопросе говорится, что компилятор будет разрешен и сможет «оптимизировать» это.
Дедупликатор

Ответы:

49

«Преждевременная оптимизация» - это не использование оптимизаций на ранней стадии . Речь идет об оптимизации до понимания проблемы, до понимания времени выполнения, и часто делает код менее читаемым и менее поддерживаемым для сомнительных результатов.

Использование «const &» вместо передачи объекта по значению является хорошо понятной оптимизацией, с хорошо понятными эффектами на время выполнения, практически без усилий и без каких-либо отрицательных последствий для читабельности и удобства сопровождения. Это на самом деле улучшает оба, потому что говорит мне, что вызов не изменит передаваемый объект. Поэтому добавление «const &» прямо при написании кода НЕ ПРЕДВАРИТЕЛЬНО.

gnasher729
источник
2
Я согласен с частью «практически без усилий» в вашем ответе. Но преждевременная оптимизация - это, прежде всего, оптимизация, прежде чем ее заметное, измеренное влияние на производительность. И я не думаю, что большинство программистов на C ++ (включая меня) делают какие-либо измерения перед использованием const&, поэтому я думаю, что вопрос довольно разумный.
Док Браун
1
Перед оптимизацией вы измеряете, чтобы узнать, стоят ли какие-либо компромиссы. С const & общее усилие набирает семь символов, и у него есть другие преимущества. Когда вы не собираетесь изменять передаваемую переменную, это имеет преимущество, даже если нет улучшения скорости.
gnasher729
3
Я не эксперт C, поэтому вопрос. const& fooговорит, что функция не будет модифицировать foo, поэтому вызывающая сторона безопасна. Но скопированное значение говорит, что никакой другой поток не может изменить foo, поэтому вызываемый объект безопасен. Правильно? Таким образом, в многопоточном приложении ответ зависит от правильности, а не от оптимизации.
user949300
1
@DocBrown, вы можете в конечном итоге поставить вопрос о мотивации разработчика, который поставил const &? Если бы он делал это только для производительности, не считая остальных, это могло бы рассматриваться как преждевременная оптимизация. Теперь, если он помещает это, потому что он знает, что это будут постоянные параметры, тогда он только самодокументирует свой код и дает возможность компилятору оптимизировать, что лучше.
Вальфрат
1
@ user949300: Немногие функции позволяют изменять свои аргументы одновременно или использовать обратные вызовы, и они прямо говорят об этом.
Дедупликатор
16

TL; DR: передача по константной ссылке все еще хорошая идея в C ++, учитывая все обстоятельства. Не преждевременная оптимизация.

TL; DR2: большинство пословиц не имеет смысла, пока они не делают.


цель

Этот ответ просто пытается немного расширить связанный элемент в C ++ Core Guidelines (впервые упоминается в комментарии amon).

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


применимость

Этот ответ применяется только к вызовам функций (не отсоединяемые вложенные области в одном потоке).

(Примечание.) Когда проходимые вещи могут покинуть область действия (т. Е. Иметь время жизни, которое потенциально может превышать внешнюю область), становится более важным удовлетворение потребности приложения в управлении временем жизни объекта, прежде чем что-либо еще. Обычно это требует использования ссылок, которые также способны управлять временем жизни, таких как умные указатели. Альтернативой может быть использование менеджера. Обратите внимание, что лямбда - это своего рода отрезная область; Лямбда-захваты ведут себя так, как будто имеют объем объекта. Поэтому будьте осторожны с лямбда-захватами. Также будьте осторожны с тем, как передается сама лямбда - копией или ссылкой.


Когда передавать по значению

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

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


Когда переходить по ссылке и т. Д.

во всех остальных ситуациях проходите мимо указателей, ссылок, умных указателей, дескрипторов (см .: идиома «тело-дескриптор») и т. д. Когда бы ни следовал этому совету, применяйте принцип правильной константности как обычно.

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


Необычные парадигмы

Существуют специальные парадигмы программирования, которые намеренно копируют. Например, обработка строк, сериализация, сетевое взаимодействие, изоляция, упаковка сторонних библиотек, межпроцессное взаимодействие с разделяемой памятью и т. Д. В этих прикладных областях или парадигмах программирования данные копируются из структур в структуры или иногда перепаковываются в байтовые массивы.


Как спецификация языка влияет на этот ответ, прежде чем рассматривается вопрос оптимизации.

Sub-TL; DR Распространение ссылки не должно вызывать код; передача по const-ссылке удовлетворяет этому критерию. Однако все остальные языки удовлетворяют этому критерию без особых усилий.

(Начинающим программистам на C ++ рекомендуется полностью пропустить этот раздел.)

(Начало этого раздела частично навеяно ответом gnasher729. Однако, другой вывод сделан.)

C ++ допускает определяемые пользователем конструкторы копирования и операторы присваивания.

(Это (был) смелый выбор, который был (был) и удивительным, и прискорбным. Это определенно отклонение от сегодняшней приемлемой нормы в языковом дизайне.)

Даже если программист C ++ не определяет его, компилятор C ++ должен сгенерировать такие методы на основе принципов языка, а затем определить, нужно ли выполнять дополнительный код, кроме memcpy. Например, class/, structкоторый содержит std::vectorчлен, должен иметь конструктор копирования и нетривиальный оператор присваивания.

В других языках конструкторы копирования и клонирование объектов не рекомендуется (за исключением случаев, когда это абсолютно необходимо и / или имеет смысл для семантики приложения), поскольку объекты имеют ссылочную семантику в соответствии с языковым дизайном. Эти языки обычно имеют механизм сборки мусора, основанный на достижимости вместо владения на основе области или подсчета ссылок.

Когда в C ++ (или C) передается ссылка или указатель (включая константную ссылку), программисту гарантируется, что никакой специальный код (пользовательские или сгенерированные компилятором функции) не будет выполнен, кроме распространения значения адреса (ссылка или указатель). Это - ясность поведения, с которым программисты на C ++ чувствуют себя комфортно.

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

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


(Не по теме) Раздел, посвященный статье «Хотите скорость? Передайте по значению!» написано около 2009 года.

Эта статья была написана в 2009 году и объясняет обоснование дизайна для r-значения в C ++. Эта статья представляет собой действительный контраргумент моего заключения в предыдущем разделе. Однако пример кода статьи и претензия к производительности уже давно опровергнуты.

Sub-TL; DR Конструкция семантики r-значения в C ++ допускает удивительно элегантную семантику на стороне пользователя Sort, например, для функции. Этот элегантный невозможно моделировать (подражать) на других языках.

Функция сортировки применяется ко всей структуре данных. Как упомянуто выше, было бы медленно, если бы много копировалось. Как оптимизация производительности (это практически актуально), функция сортировки разработана так, чтобы быть разрушительной во многих языках, кроме C ++. Деструктивный означает, что целевая структура данных модифицируется для достижения цели сортировки.

В C ++ пользователь может выбрать вызов одной из двух реализаций: деструктивной с лучшей производительностью или обычной, которая не изменяет ввод. (Шаблон опущен для краткости.)

/*caller specifically passes in input argument destructively*/
std::vector<T> my_sort(std::vector<T>&& input)
{
    std::vector<T> result(std::move(input)); /* destructive move */
    std::sort(result.begin(), result.end()); /* in-place sorting */
    return result; /* return-value optimization (RVO) */
}

/*caller specifically passes in read-only argument*/ 
std::vector<T> my_sort(const std::vector<T>& input)
{
    /* reuse destructive implementation by letting it work on a clone. */
    /* Several things involved; e.g. expiring temporaries as r-value */
    /* return-value optimization, etc. */
    return my_sort(std::vector<T>(input));  
}

/*caller can select which to call, by selecting r-value*/
std::vector<T> v1 = {...};
std::vector<T> v2 = my_sort(v1); /*non-destructive*/
std::vector<T> v3 = my_sort(std::move(v1)); /*v1 is gutted*/    

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

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


Как оптимизация компилятора влияет на этот ответ

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

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

Объекты, размер которых превышает значение регистра компьютера, могут быть скопированы с помощью явных инструкций загрузки / сохранения памяти или путем вызова уважаемой memcpyфункции. На некоторых платформах компилятор генерирует SIMD-инструкции для перемещения между двумя ячейками памяти, каждая из которых перемещается на десятки байтов (16 или 32).


Обсуждение вопроса о многословии или визуальном беспорядке

Программисты C ++ привыкли к этому, то есть, если программист не ненавидит C ++, накладные расходы по написанию или чтению const-ссылки в исходном коде не являются ужасными.

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

Вот что я представляю (без доказательств или достоверных ссылок) ...

  • Да, это влияет на производительность программного обеспечения, написанного на этом языке.
  • Если компиляторы могут понять назначение кода, он может быть достаточно умен, чтобы автоматизировать
  • К сожалению, в языках, которые предпочитают изменчивость (в отличие от функциональной чистоты), компилятор классифицирует большинство вещей как мутировавшие, поэтому автоматическое удержание константности отклонит большинство вещей как неконстантные.
  • Накладные расходы зависят от людей; люди, которые считают это чрезмерным умственным трудом, отвергли бы C ++ как жизнеспособный язык программирования.
rwong
источник
Это одна из ситуаций, когда я хотел бы принять два ответа вместо того, чтобы выбирать только один ... вздох
CharonX
8

В статье Дональда Кнута «StructuredProgrammingWithGoToStatements» он писал: «Программисты тратят огромное количество времени на размышления или беспокойство по поводу скорости некритических частей своих программ, и эти попытки повышения эффективности на самом деле оказывают сильное негативное влияние, когда рассматриваются отладка и обслуживание. Мы должны забыть о малой эффективности, скажем, в 97% случаев: преждевременная оптимизация - корень всего зла. Но мы не должны упускать наши возможности в эти критические 3% ». - преждевременная оптимизация

Это не советует программистам использовать самые медленные из доступных методов. Речь идет о четкости при написании программ. Часто ясность и эффективность являются компромиссом: если вам нужно выбрать только один, выберите ясность. Но если вы можете легко достичь и того, и другого, вам не нужно наносить ущерб ясности (например, сигнализировать, что что-то является константой), просто чтобы избежать эффективности.

Лоренс
источник
3
«Если вы должны выбрать только один, выберите ясность». Вместо этого следует отдать предпочтение второму , так как вы можете быть вынуждены выбрать другое.
Дедупликатор
@Deduplicator Спасибо. Однако в контексте ОП программист имеет свободу выбора.
Лоуренс
Ваш ответ выглядит немного более общим, чем это, хотя ...
Deduplicator
@Deduplicator Ах, но контекст моего ответа (также), что программист выбирает. Если бы выбор был навязан программисту, выбор был бы не у «вас» :). Я рассмотрел предложенное вами изменение и не буду возражать против того, чтобы вы соответствующим образом отредактировали мой ответ, но для ясности предпочитаю существующую формулировку.
Лоуренс
7

Передача ([const] [rvalue] ссылка) | (значение) должна быть о намерении и обещаниях, сделанных интерфейсом. Это не имеет ничего общего с производительностью.

Правило большого пальца Ричи:

void foo(X x);          // I intend to own the x you gave me, whether by copy, move or direct initialisation on the call stack.     

void foo(X&& x);        // I intend to steal x from you. Do not use it other than to re-assign to it after calling me.

void foo(X const& x);   // I guarantee not to change your x

void foo(X& x);         // I may modify your x and I will leave it in a defined state
Ричард Ходжес
источник
3

Теоретически, ответ должен быть да. И действительно, это да, иногда - фактически, передача по константной ссылке вместо простой передачи значения может быть пессимизацией даже в тех случаях, когда переданное значение слишком велико, чтобы поместиться в одном зарегистрироваться (или большинство других эвристиков, которые пытаются использовать, чтобы определить, когда передавать по значению или нет). Несколько лет назад Дэвид Абрахамс написал статью под названием «Хотите скорость? Передайте по значению!» покрывая некоторые из этих случаев. Это уже не легко найти, но если вы можете выкопать копию, ее стоит прочитать (IMO).

В конкретном случае перехода по ссылке сопзЬ, однако, я бы сказал , что идиомы настолько хорошо установлена , что ситуация более или менее наоборот: если вы не знаете типа будет char/ short/ int/ long, люди ожидают увидеть , что прошло по константному ссылка по умолчанию, так что, вероятно, лучше согласиться с этим, если у вас нет достаточно конкретной причины поступить иначе.

Джерри Гроб
источник