Я религиозный человек и стараюсь не совершать грехов. Вот почему я склонен писать маленькие ( меньше, чем это , если перефразировать Роберта К. Мартина) функции, чтобы соответствовать нескольким заповедям, заказанным Библией Чистого кода . Но, проверяя некоторые вещи, я попал на этот пост , ниже которого я прочитал этот комментарий:
Помните, что стоимость вызова метода может быть значительной в зависимости от языка. Почти всегда есть компромисс между написанием читаемого кода и написанием кода на языке исполнения.
При каких условиях это цитируемое утверждение остается в силе и по сей день, учитывая богатую индустрию высокопроизводительных современных компиляторов?
Это мой единственный вопрос. И дело не в том, писать ли мне длинные или маленькие функции. Я просто подчеркиваю, что ваши отзывы могут - или не - внести свой вклад в изменение моего отношения и оставить меня неспособным противостоять искушению богохульников .
источник
for(Integer index = 0, size = someList.size(); index < size; index++)
вместо простогоfor(Integer index = 0; index < someList.size(); index++)
. То, что ваш компилятор был создан за последние несколько лет, не обязательно означает, что вы можете отказаться от профилирования.main()
, другие разбивают все на 50 крошечных функций, и все они совершенно нечитаемы. Хитрость, как всегда, в том, чтобы найти хороший баланс .Ответы:
Это зависит от вашего домена.
Если вы пишете код для микроконтроллера с низким энергопотреблением, то стоимость вызова метода может быть значительной. Но если вы создаете обычный веб-сайт или приложение, то стоимость вызова метода будет незначительной по сравнению с остальной частью кода. В этом случае всегда стоит сосредоточиться на правильных алгоритмах и структурах данных, а не на микрооптимизациях, таких как вызовы методов.
И есть также вопрос компиляции методов для вас. Большинство компиляторов достаточно умны, чтобы встроить функции там, где это возможно.
И наконец, есть золотое правило производительности: ВСЕГДА ПРОФИЛЬ ПЕРВЫЙ. Не пишите «оптимизированный» код, основанный на предположениях. Если вы не уверены, напишите оба случая и посмотрите, какой из них лучше.
источник
Затраты на вызов функции полностью зависят от языка и уровня оптимизации.
На сверхнизком уровне вызовы функций и, тем более, вызовы виртуальных методов могут быть дорогостоящими, если они приводят к ошибочному прогнозированию ветвлений или кешу ЦП. Если вы написали ассемблер , вы также будете знать, что вам нужно несколько дополнительных инструкций для сохранения и восстановления регистров во время вызова. Неверно, что «достаточно умный» компилятор сможет встроить правильные функции, чтобы избежать этих издержек, потому что компиляторы ограничены семантикой языка (особенно вокруг таких функций, как диспетчеризация метода интерфейса или динамически загружаемые библиотеки).
На высоком уровне языки, такие как Perl, Python, Ruby, ведут большую бухгалтерию за вызов функции, что делает их сравнительно дорогостоящими. Это усугубляется метапрограммированием. Однажды я ускорил программное обеспечение Python 3x, просто выводя вызовы функций из очень горячей петли. В критичном к производительности коде вспомогательные функции могут иметь заметный эффект.
Но подавляющее большинство программного обеспечения не настолько критично для производительности, чтобы вы могли заметить накладные расходы на вызовы функций. В любом случае, написание чистого, простого кода окупается:
Если ваш код не критичен к производительности, это облегчает обслуживание. Даже в критически важном программном обеспечении большая часть кода не будет «горячей точкой».
Если ваш код критичен к производительности, простой код облегчает понимание кода и обнаруживает возможности для оптимизации. Самые большие выигрыши обычно не в микрооптимизациях, таких как встроенные функции, а в улучшениях алгоритмов. Или по-другому: не делайте то же самое быстрее. Найдите способ сделать меньше.
Обратите внимание, что «простой код» не означает «разложенный на тысячу крошечных функций». Каждая функция также вносит некоторые когнитивные издержки - сложнее рассуждать о более абстрактном коде. В какой-то момент эти крошечные функции могут сделать так мало, что их отсутствие упростит ваш код.
источник
Почти все пословицы о настройке кода для производительности являются частными случаями закона Амдала . Краткое, юмористическое изложение закона Амдала:
(Оптимизация всего до нуля процентов времени выполнения вполне возможна: когда вы садитесь за оптимизацию большой и сложной программы, вы, скорее всего, обнаружите, что она тратит хотя бы часть времени выполнения на вещи, которые вообще не нужны .)
Вот почему люди обычно говорят не беспокоиться о стоимости вызовов функций: независимо от того, насколько они дороги, обычно программа в целом тратит лишь небольшую часть времени выполнения на накладные расходы, поэтому их ускорение не очень помогает ,
Но если есть хитрость, которую вы можете использовать, которая ускоряет все вызовы функций, этот трюк, вероятно, того стоит. Разработчики компиляторов тратят много времени на оптимизацию функций "прологов" и "эпилогов", потому что это выгодно всем программам, скомпилированным с этим компилятором, даже если это всего лишь малость для каждой.
И, если у вас есть основания полагать , что программа будет проводить большую часть своего времени выполнения просто делает вызовы функций, то вы должны начать думать о том, является ли некоторые из этих вызовов функции не нужны. Вот несколько практических правил для того, чтобы знать, когда вы должны сделать это:
Если время выполнения функции для вызова меньше миллисекунды, но эта функция вызывается сотни тысяч раз, она, вероятно, должна быть встроенной.
Если в профиле программы показаны тысячи функций, и ни одна из них не занимает более 0,1% времени выполнения, тогда издержки на вызовы функций в совокупности, вероятно, значительны.
Если у вас есть « код лазаньи », в котором есть много уровней абстракции, которые едва ли выполняют какую-либо работу, кроме отправки на следующий уровень, и все эти уровни реализованы с помощью вызовов виртуальных методов, то есть хороший шанс, что ЦП тратит впустую много времени на косвенно-отводных трубопроводах. К сожалению, единственное лекарство от этого - избавиться от некоторых слоев, что зачастую очень сложно.
источник
final
классы и методы, где это применимо в Java, или неvirtual
методы в C # или C ++), тогда косвенность может быть устранена компилятором / средой выполнения, и вы ' Увидим выигрыш без масштабной реструктуризации. Как указывает @JorgWMittag выше, JVM может даже встроиться в случаях, когда невозможно доказать, что оптимизация ...Я буду оспаривать эту цитату:
Это действительно вводящее в заблуждение утверждение и потенциально опасное отношение. Есть некоторые конкретные случаи, когда вы должны сделать компромисс, но в целом эти два фактора являются независимыми.
Пример необходимого компромисса - когда у вас есть простой алгоритм по сравнению с более сложным, но более производительным. Реализация с хэш-таблицей, очевидно, более сложна, чем реализация со связанным списком, но поиск будет медленнее, поэтому вам, возможно, придется поменять простоту (которая является фактором читабельности) на производительность.
Что касается затрат на вызов функции, то превращение рекурсивного алгоритма в итеративный может иметь значительное преимущество в зависимости от алгоритма и языка. Но это опять-таки очень специфический сценарий, и в целом накладные расходы на вызовы функций будут незначительными или оптимизированы.
(Некоторые динамические языки, такие как Python, имеют значительные накладные расходы при вызове метода. Но если производительность становится проблемой, вам, вероятно, не стоит использовать Python в первую очередь.)
Большинство принципов читабельного кода - согласованное форматирование, значимые имена идентификаторов, соответствующие и полезные комментарии и т. Д. Не влияют на производительность. А некоторые - например, использование перечислений, а не строк - также имеют преимущества в производительности.
источник
Затраты на вызов функции в большинстве случаев не важны.
Однако больший выигрыш от встраивания кода заключается в оптимизации нового кода после встраивания .
Например, если вы вызываете функцию с постоянным аргументом, оптимизатор теперь может постоянно сворачивать этот аргумент там, где он не мог до вставки вызова. Если аргумент является указателем на функцию (или лямбду), оптимизатор теперь может также встроить вызовы этой лямбды.
Это большая причина, по которой виртуальные функции и указатели функций не привлекательны, так как вы не можете встроить их вообще, если фактический указатель функции не был постоянно согнут на всем пути к сайту вызова.
источник
Предполагая, что производительность имеет значение для вашей программы, и она действительно имеет много и много вызовов, стоимость все еще может иметь значение, а может и не иметь значения, в зависимости от типа вызова.
Если вызываемая функция мала и компилятор может ее встроить, тогда стоимость будет практически нулевой. Современные компиляторы / языковые реализации имеют JIT, оптимизацию времени соединения и / или модульные системы, разработанные для максимизации способности встроенных функций, когда это выгодно.
OTOH, есть неочевидные затраты на вызовы функций: их простое существование может помешать оптимизации компилятора до и после вызова.
Если компилятор не может рассуждать о том, что делает вызываемая функция (например, это виртуальная / динамическая диспетчеризация или функция в динамической библиотеке), то ему, возможно, придется пессимистически предположить, что функция может иметь какой-либо побочный эффект - вызвать исключение, изменить глобальное состояние или изменение любой памяти, видимой через указатели. Компилятору может потребоваться сохранить временные значения в памяти и перечитать их после вызова. Он не сможет переупорядочить инструкции вокруг вызова, поэтому он не сможет векторизовать циклы или поднять избыточные вычисления из циклов.
Например, если вы без необходимости вызываете функцию в каждой итерации цикла:
Компилятор может знать, что это чистая функция, и вывести ее из цикла (в таком ужасном случае, как этот пример, даже исправляет случайный алгоритм O (n ^ 2) как O (n)):
А затем, возможно, даже переписать цикл для обработки 4/8/16 элементов за один раз, используя команды wide / SIMD.
Но если вы добавите вызов к некоторому непрозрачному коду в цикле, даже если этот вызов ничего не делает и сам по себе очень дешев, компилятор должен предположить самое худшее - что вызов получит доступ к глобальной переменной, которая указывает на ту же память, что и
s
изменение его содержимое (даже если оноconst
в вашей функции, оно может быть неconst
где-либо еще), что делает оптимизацию невозможной:источник
Эта старая статья может ответить на ваш вопрос:
Абстрактные:
источник
В C ++ остерегайтесь проектирования вызовов функций, которые копируют аргументы, по умолчанию это «передача по значению». Затраты на вызов функции из-за сохранения регистров и других вещей, связанных с кадрами стека, могут быть перегружены непреднамеренной (и потенциально очень дорогой) копией объекта.
Существуют оптимизации, связанные с кадрами стека, которые вы должны изучить, прежде чем отказываться от сильно разложенного кода.
Большую часть времени, когда мне приходилось иметь дело с медленной программой, я обнаружил, что внесение изменений в алгоритмы привело к гораздо большему ускорению, чем встроенные вызовы функций. Например: другой инженер переделал парсер, который заполнил структуру map-of-maps. В рамках этого он удалил кэшированный индекс из одной карты в логически связанный. Это был хороший шаг устойчивости кода, однако он сделал программу непригодной к использованию из-за замедления в 100 раз из-за выполнения поиска хеша для всех будущих обращений по сравнению с использованием сохраненного индекса. Профилирование показало, что большую часть времени занимала функция хеширования.
источник
Да, прогнозирование пропущенных переходов обходится дороже на современном оборудовании, чем это было десятилетия назад, но компиляторы стали намного умнее оптимизировать это.
В качестве примера рассмотрим Java. На первый взгляд, издержки вызова функций должны быть особенно доминирующими в этом языке:
В ужасе от этих практик средний программист на С мог бы предсказать, что Java должна быть как минимум на порядок медленнее, чем С. И 20 лет назад он был бы прав. Современные тесты, однако, помещают идиоматический Java-код в несколько процентов от эквивалентного C-кода. Как это возможно?
Одна из причин заключается в том, что современные встроенные функции JVM вызывают само собой разумеющееся. Это делается с помощью умозрительного встраивания:
То есть код:
переписывается
И, конечно, среда выполнения достаточно умна, чтобы перемещать эту проверку типов до тех пор, пока точка не назначена, или исключить ее, если тип известен вызывающему коду.
Таким образом, если даже Java управляет автоматическим встраиванием методов, то нет никакой внутренней причины, по которой компилятор не может поддерживать автоматическое встраивание, и есть все основания для этого, потому что встраивание очень полезно для современных процессоров. Поэтому я едва ли могу представить себе какой-либо современный основной компилятор, не знающий об этой самой базовой стратегии оптимизации, и предположил бы, что компилятор способен на это, если не доказано обратное.
источник
Как говорят другие, сначала вы должны измерить производительность вашей программы, и, вероятно, вы не найдете никакой разницы на практике.
Тем не менее, с концептуального уровня я решил прояснить несколько вещей, которые связаны в вашем вопросе. Во-первых, вы спрашиваете:
Обратите внимание на ключевые слова «функция» и «компиляторы». Ваша цитата тонко отличается:
Это говорит о методах в объектно-ориентированном смысле.
Хотя «функция» и «метод» часто используются взаимозаменяемо, существуют различия, когда речь заходит об их стоимости (о которой вы спрашиваете) и когда речь идет о компиляции (которой вы дали контекст).
В частности, нам нужно знать о статической диспетчеризации и динамической диспетчеризации . Я буду игнорировать оптимизацию на данный момент.
На языке, подобном C, мы обычно вызываем функции со статической диспетчеризацией . Например:
Когда компилятор видит вызов
foo(y)
, он знает, на какую функциюfoo
ссылается это имя, поэтому программа вывода может сразу перейти кfoo
функции, что довольно дешево. Вот что означает статическая отправка .Альтернативой является динамическая диспетчеризация , когда компилятор не знает, какая функция вызывается. В качестве примера, вот некоторый код на Haskell (поскольку эквивалент C был бы грязным!):
Здесь
bar
функция вызывает свой аргументf
, который может быть чем угодно. Следовательно, компилятор не может просто скомпилироватьbar
инструкцию быстрого перехода, потому что он не знает, куда перейти. Вместо этого код, для которого мы генерируемbar
,f
разыскивает, чтобы выяснить, на какую функцию он указывает, а затем перейти к ней. Вот что означает динамическая отправка .Оба эти примера предназначены для функций . Вы упомянули методы , которые можно рассматривать как особый стиль динамически отправляемой функции. Например, вот немного Python:
y.foo()
Вызов использует динамическую отправку, так как он смотрит вверх значениеfoo
свойства вy
объекте, и называя все , что он находит; он не знает, чтоy
будет иметь классA
, или чтоA
класс содержитfoo
метод, поэтому мы не можем просто перейти к нему.ОК, это основная идея. Обратите внимание, что статическая отправка выполняется быстрее, чем динамическая, независимо от того, компилируем мы или интерпретируем; при прочих равных условиях Разыменование в любом случае требует дополнительных затрат.
Итак, как это влияет на современные оптимизирующие компиляторы?
Первое, на что нужно обратить внимание, - это то, что статическая диспетчеризация может быть оптимизирована более интенсивно: когда мы знаем, к какой функции мы обращаемся, мы можем делать такие вещи, как встраивание. С динамической диспетчеризацией мы не знаем, что мы прыгаем до времени выполнения, поэтому мы не можем сделать большую оптимизацию.
Во-вторых, в некоторых языках можно определить, куда будут переходить некоторые динамические диспетчеры, и, следовательно, оптимизировать их в статическую диспетчеризацию. Это позволяет нам выполнять другие оптимизации, такие как встраивание и т. Д.
В приведенном выше примере Python такой вывод довольно безнадежен, так как Python позволяет другому коду переопределять классы и свойства, поэтому сложно сделать вывод о том, что будет иметь место во всех случаях.
Если наш язык позволяет нам накладывать больше ограничений, например, ограничивая
y
классA
с помощью аннотации, то мы можем использовать эту информацию для вывода целевой функции. В языках с подклассами (а это почти все языки с классами!) Этого на самом деле недостаточно, поскольку наy
самом деле может иметься другой (под) класс, поэтому нам потребуется дополнительная информация, такая какfinal
аннотации Java, чтобы точно знать, какая функция будет вызвана.Haskell не является язык OO, но мы можем сделать вывод , значение
f
по встраиваниюbar
(который статический отправляются) вmain
подставляяfoo
дляy
. Так как цельfoo
inmain
статически известна, вызов становится статически распределенным и, вероятно, будет полностью встроен и оптимизирован (поскольку эти функции невелики, компилятор с большей вероятностью их встроит; хотя в целом мы не можем рассчитывать на это) ).Следовательно, стоимость сводится к:
Если вы используете «очень динамичный» язык, с большим количеством динамической диспетчеризации и несколькими гарантиями, доступными для компилятора, то каждый вызов будет стоить. Если вы используете «очень статичный» язык, то зрелый компилятор будет создавать очень быстрый код. Если вы находитесь между ними, то это может зависеть от вашего стиля кодирования и от того, насколько умна реализация.
источник
К сожалению, это сильно зависит от:
Прежде всего, первый закон оптимизации производительности - это профиль первым . Есть много областей, где производительность программной части не имеет отношения к производительности всего стека: вызовы базы данных, сетевые операции, операции ОС, ...
Это означает, что производительность программного обеспечения совершенно не имеет значения, даже если она не улучшает задержки, оптимизация программного обеспечения может привести к экономии энергии и экономии оборудования (или экономии батареи для мобильных приложений), что может иметь значение.
Тем не менее, они, как правило, НЕ могут быть ошеломлены, и часто алгоритмические улучшения превосходят микрооптимизацию с большим отрывом.
Итак, перед оптимизацией вам нужно понять, для чего вы оптимизируете ... и стоит ли это того.
Теперь, что касается чистой производительности программного обеспечения, она сильно варьируется между наборами инструментов.
Для вызова функции есть две стоимости:
Стоимость времени выполнения довольно очевидна; для выполнения вызова функции необходимо определенное количество работы. Например, при использовании C на платформе x86 вызов функции потребует (1) пролить регистры в стек, (2) передать аргументы в регистры, выполнить вызов и впоследствии (3) восстановить регистры из стека. Посмотрите это краткое изложение соглашений о вызовах, чтобы увидеть проделанную работу .
Этот разлив / восстановление регистра занимает нетривиальное количество раз (десятки циклов ЦП).
Обычно ожидается, что эта стоимость будет тривиальной по сравнению с фактической стоимостью выполнения функции, однако некоторые шаблоны здесь контрпродуктивны: методы получения, функции, защищенные простым условием, и т. Д.
Поэтому, кроме интерпретаторов , программист будет надеяться, что их компилятор или JIT оптимизируют ненужные вызовы функций; хотя эта надежда иногда может не принести плодов. Потому что оптимизаторы не волшебство.
Оптимизатор может обнаружить, что вызов функции тривиален, и встроить вызов: по сути, скопировать / вставить тело функции на сайте вызова. Это не всегда хорошая оптимизация (может вызвать вздутие живота), но в целом стоит, потому что встраивание предоставляет контекст , а контекст позволяет проводить больше оптимизаций.
Типичный пример:
Если
func
указывается, то оптимизатор поймет, что ветка никогда не берется, и оптимизироватьcall
доvoid call() {}
.В этом смысле вызовы функций, скрывая информацию от оптимизатора (если она еще не указана), могут препятствовать определенной оптимизации. Вызовы виртуальных функций особенно виноваты в этом, потому что девиртуализация (доказательство того, какая функция в конечном итоге вызывается во время выполнения) не всегда легка.
В заключение, мой совет - сначала написать четко , избегая преждевременной алгоритмической пессимизации (кубическая сложность или худшие укусы быстро), а затем оптимизировать только то, что требует оптимизации.
источник
Я просто собираюсь изо всех сил сказать никогда. Я считаю, что цитата безрассудна, чтобы просто выбросить туда.
Конечно, я не говорю полной правды, но мне все равно, что я буду правдивым. Это как в этом фильме «Матрица», я забыл, был ли это 1, 2 или 3 - я думаю, что это был фильм с сексуальной итальянской актрисой с большими дынями (мне не очень понравился, кроме первого), когда оракулистка сказала Киану Ривзу: «Я только что сказала тебе то, что тебе нужно было услышать» или что-то в этом роде, вот что я хочу сделать сейчас.
Программистам не нужно это слышать. Если они имеют опыт работы с профилировщиками в своих руках, и эта цитата в некоторой степени применима к их компиляторам, они уже будут знать это и узнают об этом надлежащим образом, если они понимают свой результат профилирования и почему определенные листовые вызовы являются горячими точками, посредством измерения. Если они не имеют опыта и никогда не профилировали свой код, это последнее, что им нужно услышать, что они должны суеверно пойти на компромисс с тем, как они пишут код, вплоть до вставки всего, прежде чем даже определять горячие точки в надежде, что это стать более производительным.
Во всяком случае, для более точного ответа, это зависит. Некоторые из множества условий уже перечислены среди хороших ответов. Возможные условия выбора одного языка уже сами по себе огромны, например, C ++, который должен был бы вступать в динамическую диспетчеризацию при виртуальных вызовах, и когда его можно оптимизировать, и при каких условиях компиляторы и даже компоновщики, и это уже требует подробного ответа, не говоря уже о попытке решать условия на всех возможных языках и компилятор там. Но я добавлю сверху "кого это волнует?" потому что даже работая в таких критических для производительности областях, как трассировка лучей, последнее, что я когда-либо начну делать заранее, - это ручное встраивание, прежде чем проводить какие-либо измерения.
Я верю, что некоторые люди слишком усердствуют, предлагая вам никогда не проводить микрооптимизацию перед измерением. Если оптимизация для локального подсчета ссылок является микрооптимизацией, то я часто начинаю применять такие оптимизации с самого начала, ориентируясь на ориентированный на данные дизайн в тех областях, которые, я знаю, наверняка будут критичными для производительности (например, код трассировки лучей), потому что в противном случае я знаю, что мне придется переписывать большие разделы вскоре после того, как проработал в этих областях годами. Оптимизация представления данных для попаданий в кэш часто может иметь такие же улучшения производительности, как и алгоритмические улучшения, если только мы не говорим как квадратичное время к линейному.
Но я никогда не вижу веской причины начинать встраивание перед измерениями, тем более что профилировщики приличны в раскрытии того, что может принести пользу от встраивания, но не в раскрытии того, что может принести пользу от отсутствия встраивания (и отсутствие встраивания может фактически сделать код быстрее, если вызов функции без подстановки - это редкий случай, улучшающий локальность ссылок для icache для горячего кода, а иногда даже позволяющий оптимизаторам лучше выполнять работу для общего пути выполнения).
источник