c++编译器是否优化a = b + c
Does c++ compiler optimize a = b + c
谁给我投了反对票,介意解释一下原因吗?我认为这是一个合理的问题,所有答案都非常有帮助。
理论上,当我做MyClass a = b + c
时,应该先调用const MyClass operator+
,returning一个const MyClass
对象,然后调用赋值运算符创建对象a。
当 returning 一个对象并调用赋值运算符时,我似乎会复制两次东西。这是在编译器中优化的吗?如果是,如何?如果涉及铸造,这似乎更棘手。
假设我们正在谈论 g++,它几乎是 c++ 编译器的黄金标准。 [编辑:好的,让我们说最常用的]
[编辑:] 哇,我没想到在 return-by-value 中使用 const 会受到批评。我认为在 return-by-value 非内置类型时使用 const 是令人鼓舞的吗?我记得在哪里见过它。
复制初始化不使用赋值运算符,它使用复制或移动构造函数。由于您的运算符愚蠢地 return 是一个 const
对象,移动是不可能的,因此它将使用复制构造函数。
但是,从临时对象初始化对象是允许复制省略的情况之一,因此任何体面的编译器都应该这样做,直接将a
初始化为return 值而不是创建临时值。
大多数编译器会使用copy-elision来优化它。通过调用 MyClass::operator+
创建的临时对象将直接构造到 a
而不是调用复制构造函数。
另请注意,MyClass a = ...
不调用赋值运算符,它调用复制构造函数。这称为复制初始化。
查看 here 了解有关复制省略的更多信息。
§12.8/31 中的标准描述了一种称为复制省略的优化:
This elision of copy/move operations, called copy elision, is
permitted in the following circumstances (which may be combined to
eliminate multiple copies):
in a return
statement in a function with a class return type, when the expression is the name of a
non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-
unqualified type as the function return type, the copy/move operation can be omitted by constructing
the automatic object directly into the function’s return value
when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same
cv-unqualified type, the copy/move operation can be omitted by
constructing the temporary object directly into the target of the
omitted copy/move
因此在行
MyClass a = b + c;
operator+
返回的临时对象直接构造成a
,没有出现不必要的copies/moves,甚至在return
operator+
中的语句。演示:
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;
}
任何像样的编译器的输出:
Ctor!
Ctor!
Ctor!
* Dtor!
* Dtor!
* Dtor!
为了更直接地了解您可以期待什么,让我们从一个简单的 class 开始:
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++ 我们得到这个:
[ 正常 "stuff" 设置进入 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 对象并使用构造和 return 的第三个 Integer 对象的重载运算符添加了它们,编译器还是识破了我们所有的诡计,并且 "realized"我们正在做的只是使用 atoi
读取几个 int
,将它们加在一起,然后打印出我们得到的 int
结果。
看到之后,它完全消除了函数调用并且不调用或 return 任何东西——它只是读取两个整数,将它们相加,然后打印出结果。
使用 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
它稍微重新安排了代码,但最终做的事情几乎是一样的——我们 Integer
class 的所有痕迹都消失了。
让我们将其与根本不使用 class 的结果进行比较:
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<<
除了显示原始文件行号的注释外,代码与我们使用 class.
得到的代码完全相同
我不会浪费 space 来复制粘贴 g++ 的结果;无论我们是否使用 class 和重载运算符进行加法运算,它都会生成相同的代码。
谁给我投了反对票,介意解释一下原因吗?我认为这是一个合理的问题,所有答案都非常有帮助。
理论上,当我做MyClass a = b + c
时,应该先调用const MyClass operator+
,returning一个const MyClass
对象,然后调用赋值运算符创建对象a。
当 returning 一个对象并调用赋值运算符时,我似乎会复制两次东西。这是在编译器中优化的吗?如果是,如何?如果涉及铸造,这似乎更棘手。
假设我们正在谈论 g++,它几乎是 c++ 编译器的黄金标准。 [编辑:好的,让我们说最常用的]
[编辑:] 哇,我没想到在 return-by-value 中使用 const 会受到批评。我认为在 return-by-value 非内置类型时使用 const 是令人鼓舞的吗?我记得在哪里见过它。
复制初始化不使用赋值运算符,它使用复制或移动构造函数。由于您的运算符愚蠢地 return 是一个 const
对象,移动是不可能的,因此它将使用复制构造函数。
但是,从临时对象初始化对象是允许复制省略的情况之一,因此任何体面的编译器都应该这样做,直接将a
初始化为return 值而不是创建临时值。
大多数编译器会使用copy-elision来优化它。通过调用 MyClass::operator+
创建的临时对象将直接构造到 a
而不是调用复制构造函数。
另请注意,MyClass a = ...
不调用赋值运算符,它调用复制构造函数。这称为复制初始化。
查看 here 了解有关复制省略的更多信息。
§12.8/31 中的标准描述了一种称为复制省略的优化:
This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):
in a
return
statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv- unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return valuewhen a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move
因此在行
MyClass a = b + c;
operator+
返回的临时对象直接构造成a
,没有出现不必要的copies/moves,甚至在return
operator+
中的语句。演示:
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;
}
任何像样的编译器的输出:
Ctor!
Ctor!
Ctor!
* Dtor!
* Dtor!
* Dtor!
为了更直接地了解您可以期待什么,让我们从一个简单的 class 开始:
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++ 我们得到这个:
[ 正常 "stuff" 设置进入 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 对象并使用构造和 return 的第三个 Integer 对象的重载运算符添加了它们,编译器还是识破了我们所有的诡计,并且 "realized"我们正在做的只是使用 atoi
读取几个 int
,将它们加在一起,然后打印出我们得到的 int
结果。
看到之后,它完全消除了函数调用并且不调用或 return 任何东西——它只是读取两个整数,将它们相加,然后打印出结果。
使用 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
它稍微重新安排了代码,但最终做的事情几乎是一样的——我们 Integer
class 的所有痕迹都消失了。
让我们将其与根本不使用 class 的结果进行比较:
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<<
除了显示原始文件行号的注释外,代码与我们使用 class.
得到的代码完全相同我不会浪费 space 来复制粘贴 g++ 的结果;无论我们是否使用 class 和重载运算符进行加法运算,它都会生成相同的代码。