Когда мы создаем функцию-член для класса в С++, у него есть неявный дополнительный аргумент, который является указателем на вызывающий объект, называемый this
.
Ответ 2
... class в С++, как я понимаю, он имеет неявный дополнительный аргумент, который является указателем на вызывающий объект
Важно отметить, что С++ запущен как C с объектами.
Для этого указатель this
не является тем, который неявно присутствует в функции-члене, но вместо этого функция-член при компиляции нуждается в способе узнать, к чему относится this
; таким образом, понятие неявного указателя this
для передаваемого вызывающего объекта.
Иными словами, давайте возьмем ваш класс С++ и сделаем его версией C:
С++
class foo
{
private:
int bar;
public:
int get_one()
{
return 1;
}
int get_bar()
{
return this->bar;
}
int get_foo(int i)
{
return this->bar + i;
}
};
int main(int argc, char** argv)
{
foo f;
printf("%d\n", f.get_one());
printf("%d\n", f.get_bar());
printf("%d\n", f.get_foo(10));
return 0;
}
С
typedef struct foo
{
int bar;
} foo;
int foo_get_one(foo *this)
{
return 1;
}
int foo_get_bar(foo *this)
{
return this->bar;
}
int foo_get_foo(int i, foo *this)
{
return this->bar + i;
}
int main(int argc, char** argv)
{
foo f;
printf("%d\n", foo_get_one(&f));
printf("%d\n", foo_get_bar(&f));
printf("%d\n", foo_get_foo(10, &f));
return 0;
}
Когда программа С++ скомпилирована и собрана, указатель this
"добавляется" к искаженной функции, чтобы "знать", какой объект вызывает функцию-член.
Итак, foo::get_one
может быть "искалечен" до C-эквивалента foo_get_one(foo *this)
, foo::get_bar
может быть искажен до foo_get_bar(foo *this)
, а foo::get_foo(int)
может быть foo_get_foo(int, foo *this)
и т.д.
Используют ли обе функции (get_one
и get_bar
) как неявный параметр, хотя только один get_bar
использует его? Похоже, что это немного от этого.
Это функция компилятора, и если не было сделано никакой оптимизации, эвристика может по-прежнему исключать указатель this
в искаженной функции, где объект не нужно вызывать (для сохранения стека), но это сильно зависит по коду и тому, как он компилируется и какой системе.
В частности, если функция была такой же простой, как foo::get_one
(просто вернув 1
), скорее всего, компилятор может просто поместить константу 1
вместо вызова object->get_one()
, устраняя необходимо для любых ссылок/указателей.
Надеюсь, что это поможет.
Ответ 3
Семантически указатель this
всегда доступен в функции-члене, как указывал другой пользователь . То есть вы могли бы впоследствии изменить функцию, чтобы использовать ее без проблем (и, в частности, без необходимости перекомпилировать код вызова в других единицах перевода) или в случае функции virtual
переопределенная версия в подклассе может использовать this
, даже если базовая реализация не выполнялась.
Таким образом, оставшийся интересный вопрос заключается в том, какое влияние на производительность оказывает это, если таковое имеется. Возможно, стоимость звонящего и/или вызываемого абонента может быть иной, и стоимость может быть разной, если она включена, а не включена. Мы рассмотрим все перестановки ниже:
встраиваемой
В встроенном случае компилятор может видеть как сайт вызова, так и реализацию функции 1 и поэтому, по-видимому, не нужно следовать какому-либо конкретному соглашению о вызове, и поэтому стоимость скрытого this
должен уйти. Заметим также, что в этом случае нет никакого реального различия между кодом "вызываемого" и "вызываемым" кодом, поскольку они объединены при оптимизации вместе на сайте вызова.
Можно использовать следующий тестовый код:
#include <stdio.h>
class foo
{
private:
int bar;
public:
int get_one_member()
{
return 1; // Not using `this`
}
};
int get_one_global() {
return 2;
}
int main(int argc, char **) {
foo f = foo();
if(argc) {
puts("a");
return f.get_one_member();
} else {
puts("b");
return get_one_global();
}
}
Обратите внимание, что два вызова puts
находятся здесь, чтобы сделать ветки немного более разными - в противном случае компиляторы достаточно умны, чтобы просто использовать условный набор/перемещение, и поэтому вы даже не можете отделить отдельные тела двух функций.
Все gcc, icc и clang встроить два вызова и сгенерировать код, эквивалентный как для функции-члена, так и для нечлена, без какого-либо следа указателя this
в случай члена. Давайте посмотрим на код clang
, поскольку он самый чистый:
main:
push rax
test edi,edi
je 400556 <main+0x16>
# this is the member case
mov edi,0x4005f4
call 400400 <[email protected]>
mov eax,0x1
pop rcx
ret
# this is the non-member case
mov edi,0x4005f6
call 400400 <[email protected]>
mov eax,0x2
pop rcx
ret
Оба пути генерируют одну и ту же последовательность из четырех инструкций, ведущих к финальной ret
- две команды для вызова puts
, одну команду для mov
возвращаемое значение 1
или 2
в eax
и a pop rcx
для очистки стека 2. Таким образом, фактический вызов взял ровно одну инструкцию в любом случае, и не было никакого манипулирования указателем this
или передачи вообще.
Вне строки
В расходах за пределами сети поддержка указателя this
фактически будет иметь некоторые реальные, но, как правило, небольшие затраты, по крайней мере, на стороне вызывающего абонента.
Мы используем аналогичную тестовую программу, но с функциями-членами, объявленными вне очереди, и с инкрустацией этих функций отключены 3:
class foo
{
private:
int bar;
public:
int __attribute__ ((noinline)) get_one_member();
};
int foo::get_one_member()
{
return 1; // Not using `this`
}
int __attribute__ ((noinline)) get_one_global() {
return 2;
}
int main(int argc, char **) {
foo f = foo();
return argc ? f.get_one_member() :get_one_global();
}
Этот тестовый код несколько проще, чем последний, поскольку для выделения двух ветвей не требуется вызов puts
.
Вызов сайта
Посмотрите на сборку, что gcc
4генерирует для main
(т.е. на сайты вызовов для функций):
main:
test edi,edi
jne 400409 <main+0x9>
# the global branch
jmp 400530 <get_one_global()>
# the member branch
lea rdi,[rsp-0x18]
jmp 400520 <foo::get_one_member()>
nop WORD PTR cs:[rax+rax*1+0x0]
nop DWORD PTR [rax]
Здесь оба вызова функций фактически реализуются с помощью jmp
- это тип оптимизации хвостового вызова, так как они являются последними функциями, называемыми main, поэтому ret
для вызываемой функции фактически возвращается вызывающему абоненту main
- но здесь вызывающая функция-член оплачивает дополнительную цену:
lea rdi,[rsp-0x18]
Загрузите указатель this
в стек в rdi
, который получает первый аргумент, который является this
для функций-членов С++. Таким образом, есть (небольшая) дополнительная стоимость.
Тело функции
Теперь, когда call-сайт оплачивает некоторую стоимость, чтобы передать (не использованный) this
указатель, в этом случае, по крайней мере, фактические тела функций по-прежнему одинаково эффективны:
foo::get_one_member():
mov eax,0x1
ret
get_one_global():
mov eax,0x2
ret
Оба состоят из одного mov
и a ret
. Таким образом, сама функция может просто игнорировать значение this
, поскольку оно не используется.
Возникает вопрос, действительно ли это в действительности: будет ли тело функции функции-члена, которая не использует this
, всегда скомпилироваться так же эффективно, как эквивалентная функция, не являющаяся членом?
Короткий ответ нет - по крайней мере для большинства современных ABI, которые передают аргументы в регистрах. Указатель this
принимает регистр параметров в соглашении о вызове, поэтому при компиляции функции-члена вы нажимаете максимальное количество аргументов, переданных регистром, на один параметр.
Возьмем, к примеру, эту функцию, которая просто добавляет шесть параметров int
вместе:
int add6(int a, int b, int c, int d, int e, int f) {
return a + b + c + d + e + f;
}
При компиляции как функции-члена на платформе x86-64 с помощью SysV ABI, вам нужно будет передать регистр в стек для функции-члена, в результате получится код вроде этого:
foo::add6_member(int, int, int, int, int, int):
add esi,edx
mov eax,DWORD PTR [rsp+0x8]
add ecx,esi
add ecx,r8d
add ecx,r9d
add eax,ecx
ret
Обратите внимание на чтение из стека eax,DWORD PTR [rsp+0x8]
, которое обычно добавляет несколько циклов латентности 5 и одну инструкцию по gcc 6 по сравнению с версией, не являющейся членом, которая памяти не читается:
add6_nonmember(int, int, int, int, int, int):
add edi,esi
add edx,edi
add ecx,edx
add ecx,r8d
lea eax,[rcx+r9*1]
ret
Теперь у вас обычно не будет шести или более аргументов функции (особенно очень коротких, чувствительных к производительности) - но это, по крайней мере, показывает, что даже на стороне генерации кода вызываемого абонента этот скрытый указатель this
isn ' t всегда бесплатно.
Отметим также, что, хотя в примерах используются x86-64 codegen и SysV ABI, те же основные принципы применимы к любому ABI, который передает некоторые аргументы в регистры.
1 Обратите внимание, что эта оптимизация применима только к эффективным не виртуальным функциям - так как только тогда компилятор может узнать о фактической реализации функции.
2 Я предполагаю, что для этого - это отменяет push rax
в верхней части метода, так что rsp
имеет правильное значение при возврате, но я не знаю, почему push/pop
пара должна быть там, в первую очередь. Другие компиляторы используют разные стратегии, такие как add rsp, 8
и sub rsp,8
.
3 На практике вы не собираетесь отключать подобную процедуру, но несобственная ошибка будет происходить только потому, что методы находятся в разных единицах компиляции. Из-за того, как работает godbolt, я не могу этого точно сделать, поэтому отключение вставки имеет тот же эффект.
4 Как ни странно, я не мог получить clang
, чтобы остановить вложение любой функции, либо с атрибутом noinline
, либо с помощью -fno-inline
.
5 Фактически, часто несколько циклов больше, чем обычная L1-hit латентность 4 циклов на Intel, из-за хранения-пересылки недавно написанного значения.
6 В принципе, по крайней мере на x86, однократное наказание может быть устранено с помощью add
с операндом источника памяти, а не с mov
из памяти с последующим reg-reg add
и фактически clang и icc сделайте именно это. Я не думаю, что один подход доминирует, хотя подход gcc
с отдельным mov
лучше переносит нагрузку с критического пути - инициирует его раньше, а затем использует его только в последней инструкции, тогда как icc
добавляет 1 цикл к критическому пути с использованием mov
, а подход clang
кажется худшим из всех - наложение всех добавок вместе на цепочку длинных зависимостей на eax
, которая заканчивается чтением памяти.