Эквивалентен ли по производительности код, представленный ниже? Ответ: нет. За подробностями и объяснениями прошу под кат. Доброго времени суток, перед тем, как раскрыть тему, хотел бы представиться.
Меня зовут Искандер и я время от времени отправляю коммиты в репозиторий golang/go. Раньше я делал это от лица команды Intel Go, но наши пути разошлись и теперь я независимый контрибьютор. С недавних пор работаю в vk в команде инфраструктуры. В свободное время делаю разные инструменты для Go, типа go-critic и go-consistent. А ещё я рисую гоферов. 
 Сразу приступим к сравнению и набросаем бенчмарк: Вместо того, чтобы рекомендовать вам benchstat, я покажу вам benchrun. С помощью одной команды мы можем запустить оба бенчмарка и получить сравнение: Вариант с ручным встраиванием гораздо быстрее кода, который был получен при встраивании тела функции компилятором. Попробуем разобраться почему это происходит. Вспомним реализацию strings.HasPrefix: Функция HasPrefix встраивается компилятором.
Проверить это можно следующим образом: Для вызова strings.HasPrefix из варианта (A) мы получаем следующий машинный код: Не обращайте внимания на то, что код выглядит как лапша. На что стоит обратить внимание: Но что же тогда генерируется для встроенного вручную варианта, кода из примера (B)? А вот тут компилятор не генерирует вызова runtime.memequal и производится сравнение единственного символа напрямую. В идеале, то же самое он должен был сделать и для первого варианта. Мы наблюдаем слабую сторону оптимизатора Go, её и разберём. Причина, по которой вызов strings.HasPrefix(s, "#") может быть оптимизирован в том, что аргумент-префикс является константой. Нам известна его длина и содержимое. Нет смысла вызывать runtime.memequal для коротких строк, быстрее сделать сравнение символов "на месте", избегая лишнего вызова. Как вы знаете, в компиляторах обычно есть как минимум две части: compiler frontend и compiler backend. Первая работает с более высокоуровневым представлением, вторая уже ближе к машине и промежуточное представление будет похожим на поток инструкций. Уже несколько версий в Go используется SSA представление для оптимизаций в backend части компилятора. Сворачивание констант, такое как {10*2 => 20}, реализовано в backend'е. Вообще большинство операций, связанных с понижением вычислительной стоимости выражений находятся именно в этой части компилятора. Но есть исключения. Одним из исключений является оптимизация сравнений константных строк. Когда компилятор видит сравнение строк (или подстрок), в котором один или оба операнда являются константами, генерируется более эффективный код, нежели вызов runtime.memequal. Посмотреть отвечающий за это исходный код можно в файле cmd/compile/internal/gc/walk.go:3362. Встраивание функций происходит до запуска этих оптимизаций, но тоже во frontend части компилятора. Казалось бы, что же всё-таки не даёт этой оптимизации сработать в нашем случае? Вот как будет происходить встраивание: При встраивании функций компилятор присваивает аргументы временным переменным, что ломает оптимизации, поскольку алгоритм в walk.go видит не константы, а аргументы с переменными. В этом проблема. К слову, оптимизациям из backend'а, которые имеют в распоряжении SSA, это не мешает. Но там присутствуют другие проблемы, например, невозможность восстановить высокоуровневые конструкции языка для их эффективного сопоставления (работа по устранению этого недостатка "планируется" уже несколько лет). Представим функцию, которой важно выделять временный буфер на стеке: Поскольку buf не "убегает", компилятор сможет выделить эти 16 байтов на стеке, без аллокации на куче. Опять же, всё благодаря константному значению при вызове make. Для выделения памяти на стеке нам важно знать требуемый размер, который будет входить в состав фрейма, отводимого под вызов функции. Допустим, в будущем нам захотелось выделять временные буферы разных размеров и инкапсулировать некоторую логику в методах. Мы ввели новую абстракцию и решили везде использовать новый тип tmpBuf. Конструирующая функция предельно проста: Адаптируем исходный пример: Конструктор будет встраиваться, но вот аллокация теперь всегда будет на куче, по той же причине передачи аргументов через временные переменные. Escape analysis будет видеть make([]byte, 0, _sizeHint), что не попадает под его паттерны распознавания оптимизируемых вызовов make. Если бы у нас было "всё как у людей", проблемы бы не существовало, после встраивания конструктора newTmpBuf было бы ясно, что размер всё так же известен на этапе компиляции. Это огорчает чуть ли не сильнее, чем ситуация со сравнением строк.  Ситуацию можно довольно легко поправить и я уже выслал первую часть решения.  Если вы считаете, что описанная в статье проблема действительно нуждается в решении, поставьте, пожалуйста, палец вверх в соответствующем issue.   Моя позиция состоит в том, что встраивать код руками только потому что он работает быстрее в текущей версии Go — это неправильно. Нужно исправить этот недочёт в оптимизаторе, хотя бы до того уровня, чтобы примеры, описанные выше, работали без неожиданных регрессий производительности. Если всё пойдёт по плану, эта оптимизация войдёт в релиз Go 1.13. Спасибо за внимание. Эта секция для самых храбрых, тех, кто ещё не устал читать. Итак, у нас несколько мест, которые работают хуже при использовании переменных вместо их значений напрямую. Предложенное решение состоит в том, чтобы ввести во frontend части компилятора новую функцию, которая позволяет по имени получить последнее привязанное значение. После этого в каждой оптимизации, которая ожидает константное значение, не сдаваться при обнаружении переменной, а получать это ранее сохранённое состояние. Сигнатура нашей новой функции может выглядеть так: Определение Node можно посмотреть в файле syntax.go. Каждому определению переменной соответствует Node с тегом ONAME. Внутри Node.Name.Defn для большинства таких переменных имеется инициализирующее значение. Если Node уже литерал, ничего делать не нужно и мы просто возвращаем n. Если же это ONAME (переменная), то можно попробовать извлечь из n.Name.Defn то самое инициализирующее значение. Но как быть с модификациями между объявлением и чтением переменной, для которой мы вызываем getConstValue? Если мы ограничимся только read-only переменными, то никакой проблемы не существует. В frontend'е Go уже есть специальные флаги ноды, которые отмечают подобные имена. Если переменная модифицировалась, getConstValue не будет возвращать инициализирующее значение. Программисты, обычно, не модифицируют входные аргументы числовых и строковых типов и это позволяет покрыть довольно большое число случаев этим примитивным алгоритмом. Теперь мы готовы рассмотреть реализацию: Примерно вот так меняется код, который зависит от констант: Отлично, и оно даже работает: До этого изменения escape analysis не мог получить через n значение 10, из-за чего делал предположение о необходимости разместить xs в куче. Код выше синтаксически похож на ситуацию, наблюдаемую при встраивании. n может быть временной переменной, которая добавляется при передаче аргумента. К сожалению, есть нюансы. Мы решили проблему для локальных переменных, вводимых через OAS, но Go инициализирует переменные для встроенных функций через OAS2. Из-за этого нам понадобится второе изменение, расширяющее функцию getConstValue и немного модифицирующее код самого inliner'а, ведь, помимо прочего, OAS2 не имеет подходящего поля Defn. Это были плохие новости. Хорошие новости: в русскоязычном слаке появился канал #gocontributing, где можно делиться своими идеями и планами, задавать вопросы, а также обсуждать всё, что связано с участием в разработке Go. Making developer tools & video games