每个c ++成员函数是否隐含地将“this”作为输入?

时间:2017-01-15 23:50:45

标签: c++ performance this language-lawyer member-functions

当我们在c ++中为类创建一个成员函数时,它有一个隐式的额外参数,它是一个指向调用对象的指针 - 称为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_oneget_bar)都会将this作为隐式参数,即使其中只有一个实际使用它吗? 这样做似乎有点浪费。

注意:我理解正确的做法是让get_one()静态,答案可能取决于实施,但我只是好奇

4 个答案:

答案 0 :(得分:22)

  

两个函数(get_one和get_bar)都会将此作为隐式参数,即使只有onle get_bar使用它吗?

是(除非编译器对其进行优化,但这并不意味着您可以在没有有效对象的情况下调用该函数)。

  

这样做似乎有点浪费

那么,如果它没有使用任何会员数据,为什么会员呢?有时,正确的方法是使它成为同一名称空间中的自由函数。

答案 1 :(得分:6)

  

... c ++中的类,据我所知,它有一个隐含的额外参数,它是一个指向调用对象的指针

重要的是要注意C ++以对象的形式开始。

为此,this指针不是隐式存在于成员函数中的指针,而是成员函数,当编译出来时,需要一种方法来知道this指的是什么;因此,传入调用对象的隐式this指针的概念。

换句话说,让我们把你的C ++类变成C版:

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;
}

C

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;
}

编译和汇编C ++程序时,this指针被添加""对于错位的功能,以便知道"什么对象调用成员函数。

所以foo::get_one可能会被严重打击"相当于foo_get_one(foo *this)的C等价物,foo::get_bar可以被修改为foo_get_bar(foo *this)foo::get_foo(int)可能会被foo_get_foo(int, foo *this)等等。

  

两个函数(get_oneget_bar)都会将此作为隐式参数,即使只有一个get_bar使用它吗?这样做似乎有点浪费。

这是编译器的一个功能,如果绝对没有进行优化,启发式方法可能仍然会消除不需要调用对象的损坏函数中的this指针(以节省堆栈),但这是高度依赖于代码及其编译方式和系统。

更具体地说,如果函数是一个像foo::get_one那样简单的函数(只返回1),编译器可能只是将常量1放在代替调用的位置object->get_one()Get-Content 'hist4.txt' | ForEach-Object { $_.split(" ")[0]} | ForEach-Object { $rgbArray += $_ for( $i = 1; $i -le $rgbArray.length; $i++ ) { $rgbA0=$rgbArray[$i] $rgbA1=$rgbArray[$i + 1] //compare A0 and A1... } } ,无需任何引用/指针。

希望可以提供帮助。

答案 2 :(得分:3)

语义 this指针始终在成员函数中可用 - 作为另一个用户pointed out。也就是说,您可以稍后更改函数以使用它而不会出现问题(特别是,无需在其他翻译单元中重新编译调用代码)或者在virtual函数的情况下,可以在一个子类可以使用this,即使基本实现没有。

所以剩下的有趣问题是这会带来什么样的性能影响,如果有的话。 调用者和/或被调用者可能会有成本,并且内联时不会内联成本可能会有所不同。我们检查下面的所有排列:

内联

inlined 的情况下,编译器可以看到调用站点和函数实现 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调用只是为了使分支更加不同 - 否则编译器足够聪明,只能使用条件集/移动,所以你甚至不能将这两个函数的内联体分开。

所有gcciccclang内联两个调用,并生成与成员和非成员函数等效的代码,没有任何{{1的跟踪成员案例中的指针。让我们看看this代码,因为它是最干净的代码:

clang

这两个路径生成完全相同的4个指令序列,直到最终main: push rax test edi,edi je 400556 <main+0x16> # this is the member case mov edi,0x4005f4 call 400400 <puts@plt> mov eax,0x1 pop rcx ret # this is the non-member case mov edi,0x4005f6 call 400400 <puts@plt> mov eax,0x2 pop rcx ret - ret调用的两条指令,puts单条指令} mov1返回2的值,以及eax来清理堆栈 2 。因此,在任何一种情况下,实际调用只接受一条指令,并且根本没有pop rcx指针操作或传递。

脱节

在外线成本中,支持this指针实际上会有一些实际但通常很小的成本,至少在来电方面是这样。

我们使用类似的测试程序,但是成员函数声明为out-of-line并且内联这些函数已禁用 3

this

这个测试代码比上一个更简单,因为它不需要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 4 generates gcc的汇编(即函数的调用点):< / p>

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] 实现的 - 这是一种尾调用优化,因为它们是main中调用的最后一个函数,因此被调用函数的jmp实际返回到ret的来电者 - 但是这里会员功能的来电者需要支付额外的费用:

main

lea rdi,[rsp-0x18] 指针加载到堆栈中this,它接收第一个参数,对于C ++成员函数是rdi。所以有一个(小)额外费用。

功能主体

现在,当呼叫站点支付一些成本来传递(未使用的)this指针时,至少在这种情况下,实际的函数体仍然具有同等效率:

this

两者都由一个foo::get_one_member(): mov eax,0x1 ret get_one_global(): mov eax,0x2 ret 和一个mov组成。因此,函数本身可以简单地忽略ret值,因为它没有被使用。

这提出了这样的问题:一般来说这是否正确 - 不使用this的成员函数的函数体是否总是被编译为与等效的非成员函数一样有效?

简短的回答是 no - 至少对于在寄存器中传递参数的大多数现代ABI来说。 this指针在调用约定中占用一个参数寄存器,因此在编译成员函数时,您可以更快地获得一个参数的最大寄存器传递参数数。

以此函数为例,简单地将其六个this参数加在一起:

int

当使用SysV ABI在x86-64平台上编译为成员函数时,您必须在堆栈上传递成员函数的寄存器,从而产生code like this:< / p>

int add6(int a, int b, int c, int d, int e, int f) {
  return a + b + c + d + e + f;
}

注意从堆栈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 读取,这通常会在gcc 6 上添加一些延迟 5 和一条指令>与非成员版本相比,没有内存读取:

eax,DWORD PTR [rsp+0x8]

现在你通常 通常有一个函数的六个或更多参数(特别是非常短的,性能敏感的参数) - 但这至少表明即使在被调用者代码生成方面也是如此,这个隐藏的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 指针并不总是免费的。

另请注意,虽然示例使用了x86-64 codegen和SysV ABI,但相同的基本原则将适用于在寄存器中传递一些参数的任何ABI。

1 请注意,此优化仅适用于有效的非虚函数 - 因为只有这样,编译器才能知道实际的函数实现。

2 猜测它的用途是什么 - 这会撤消方法顶部的this,以便{ {1}}在返回时具有正确的值,但我不知道为什么push rax对首先需要在那里。其他编译器使用不同的策略,例如rsppush/pop

3 在实践中,你并没有真正禁用这样的内联,但内联失败只会因为方法在不同的编译单元中而发生。由于godbolt的工作方式,我无法做到这一点,因此禁用内联具有相同的效果。

4 奇怪的是,我无法让add rsp, 8停止使用属性sub rsp,8clang来内联任一函数。

5 实际上,由于最近写入的值的存储转发,通常比英特尔的4个周期的通常 L1命中延迟多几个周期。

6 原则上,至少在x86上,可以通过使用带有内存源操作数的noinline来消除单指令惩罚,而不是来自-fno-inline内存与后续注册add,实际上clangicc完全相同。我并不认为一种方法占主导地位 - mov方法与单独的add更能够将负载从关键路径移开 - 尽早启动它然后仅在最后使用它指令,而gcc方法在涉及movicc方法的关键路径上增加了1个周期似乎是最糟糕的 - 将所有加法串联在{{long}上的长依赖链上{1}}以内存读取结束。

答案 3 :(得分:-5)

如果您不使用this,那么您无法判断它是否可用。所以实际上没有区别。这就像询问落在无人居住的森林中的树是否发出声音一样。这实际上是一个毫无意义的问题。

我可以告诉你:如果你想在成员函数中使用this,你可以。您始终可以使用该选项。