Как вызывается функция в с
Функции (functions) в C++: перегрузки и прототипы
Привет, дорогой читатель! Вам, наверное, приходилось в программе использовать один и тот же блок кода несколько раз. Например, выводить на экран одну и ту же строчку. Для того, чтобы каждый раз не писать одинаковый блок кода в C++, присутствуют функции.
Сегодня мы разберем, что такое функции и как правильно их использовать в своей программе. Поехали!
Что такое функции
Функции — это блок кода, который вы можете использовать в любом участке вашей программы неограниченное количество раз. Например, в программе ниже мы выводим 2 строки (без применения функций):
А вот если бы мы использовали функции, то у нас получилось бы так:
Мы хотим, чтобы вы обратили внимание на увеличение количества строк в первой программе при выводе этих двух строк 5 раз.
Как видите, если правильно применять функции, то можно уменьшить программу в несколько раз. Но вы должны помнить — бессмысленно использовать функции без видимых оснований (например, если логика внутри функции слишком специфична).
Вашему компилятору будет совершенно без разницы, использовали вы функции или несколько раз вставили одинаковый блок кода, в итоге он выведет одинаковый результат.
Чтобы понять, как работают локальные переменные (например, переменные в функциях) и глобальные переменные, можете почитать данную статью.
Как создать функции в C++
Таким образом, чтобы создать функции, нужно использовать конструкцию, которая находится пониже:
Давайте разберем эту конструкцию:
Если вы не знали main() — это тоже функция.
Как вызывать функцию
Для вызова функций вам нужно использовать такую конструкцию:
Например, выше для вызова функции stroka() (эта функция находится выше) нам нужно использовать такую конструкцию:
Как видите, мы не вписывали аргументы в круглые скобки, так как мы их не указали при создании функции.
Зачем использовать функции
Практически все программисты на текущее время постоянно используют функции, поскольку это экономит их время и позволяет более интенсивно работать. Особенно хорошо отзываются те программисты, которые уже работали над большими проектами, имеющими тысячи строк кода за собой.
А если бы вы использовали функцию, которая выводила сообщение «Привет!», то тогда бы вам пришлось только найти эту функцию и изменить ее!
Перегрузка функций
В С++ вы можете создавать функции с одинаковыми именами. Вы наверно удивлены, что такое вообще возможно так, как если у нас будет 3 функции с одинаковыми именами и мы захотим вызвать одну из этих функций, то мы таким образом вызовем все 3 функции и получится полная каша. Но компилятор думает про это совершенно по-другому.
Все дело в том, что у каждой функции есть свое полное имя (или по-другому сигнатура). Параметры функции — это вся информация о функции. В эту информацию входят:
Именно поэтому компилятор считает функции с одинаковыми именами разными, если сигнатуры соответственно тоже разные.
Перегрузка функций — это создание функций с одинаковыми именами, но с разными сигнатурами (полными именами).
В примере ниже все функции разные, хотя и имена у них одинаковые:
2.1 – Знакомство с функциями в C++
В предыдущей главе мы определили функцию как набор инструкций, которые выполняются последовательно. Хотя это, безусловно, правда, это определение не дает подробного понимания того, почему функции полезны. Давайте обновим наше определение: функция – это многократно используемая последовательность инструкций, предназначенная для выполнения определенной работы.
Рассмотрим случай, который может иметь место в реальной жизни: вы читаете книгу и вспоминаете, что вам нужно позвонить по телефону. Вы помещаете закладку в свою книгу, совершаете телефонный звонок, а когда заканчиваете телефонный звонок, вы возвращаетесь в место, которое отметили закладкой, и продолжаете читать книгу с того места, где остановились.
Программы на C++ могут работать точно так же. Программа будет последовательно выполнять инструкции внутри одной функции, пока не обнаружит вызов другой функции. Вызов функции – это выражение, которое говорит CPU прервать текущую функцию и выполнить другую функцию. CPU «помещает закладку» в текущую точку выполнения, а затем вызывает (выполняет) функцию, указанную в вызове функции. Когда вызываемая функция завершается, CPU возвращается в точку, отмеченную закладкой, и возобновляет выполнение.
Функция, инициирующая вызов функции, называется вызывающей функцией или caller, а вызываемая функция – callee.
Пример пользовательской функции
Во-первых, давайте начнем с самого базового синтаксиса для определения пользовательской функции. В этом уроке все пользовательские функции (кроме main ) будут иметь следующий вид:
Мы поговорим подробнее о различных частях этого синтаксиса в нескольких следующих уроках. На данный момент идентификатор будет просто заменен именем вашей пользовательской функции. Фигурные скобки и инструкции между ними называются телом функции.
Вот пример программы, которая показывает, как определяется и вызывается новая функция:
Эта программа создает следующий вывод:
Предупреждение
Не забывайте при вызове функции ставить круглые скобки () после имени функции.
Вызов функций более одного раза
Одна полезная особенность функций заключается в том, что их можно вызывать более одного раза. Вот программа, которая демонстрирует это:
Эта программа создает следующий вывод:
Так как doPrint вызывается из main дважды, doPrint выполняется дважды, и In doPrint() печатается дважды (один раз для каждого вызова).
Функции, вызывающие функции, вызывающие функции
Эта программа создает следующий вывод:
Вложенные функции не поддерживаются
В отличие от некоторых других языков программирования, в C++ функции не могут быть определены внутри других функций. Следующая программа не является корректной:
Правильный способ написать приведенную выше программу:
Небольшой тест
Вопрос 1
Как в определении функции называются фигурные скобки и инструкции между ними?
Вопрос 2
Что печатает следующая программа? Не компилируйте ее, просто отследите код самостоятельно.
Функции (C++)
Функцию можно вызывать или вызыватьиз любого числа мест в программе. Значения, передаваемые в функцию, являются аргументами, типы которых должны быть совместимы с типами параметров в определении функции.
Длина функции практически не ограничена, однако для максимальной эффективности кода целесообразно использовать функции, каждая из которых выполняет одиночную, четко определенную задачу. Сложные алгоритмы лучше разбивать на более короткие и простые для понимания функции, если это возможно.
Функции, определенные в области видимости класса, называются функциями-членами. В C++, в отличие от других языков, функции можно также определять в области видимости пространства имен (включая неявное глобальное пространство имен). Такие функции называются бесплатными функциями или функциями, не являющимися членами. Они широко используются в стандартной библиотеке.
Функции могут быть перегружены, а это значит, что разные версии функции могут совместно использовать одно и то же имя, если они отличаются числом и (или) типом формальных параметров. Дополнительные сведения см. в разделе перегрузка функций.
Части объявления функции
Минимальное объявление функции состоит из возвращаемого типа, имени функции и списка параметров (который может быть пустым) вместе с дополнительными ключевыми словами, которые предоставляют компилятору дополнительные инструкции. В следующем примере показано объявление функции:
Определение функции состоит из объявления, а также тела, который является кодом между фигурными скобками:
Объявление функции, за которым следует точка с запятой, может многократно встречаться в разных местах кода программы. Оно необходимо перед любыми вызовами этой функции в каждой записи преобразования. По правилу одного определения, определение функции должно фигурировать в коде программы лишь один раз.
При объявлении функции необходимо указать:
Возвращаемый тип, указывающий тип значения, возвращаемого функцией, или void значение, если значения не возвращаются. В C++ 11 auto является допустимым возвращаемым типом, который указывает компилятору вывести тип из оператора return. В C++ 14 decltype(auto) также разрешено. Дополнительные сведения см. в подразделе «Выведение возвращаемых типов» ниже.
Имя функции, которое должно начинаться с буквы или символа подчеркивания и не должно содержать пробелов. В стандартной библиотеке со знака подчеркивания обычно начинаются имена закрытых функций-членов или функций, не являющихся членами и не предназначенных для использования в вашем коде.
Список параметров, заключенный в скобки. В этом списке через запятую указывается нужное (возможно, нулевое) число параметров, задающих тип и, при необходимости, локальное имя, по которому к значениям можно получить доступ в теле функции.
Необязательные элементы объявления функции:
constexpr — указывает, что возвращаемое значение функции является константой, значение которой может быть определено во время компиляции.
Дополнительные сведения см. в разделе Преобразование единиц и компоновки.
inline — отдает компилятору команду заменять каждый вызов функции ее кодом. Подстановка может улучшить эффективность кода в сценариях, где функция выполняется быстро и многократно вызывается во фрагментах, являющихся критическими для производительности программы.
Дополнительные сведения см. в разделе встроенные функции.
(только функции-члены) static применение к функции-члену означает, что функция не связана ни с одним экземпляром объекта класса.
(Только функции-члены, не являющиеся статическими) Квалификатор ref, указывающий компилятору, какую перегрузку функции следует выбрать, когда неявный параметр объекта ( *this ) является ссылкой rvalue или ссылкой lvalue. Дополнительные сведения см. в разделе перегрузка функций.
На следующем рисунке показаны компоненты определения функции. Затененная область является телом функции.
Части определения функции
Определения функций
Определение функции состоит из объявления и тела функции, заключенной в фигурные скобки, которые содержат объявления переменных, инструкции и выражения. В следующем примере показано полное определение функции:
Переменные, объявленные в теле функции, называются локальными. Они исчезают из области видимости при выходе из функции, поэтому функция никогда не должна возвращать ссылку на локальную переменную.
функции const и constexpr
Шаблоны функций
Шаблоны функций подобны шаблонам классов. Их задача заключается в создании конкретных функций на основе аргументов шаблонов. Во многих случаях шаблоны могут определять типы аргументов, поэтому их не требуется явно указывать.
Параметры и аргументы функций
У функции имеется список параметров, в котором через запятую перечислено необходимое (возможно, нулевое) число типов. Каждому параметру присваивается имя, по которому к нему можно получить доступ в теле функции. В шаблоне функции могут указываться дополнительные типы или значения параметров. Вызывающий объект передает аргументы, представляющие собой конкретные значения, типы которых совместимы со списком параметров.
По умолчанию аргументы передаются функции по значению, то есть функция получает копию передаваемого объекта. Копирование крупных объектов может быть ресурсозатратным и неоправданным. Чтобы аргументы передавались по ссылке (в частности в ссылке lvalue), добавьте в параметр квантификатор ссылки.
Если функция изменяет аргумент, передаваемый по ссылке, изменяется исходный объект, а не его локальная копия. Чтобы предотвратить изменение такого аргумента функцией, укажите для параметра значение const & :
C++ 11: Чтобы явно отреагировать на аргументы, передаваемые по ссылке rvalue или lvalue-Reference, используйте двойной амперсанд для параметра, чтобы указать универсальную ссылку:
Функция, объявленная с ключевым словом Single void в списке объявлений параметров, не принимает аргументов, если ключевое слово void является первым и единственным членом списка объявлений аргумента. Аргументы типа void в любом расположении в списке выдают ошибки. Пример:
Обратите внимание, что, хотя недопустимо указывать void аргумент, за исключением описанного здесь, типы, производные от типа void (например, указатели на void и массивы void ), могут отображаться в любом месте списка объявлений аргумента.
Аргументы по умолчанию
Последним параметрам в сигнатуре функции можно назначить аргумент по умолчанию, т. е. вызывающий объект сможет опустить аргумент при вызове функции, если не требуется указать какое-либо другое значение.
Дополнительные сведения см. в разделе аргументы по умолчанию.
типов возвращаемых функциями значений;
Завершающие возвращаемые типы
«Обычные» возвращаемые типы расположены слева от сигнатуры функции. Завершающий возвращаемый тип расположен в правой части сигнатуры и предшествует оператору. Завершающие возвращаемые типы особенно полезны в шаблонах функций, когда тип возвращаемого значения зависит от параметров шаблона.
Если auto используется в сочетании с завершающим возвращаемым типом, он просто выступает в качестве заполнителя для любого результата выражения decltype и не выполняет выведение типов.
Локальные переменные функции
Выведение возвращаемых типов (C++14)
В этом примере auto будет выведена как неконстантная копия значения суммы LHS и RHS.
Обратите внимание, что не auto сохраняет константу-rvalue характеристики типа, который он выводит. Для функций перенаправления, возвращаемое значение которых должно сохранять аргументы const-rvalue характеристики или ref-rvalue характеристики из своих аргументов, можно использовать decltype(auto) ключевое слово, которое использует decltype правила вывода типа и сохраняет все сведения о типе. decltype(auto) может использоваться как обычное возвращаемое значение с левой стороны или как завершающее возвращаемое значение.
В следующем примере (на основе кода из N3493) показано, как использовать, чтобы обеспечить точную пересылку аргументов функции в возвращаемый тип, который не известен до создания экземпляра шаблона.
Возврат нескольких значений из функции
Существует несколько способов вернуть более одного значения из функции:
Инкапсулирует значения в именованном классе или объекте структуры. Требует, чтобы определение класса или структуры было видимым для вызывающего объекта:
Возвращает объект «канал» std:: Tuple или std::p Air:
Visual Studio 2017 версии 15,3 и более поздних версий (доступно в режиме и более поздних версиях): используйте структурированные привязки. Преимущество структурированных привязок заключается в том, что переменные, хранящие возвращаемые значения, инициализируются одновременно с объявлением, что в некоторых случаях может быть значительно более эффективным. В операторе auto[x, y, z] = f(); скобки представляют и инициализируют имена, которые находятся в области действия для всего блока Function.
Помимо использования возвращаемого значения, можно «возвращать» значения, определив любое количество параметров для использования передачи по ссылке, чтобы функция могла изменять или инициализировать значения объектов, предоставляемых вызывающим объектом. Дополнительные сведения см. в разделе аргументы функции ссылочного типа.
Указатели функций
Как и в C, в C++ поддерживаются указатели на функции. Однако более типобезопасной альтернативой обычно служит использование объекта-функции.
Рекомендуется typedef использовать для объявления псевдонима для типа указателя функции при объявлении функции, возвращающей тип указателя функции. Например.
Если оно не используется, то правильный синтаксис объявления функции можно вывести из синтаксиса декларатора для указателя на функцию, заменив идентификатор (в приведенном выше примере — fp ) на имя функции и список аргументов, как показано выше:
Предыдущее объявление эквивалентно объявлению, typedef приведенному выше.
Функции в языке Си
Функция — это самостоятельная единица программы, которая спроектирована для реализации конкретной подзадачи.
Функция является подпрограммой, которая может содержаться в основной программе, а может быть создана отдельно (в библиотеке). Каждая функция выполняет в программе определенные действия.
Сигнатура функции определяет правила использования функции. Обычно сигнатура представляет собой описание функции, включающее имя функции, перечень формальных параметров с их типами и тип возвращаемого значения.
Семантика функции определяет способ реализации функции. Обычно представляет собой тело функции.
Определение функции
Каждая функция в языке Си должна быть определена, то есть должны быть указаны:
Определение функции имеет следующий синтаксис:
Пример : Функция сложения двух вещественных чисел
Различают системные (в составе систем программирования) и собственные функции.
Собственные функции — это функции, написанные пользователем для решения конкретной подзадачи.
Разбиение программ на функции дает следующие преимущества:
С точки зрения вызывающей программы функцию можно представить как некий «черный ящик», у которого есть несколько входов и один выход. С точки зрения вызывающей программы неважно, каким образом производится обработка информации внутри функции. Для корректного использования функции достаточно знать лишь ее сигнатуру.
Вызов функции
Общий вид вызова функции
Фактический аргумент — это величина, которая присваивается формальному аргументу при вызове функции. Таким образом, формальный аргумент — это переменная в вызываемой функции, а фактический аргумент — это конкретное значение, присвоенное этой переменной вызывающей функцией. Фактический аргумент может быть константой, переменной или выражением. Если фактический аргумент представлен в виде выражения, то его значение сначала вычисляется, а затем передается в вызываемую функцию. Если в функцию требуется передать несколько значений, то они записываются через запятую. При этом формальные параметры заменяются значениями фактических параметров в порядке их следования в сигнатуре функции.
Возврат в вызывающую функцию
По окончании выполнения вызываемой функции осуществляется возврат значения в точку ее вызова. Это значение присваивается переменной, тип которой должен соответствовать типу возвращаемого значения функции. Функция может передать в вызывающую программу только одно значение. Для передачи возвращаемого значения в вызывающую функцию используется оператор return в одной из форм:
Действие оператора следующее: значение выражения, заключенного в скобки, вычисляется и передается в вызывающую функцию. Возвращаемое значение может использоваться в вызывающей программе как часть некоторого выражения.
Оператор return также завершает выполнение функции и передает управление следующему оператору в вызывающей функции. Оператор return не обязательно должен находиться в конце тела функции.
Пример : Посчитать сумму двух чисел.
В языке Си нельзя определять одну функцию внутри другой.
В языке Си нет требования, чтобы семантика функции обязательно предшествовало её вызову. Функции могут определяться как до вызывающей функции, так и после нее. Однако если семантика вызываемой функции описывается ниже ее вызова, необходимо до вызова функции определить прототип этой функции, содержащий:
Прототип необходим для того, чтобы компилятор мог осуществить проверку соответствия типов передаваемых фактических аргументов типам формальных аргументов. Имена формальных аргументов в прототипе функции могут отсутствовать.
Если в примере выше тело функции сложения чисел разместить после тела функции main, то код будет выглядеть следующим образом:
Рекурсивные функции
Рекурсия — вызов функции из самой функции.
Пример рекурсивной функции — функция вычисления факториала.
Результат выполнения
Более подробно рекурсивные функции рассмотрены в этой статье.
Математические функции
Основные математические функции стандартной библиотеки.
Функция | Описание |
int abs( int x) | Модуль целого числа x |
double acos( double x) | Арккосинус x |
double asin( double x) | Арксинус x |
double atan( double x) | Арктангенс x |
double cos( double x) | Косинус x |
double cosh( double x) | Косинус гиперболический x |
double exp( double x) | Экспонента x |
double fabs( double x) | Модуль вещественного числа |
double fmod( double x, double y) | Остаток от деления x/y |
double log( double x) | Натуральный логарифм x |
double log10( double x) | Десятичный логарифм x |
double pow( double x, double y) | x в степени y |
double sin( double x) | Синус x |
double sinh( double x) | Синус гиперболический x |
double sqrt( double x) | Квадратный корень x |
double tan( double x) | Тангенс x |
double tanh( double x) | Тангенс гиперболический x |
Особенности использования функций в языке C++ рассмотрены в этой статье.
Особенности вызова функций в С++
Не так давно у меня произошёл очередной разговор с коллегой на извечную тему: «по ссылке, или по значению». В результате возникла данная статья. В ней я хочу изложить результаты моего исследования по этой и смежным темам. Далее будут рассмотрены:
Осторожно! Статья содержит большое количество кода на C++ и ассемблере (Intel ASM с комментариями), а также множество таблиц с оценками производительности. Всё написанное актуально для x86-64 System V ABI, который используется во всех современных Unix операционных системах, к примеру, в Linux и macOS.
Содержание
Регистры в x86-64
Все данные хранятся в оперативной памяти. Для ускорения работы с ней используются многоуровневые кэши. Но для изменения данных, так или иначе, используются регистры (обсуждение в комментариях). Ниже дано очень краткое описание наиболее используемых регистров в архитектуре x86-64.
Передача параметров
В этом параграфе приведено несколько сокращённое и упрощённое описание алгоритма распределения аргументов по регистрам/стеку. Полное описание можно увидеть на странице 17 «System V ABI».
Введём несколько классов объектов:
Для унификации описания, типы __int128 и complex представляются как структуры из двух полей:
В начале каждый аргумент функции классифицируется:
После классификации, все 8 байтные куски (в одном куске может быть несколько полей структуры, или элементов массива) распределяются по регистрам:
Аргументы рассматриваются слева направо. Те аргументы, которым не хватило регистров, передаются через стек. Если какому-либо куску аргумента не хватило регистра, то весь аргумент передаётся через стек.
Возвращение значений производится следующим образом:
Сводная таблица с регистрами и их назначением, очень полезна при чтении ассемблера:
Регистр | Назначение |
---|---|
rax | Временный регистр, возврат первого (ret 1) INTEGER результата. |
rbx | Принадлежит вызывающей функции, не должен быть изменён на момент возврата. |
rcx | Передача четвёртого (4) INTEGER аргумента. |
rdx | Передача третьего (3) INTEGER аргумента, возврат второго (ret 2) INTEGER результата. |
rsp | Указатель на стек. |
rbp | Принадлежит вызывающей функции, не должен быть изменён на момент возврата. |
rsi | Передача второго (2) INTEGER аргумента. |
rdi | Передача первого (1) INTEGER аргумента. |
r8 | Передача пятого (5) INTEGER аргумента. |
r9 | Передача шестого (6) INTEGER аргумента. |
r10-r11 | Временные регистры. |
r12-r15 | Принадлежит вызывающей функции, не должны быть изменены на момент возврата. |
xmm0-xmm1 | Передача и возврат первого и второго SSE аргументов. |
xmm2-xmm7 | Передача с третьего по шестой SSE аргументов. |
xmm8-xmm15 | Временные регистры. |
Регистры, принадлежащее вызывающей функции, или не должны использоваться, или их значения должны быть куда-то сохранены, к примеру, на стек, а потом восстановлены.
Простые примеры
Рассмотрим что-нибудь простое.
Параметры передаются так:
Имя | Регистр | Имя | Регистр | Результат |
---|---|---|---|---|
a | rdi | d | rcx | xmm0 |
b | rsi | x | xmm0 | |
c | rdx | y | xmm1 |
Рассмотрим сгенерированный код подробнее.
Если в функцию передавать параметры простых типов, то нужно сильно постараться, чтобы они были переданы не через регистры.
Рассмотрим различные примеры агрегатов. Массивы можно рассматривать как структуры с несколькими полями.
Имя | Регистр | Имя | Регистр | Результат |
---|---|---|---|---|
s.a | xmm0 | s.b | xmm1 | xmm0 |
Казалось бы, ничто не мешает запихнуть сразу два double в один xmm регистр. Но увы, алгоритм распределения оперирует только восьмибайтными кусками.
Если добавить ещё одно double поле, то вся структура будет передана через стек, так как её размер превысит 128 байт.
Имя | Регистр | Имя | Регистр | Результат |
---|---|---|---|---|
s.a | rdi | s.b | rsi | rax |
Рассмотрим что-нибудь более интересное.
Имя | Регистр | Имя | Регистр | Результат |
---|---|---|---|---|
s1.a | xmm0 | s1.b | xmm0 | xmm0, xmm1 |
s1.c | xmm1 | s1.d | xmm1 | |
s2.a | xmm2 | s2.b | xmm2 | |
s2.c | xmm3 | s2.d | xmm3 |
В каждый xmm регистр запихиваются по два float поля.
Имя | Регистр | Имя | Регистр | Результат |
---|---|---|---|---|
s1.a | rdi | s1.b | rdi | rax, rdx |
s1.c | rsi | s1.d | rsi | |
s2.a | rdx | s2.b | rdx | |
s2.c | rcx | s2.d | rcx |
Внутри функции происходит битовая магия, но принцип примерно ясен. Каждая пара 32 битных чисел запаковывается в один 64 битный регистр. Возврат производится таким же образом.
Посмотрим, что будет, если начать смешивать типы полей, но так, чтобы в пределах 8 байтовых кусков они были одинакового класса.
Имя | Регистр | Имя | Регистр | Результат |
---|---|---|---|---|
s1.a | rdi | s1.b | rdi | rax, xmm0 |
s1.c | xmm0 | s1.d | xmm0 | |
s2.a | rsi | s2.b | rsi | |
s2.c | xmm1 | s2.d | xmm1 |
Но это не интересно, так как типы полей в каждом 8 байтном куске совпадают. Перемешаем поля.
Имя | Регистр | Имя | Регистр | Результат |
---|---|---|---|---|
s1.a | rdi | s1.b | rdi | rax, rdx |
s1.c | rsi | s1.d | rsi | |
s2.a | rdx | s2.b | rdx | |
s2.c | rcx | s2.d | rcx |
Тут можно видеть 6 операций сдвига для извлечения float полей и их запихивания в регистр с результатом. А также отсутствие каких-либо векторных операций. В общем, лучше не мешать типы полей в пределах 8 байтовых кусков структуры.
Передача по ссылке
Передача параметров по константной ссылке аналогично передаче указателя на объект. Если объект не помещается в регистрах, то он передаётся и возвращается через стек. Посмотрим, как это происходит. Для реалистичности рассмотрим структуру для трёхмерной точки.
Сравним код функций. Использоваться будут преимущественно новые xmm регистры, поэтому логика вполне понятна.
Теперь посмотрим на место вызова этих функций.
Посмотрим, что если у нас много полей, но структура всё-таки влезает в регистры. Тут начинается самое интересное.
Код для функции, принимающей аргументы по значению.
Имя | Регистр | Имя | Регистр | Результат |
---|---|---|---|---|
s1.d[1:8] | rdi | s1.d[8:16] | rsi | rax, rdx |
s2.d[1:8] | rdx | s2.d[8:16] | rcx |
Имя | Регистр | Имя | Регистр | Результат |
---|---|---|---|---|
s1 | rdi | s2 | rsi | rax, rdx |
О да! Это выглядит гораздо лучше. Мы можем загрузить сразу 16 однобайтовых элементов в xmm регистр и вызывать vpaddb которая их все сложит за одну операцию. После этого результат копируется в выходные регистры через стек. Можно подумать, что от этой последней операции можно избавиться, заменив первый аргумент на не константную ссылку.
Имя | Регистр | Имя | Регистр | Результат |
---|---|---|---|---|
s1 | rdi | s2 | rsi |
Кажется, что-то пошло не так. Это получилось из-за того, что, по умолчанию, компилятор очень осторожен, и предполагает, что программист может быть не в духе и напишет что-то вроде такого:
Что и даёт желаемый результат.
Сравнение производительности
Code | Cycles per iteration |
---|---|
St a, b; st(a, b); | 7.6 |
4 x foo no reuse | 121.9 |
4 x foo | 117.7 |
4 x fooR no reuse | 66.3 |
4 x fooR | 64.6 |
4 x fooR1 | 84.5 |
4 x fooR2 | 20.6 |
4 x foo inline | 51.9 |
4 x fooR inline | 30.5 |
4 x fooR1 inline | 8.8 |
4 x fooR2 inline | 8.8 |
Прозрачные указатели
Посмотрим, что будет, если добавить немного деструктора.
Как видно, загрузка, умножение (через сложение) и сохранение производится по одному полю за раз. Компилятор не очень хочет оптимизировать не POD типы. Версия функции с константной ссылкой Point3f scaleR(const Point3f&) даст идентичный код. Посмотрим на место вызова.
Если сделать деструктор NOINLINE, то всё будет гораздо запутаннее.
Если параметр p явно передавать по ссылке, то кода станет несколько меньше и будет произведено только два вызова деструктора.
Переиспользование стека
Посмотрим, как компилятор переиспользует регистры и стек между вызовами. Будут использоваться функции из параграфа про ссылки.
Видно, что в случае с передачей через регистры, кода значительно меньше. Если аргумент не помещается в регистры, то всё точно наоборот.
Видно, что при передаче аргументов, помещающихся в регистры, код значительно короче, если передавать по значению. Если же они не помещаются – то лучше передавать по ссылке. В этом случае компилятор может удалить лишние копирования и передавать сразу указатель на стек с результатом предыдущего вызова. Причём он не может сделать так же, если параметр передаётся по значению через стек, и ему приходится выполнять лишние копирования.
Сравнение производительности
Посмотрим, какое влияние всё это оказывает на быстродействие. Функции для получения точек были объявлены в отдельном файле, чтобы предотвратить оптимизации для константных данных. Напомню, что при возврате из функции и передаче по значению, Point3f будет передаваться через регистры, а Point3d – через стек.
Code | Cycles per iteration |
---|---|
auto r = pf(); | 6.7 |
auto r = scale(pf()); | 11.1 |
auto r = scaleR(pf()); | 12.6 |
auto r = scale(scale(pf())); | 18.2 |
auto r = scaleR(scaleR(pf())); | 18.3 |
auto r = scale(scale(scale(pf()))); | 16.8 |
auto r = scaleR(scaleR(scaleR(pf()))); | 20.2 |
auto r = pd(); | 7.3 |
auto r = scale(pd()); | 11.7 |
auto r = scaleR(pd()); | 11.0 |
auto r = scale(scale(pd())); | 16.9 |
auto r = scaleR(scaleR(pd())); | 14.1 |
auto r = scale(scale(scale(pd()))); | 21.2 |
auto r = scaleR(scaleR(scaleR(pd()))); | 17.2 |
Если функции пометить INLINE | 8.1 — 8.9 |
Что можно заметить:
Тип optional
будет совсем не эквивалентно
Несмотря на одинаковые размер и выравнивание, OptPoint1 будет передаваться через стек, а OptPoint2 – через регистры.
Видно, что в случае с простой структурой, компилятор смог соптимизировать код чуть лучше, но в остальном всё одинаково.
Виртуальные функции
Кратко рассмотрим, как у классов вызываются методы, в том числе виртуальные. Для реалистичности придумаем следующий пример. Допустим, мы делаем математическую библиотеку. Есть базовый класс функции, какие-то общие методы для работы с ним и наследники, реализующие, собственно, функции. В начале рассмотрим реализацию через наследование и виртуальные методы.
В результат получим что-то вроде такого.
Если сделать деструктор protected и не виртуальным, то весь код связанный с обработкой исключений исчезнет (строки 34-37). Если же оставить деструктор виртуальным и убрать NOINLINE, то компилятор встроит все вызовы функций и методов и запишет на стек готовый результат ( false в данном случае). Если пометить деструктор NOINLINE, то добавится просто куча кода с его вызовами. Для интереса переделаем пример с использованием шаблонов.
Как видно, код заметно компактнее даже с NOINLINE.
Сравнение производительности
В данном случае была будет проводится итерация по вектору с 1000 элементов типа Add и вызова isFixedPoint для каждого из них.
Что можно заметить:
Хвостовые вызовы
Немного оффтоп. Компилятор clang умеет оптимизировать хвостовую рекурсию. К примеру, возьмём функцию быстрого возведения в степень:
Как видно, нет ни одного рекурсивного вызова, лишь циклы. Код этой же функции, реализованной через цикл, будет примерно аналогичен. Причём рекурсивный даже будет на
Но я хотел рассмотреть особенности не рекурсивных вызовов функций непосредственно перед возвратом. Сравним три версии одной и той же функции по прибавлению единицы к аргументу.
Если пример немного усложнить и замерить производительность, то разница между скоростью выполнения функций будет в районе 10%.
Инициализация
В данном разделе я хочу рассмотреть особенности инициализации структур и массивов. Рассмотрим классы для 2D точки с различными состояниями по умолчанию:
Структура Point не инициализируется. ZeroPoint заполняется нулями. По стандарту IEEE 754-1985:
The number zero is represented specially: sign = 0 for positive zero, 1 for negative zero; biased exponent = 0; fraction = 0;
Один элемент
Просто выделяем место на стеке без какой-либо инициализации.
Обе строки дают идентичный результат.
То же самое, но в регистр загружаем значение из раздела с константными данными.
Небольшой массив
Здесь и далее будут использоваться следующие константы:
Рассмотрим простой массив на стеке.
Как и раньше – просто перемещение указателя на стек.
Как и в случае с одной точкой, но размер стека пропорционально больше.
Заметим явный вызов конструктора по умолчанию в третьей строке.
Память очищается кусками по 256 бит, или 2 точки.
Тут каждая точка инициализируется по отдельности.
Большой массив
Это было предсказуемо.
Тут уже используется цикл. В каждой итерации копируются сразу три элемента, так как число 321 делится на 3 без остатка. В регистрах rax, rcx хранятся адреса начала и конца массива.
Динамический массив
Как и в случае с массивом, инициализация происходит пачками через цикл. В случае с NanPoint листинг примерно аналогичен.
A вот при использовании ZeroPoint код заметно отличается.
Можно увидеть два цикла. Первый, LBB0_9 заполняет по одной точке до тех пор, пока количество оставшихся элементов не станет кратно 8. После чего идёт заполнение сразу 8 точек за итерацию.
Как и в прошлый раз, листинг с Point примерно такой же, а при ZeroPoint опять используется memset :
произведёт больше 250 строк ассемблера, что заметно больше случая с передачей размера непосредственно в конструктор. Но operator new вызывается только один раз, так как при создании пустого вектора он не используется.
Сравнение производительности
Теперь сравним скорость выполенения всех вышерассмотренных примеров.
Code | Cycles per iteration |
---|---|
Point p; | 4.5 |
ZeroPoint p; | 5.2 |
NanPoint p; | 4.5 |
array p; | 4.5 |
array p; | 6.7 |
array p; | 6.7 |
array p; | 4.5 |
array p; | 296.0 |
array p; | 391.0 |
array p<>; | 292.0 |
array p<>; | 657.0 |
vector p(smallSize); | 32.3 |
vector p(smallSize); | 33.8 |
vector p(smallSize); | 33.8 |
vector p(bigSize); | 323.0 |
vector p(bigSize); | 308.0 |
vector p(bigSize); | 281.0 |
vector p(smallUnknownSize); | 44.1 |
vector p(smallUnknownSize); | 37.6 |
vector p(bigUnknownSize); | 311.0 |
vector p(bigUnknownSize); | 315.0 |
vector p(bigUnknownSize); | 290.0 |
vector p; p.resize(bigUnknownSize); | 315.0 |