Подтвердить что ты не робот

Использует ли каждая функция-член С++ `this` как ввод неявно?

Когда мы создаем функцию-член для класса в С++, у него есть неявный дополнительный аргумент, который является указателем на вызывающий объект, называемый this.

Это верно для любой функции, даже если она не использует указатель this. Например, учитывая класс

class foo
{
private:
    int bar;
public:
    int get_one()
    {
      return 1;  // Not using `this`
    }
    int get_bar()
    {
        return this->bar;  // Using `this`
    }
}

И обе функции (get_one и get_bar) принимают this как неявный параметр, хотя только один из них действительно использует его?
Похоже, что это немного от этого.

Примечание. Я понимаю, что правильная вещь должна состоять в том, чтобы сделать get_one() static, и что ответ может зависеть от реализации, но мне просто интересно.суб >

4b9b3361

Ответ 1

Будут ли обе функции (get_one и get_bar) воспринимать это как неявный параметр, хотя только он использует get_bar?

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

Кажется, что это пустая трата

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

Ответ 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, которая заканчивается чтением памяти.

Ответ 4

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

Я могу сказать вам следующее: если вы хотите использовать this в функции-члене, вы можете. Эта опция всегда доступна вам.