【问题标题】:Does c++ compiler optimize a = b + cc++ 编译器是否优化 a = b + c
【发布时间】:2015-06-16 06:13:59
【问题描述】:

谁对我投了反对票,介意解释一下原因吗?我认为这是一个合理的问题,所有答案都非常有帮助。

理论上,当我做MyClass a = b + c时,应该先调用const MyClass operator+,返回一个const MyClass对象,然后调用赋值运算符创建对象a。

在返回一个对象并调用赋值运算符时,我似乎会复制两次。这是在编译器中优化的吗?如果是,如何?如果涉及铸造,这似乎更棘手。

假设我们谈论的是 g++,它几乎是 c++ 编译器的黄金标准。 [编辑:好的,我们说最常用的]

[编辑:] 哇,我没想到在按值返回中使用 const 会受到批评。我认为在非内置类型的按值返回时使用 const 是被鼓励的吗?我记得在某个地方看到过。

【问题讨论】:

  • 当然这取决于您使用的编译器。
  • 返回const MyClass 通常是个坏主意,因为这会破坏移动语义。
  • 我说这也取决于operator+本身以及是否可以内联。
  • 没有赋值运算符,无论是理论上还是其他方面。
  • 黄金标准可以说是 Clang。

标签: c++ operator-overloading compiler-optimization


【解决方案1】:

复制初始化不使用赋值运算符,它使用复制或移动构造函数。由于您的操作员愚蠢地返回了一个const 对象,因此无法移动,因此它将使用复制构造函数。

但是,从临时对象初始化对象是允许复制省略的情况之一,因此任何体面的编译器都应该这样做,将a直接初始化为返回值,而不是创建一个临时的。

【讨论】:

  • 曾经有一个论点,对于相同类型的变量和初始化表达式,它必须进行复制省略,但我不记得了。啊。
  • 您好,感谢您的评论。我做了一些编辑。我没想到在按值返回中使用 const 会受到批评。我认为在非内置类型的按值返回时使用 const 是被鼓励的吗?我记得在某处见过它
  • @CodeNoob:有些人曾经建议过它(这样像a+b=c 这样的奇怪代码将无法编译,而不是给出奇怪的行为),但现在移动语义使它成为一个坏主意,因为你不能从一个常量对象中移动。
【解决方案2】:

大多数编译器会使用 copy-elision 对此进行优化。调用MyClass::operator+创建的临时对象将直接构造成a,而不是调用复制构造函数。

还要注意MyClass a = ... 不调用赋值运算符,它调用复制构造函数。这称为复制初始化。

查看here 了解有关复制省略的更多信息。

【讨论】:

  • 反对票有什么用?我说错了吗?
【解决方案3】:

有一种称为复制省略的优化,在 §12.8/31 中的标准中有描述:

这种复制/移动操作的省略,称为复制省略,是 在以下情况下允许(可以结合到 消除多个副本):

  • 在具有类返回类型的函数中的 return 语句中,当表达式是 具有相同 cv- 的非易失性自动对象(函数或 catch 子句参数除外) 非限定类型作为函数返回类型,可以通过构造省略复制/移动操作 自动对象直接转化为函数的返回值

  • 当尚未绑定到引用 (12.2) 的临时类对象将被复制/移动到具有相同的类对象时 cv-unqualified 类型,复制/移动操作可以省略 将临时对象直接构造到 省略复制/移动

因此在行中

MyClass a = b + c;

operator+ 返回的临时对象直接构造成a,不会发生不必要的复制/移动,即使在operator+return 语句中也不会发生。一个示范:

struct MyClass
{
    int i;

    MyClass operator+( MyClass const& m ) {
        MyClass r = m.i + i;
        return r;
    }

    MyClass(int i) : i(i) {std::cout << "Ctor!\n";}
    // Move ctor not implicitly declared, see §12.8/9
    MyClass(MyClass const&) {std::cout << "Copy-Ctor!\n";}
    ~MyClass() {std::cout << "* Dtor!\n";}
};

int main() {
    MyClass c{7}, b{3},
            a = b + c;
}

任何体面的编译器的输出:

克托!
克托!
克托!
* Dtor!
* Dtor!
* Dtor!

Live on Coliru

【讨论】:

    【解决方案4】:

    为了更直接地了解您可以期待什么,让我们从这样一个简单的类开始:

    class Integer {
        int a;
        public:
        Integer(int a) : a(a) {}
    
        friend Integer operator+(Integer a, Integer b) {
            return Integer(a.a + b.a);
        }
    
        friend std::ostream &operator<<(std::ostream &os, Integer const &i) {
            return os << i.a;
        }
    };
    

    为了演示,让我们添加一个main,它从外界读取一些数据,创建几个 Integer 对象,然后打印出添加它们的结果。输入和输出将来自外部世界,因此编译器不能花哨地把所有东西都优化出来。

    int main(int argc, char **argv) {
        Integer a(atoi(argv[1])), b(atoi(argv[2]));
        Integer c = a + b;    // Line 20
        std::cout << c;
    }
    

    注意那里的line 20——它在下面变得很重要。

    现在,让我们编译它,看看编译器会生成什么代码。使用 VC++ 我们得到这个:

    [设置main的条目的普通“东西”已省略]

    ; Line 19
        mov rcx, QWORD PTR [rdx+8]
        mov rdi, rdx
        call    atoi
        mov rcx, QWORD PTR [rdi+16]
        mov ebx, eax
        call    atoi
    ; Line 20
        lea edx, DWORD PTR [rax+rbx]
    ; Line 21
        call    ??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@H@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<
    

    因此,即使我们创建了两个 Integer 对象并使用构造并返回第三个 Integer 对象的重载运算符添加它们,编译器已经看穿了我们所有的诡计,并“意识到”我们所做的只是使用atoi读取几个ints,将它们加在一起,然后打印出我们得到的int

    看到这一点,它完全消除了函数调用,不调用或返回任何东西——它只是读取两个整数,将它们相加,然后打印出结果。

    使用 gcc 的结果几乎相同:

    movq    8(%rbx), %rcx
    call    atoi                    ; <--- get first item
    movq    16(%rbx), %rcx
    movl    %eax, %esi
    call    atoi                    ; <--- get second item
    movq    .refptr._ZSt4cout(%rip), %rcx
    leal    (%rsi,%rax), %edx       ; <--- the addition
    call    _ZNSolsEi               ; <--- print result
    

    它稍微重新排列了代码,但ultimate 做了几乎相同的事情——我们Integer 类的所有痕迹都消失了。

    让我们将其与完全不使用类的情况进行比较:

    int main(int argc, char **argv) {
        int a = atoi(argv[1]);
        int b = atoi(argv[2]);
        int c = a + b;
        std::cout << c;
    }
    

    使用 VC++,这会产生以下结果:

    ; Line 5
        mov rcx, QWORD PTR [rdx+8]
        mov rdi, rdx
        call    atoi
    ; Line 6
        mov rcx, QWORD PTR [rdi+16]
        mov ebx, eax
        call    atoi
    ; Line 7
        lea edx, DWORD PTR [rbx+rax]
    ; Line 8
        call    ??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@H@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<
    

    除了显示原始文件中行号的 cmets 之外,代码完全与我们使用该类得到的相同。

    我不会浪费空间来复制和粘贴 g++ 的结果;无论我们是否使用我们的类和重载运算符进行加法,它也会产生相同的代码。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-18
      • 2011-08-01
      • 2014-02-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多