为什么使用默认构造函数“{}”而不是“= default”会有性能差异?
Why is there performance variation using default constructor "{}" instead of "= default"?
我最近注意到我遇到了性能问题,因为我声明了一个默认构造函数,例如:
Foo() = default;
而不是
Foo() {}
(仅供参考,我需要显式声明它,因为我还有一个可变参数构造函数,否则会覆盖默认构造函数)
这对我来说似乎很奇怪,因为我认为这两行代码是相同的(好吧,只要默认构造函数是可能的。如果默认构造函数是不可能的,第二行代码将产生一个错误,第一个会隐式删除默认构造函数。'不是我的情况!)。
好的,所以我做了一个小测试器,结果因编译器的不同而有很大差异,但在某些设置下,我得到一致的结果,一个比另一个更快:
#include <chrono>
template <typename T>
double TimeDefaultConstructor (int n_iterations)
{
auto start_time = std::chrono::system_clock::now();
for (int i = 0; i < n_iterations; ++i)
T t;
auto end_time = std::chrono::system_clock::now();
std::chrono::duration<double> elapsed_seconds = end_time - start_time;
return elapsed_seconds.count();
}
template <typename T, typename S>
double CompareDefaultConstructors (int n_comparisons, int n_iterations)
{
int n_comparisons_with_T_faster = 0;
for (int i = 0; i < n_comparisons; ++i)
{
double time_for_T = TimeDefaultConstructor<T>(n_iterations);
double time_for_S = TimeDefaultConstructor<S>(n_iterations);
if (time_for_T < time_for_S)
++n_comparisons_with_T_faster;
}
return (double) n_comparisons_with_T_faster / n_comparisons;
}
#include <vector>
template <typename T>
struct Foo
{
std::vector<T> data_;
Foo() = default;
};
template <typename T>
struct Bar
{
std::vector<T> data_;
Bar() {};
};
#include <iostream>
int main ()
{
int n_comparisons = 10000;
int n_iterations = 10000;
typedef int T;
double result = CompareDefaultConstructors<Foo<T>,Bar<T>> (n_comparisons, n_iterations);
std::cout << "With " << n_comparisons << " comparisons of " << n_iterations
<< " iterations of the default constructor, Foo<" << typeid(T).name() << "> was faster than Bar<" << typeid(T).name() << "> "
<< result*100 << "% of the time" << std::endl;
std::cout << "swapping orientation:" << std::endl;
result = CompareDefaultConstructors<Bar<T>,Foo<T>> (n_comparisons, n_iterations);
std::cout << "With " << n_comparisons << " comparisons of " << n_iterations
<< " iterations of the default constructor, Bar<" << typeid(T).name() << "> was faster than Foo<" << typeid(T).name() << "> "
<< result*100 << "% of the time" << std::endl;
return 0;
}
将上面的程序与 g++ -std=c++11
一起使用,我始终得到类似于:
的输出
With 10000 comparisons of 10000 iterations of the
default constructor, Foo was faster than Bar 4.69% of the time
swapping orientation:
With 10000 comparisons of 10000 iterations of the
default constructor, Bar was faster than Foo 96.23% of the
time
更改编译器设置似乎会改变结果,有时甚至会完全翻转。但我不明白的是为什么它很重要?
Foo() = default;
是一个 普通 构造函数。
Foo() {}
是用户定义的构造函数,根据定义,用户定义的构造函数永远不会微不足道,即使它们为空也是如此。
另请参阅:Trivial default constructor and std::is_trivial。
预计当启用编译器优化时,普通构造函数可能比用户提供的构造函数更快。
Foo() = default;
和Foo() {};
是不同的。前者是普通的默认构造函数,而后者是默认构造函数的自定义版本,除了默认的东西外什么都不做。
这可以通过 type_traits 观察到。这样的更改可能会影响 allocation/construction 在模板函数解决方案中选择的例程,从而导致使用完全不同的代码。
虽然这对于默认构造函数应该无关紧要 - 对于副本 constructor/assignment 它可能会发生很大变化。所以 = default
是首选。
该基准测试没有衡量它应该衡量的内容。将 Bar() {};
替换为 Bar() = default;
使 Foo
和 Bar
相同,您将得到相同的结果:
With 10000 comparisons of 10000 iterations of the default constructor, Foo was faster than Bar 69.89% of the time
swapping orientation:
With 10000 comparisons of 10000 iterations of the default constructor, Bar was faster than Foo 29.9% of the time
这是一个生动的演示,表明您衡量的不是构造函数,而是其他东西。
当您启用 -O1
优化时,T t;
的 for
循环退化为 1:
test ebx, ebx
jle .L3
mov eax, 0
.L4:
add eax, 1
cmp ebx, eax
jne .L4
.L3:
Foo
和 Bar
。也就是说,进入一个简单的 for (int i = 0; i < n_iterations; ++i);
循环。
当您启用 -O2
或 -O3
时,它会被完全优化。
如果不进行优化 (-O0
),您将得到以下程序集:
mov DWORD PTR [rbp-4], 0
.L35:
mov eax, DWORD PTR [rbp-4]
cmp eax, DWORD PTR [rbp-68]
jge .L34
lea rax, [rbp-64]
mov rdi, rax
call Foo<int>::Foo()
lea rax, [rbp-64]
mov rdi, rax
call Foo<int>::~Foo()
add DWORD PTR [rbp-4], 1
jmp .L35
.L34:
Bar
也一样,Foo
替换为 Bar
。
现在让我们来看看构造函数:
Foo<int>::Foo()
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call std::vector<int, std::allocator<int> >::vector()
nop
leave
ret
和
Bar<int>::Bar()
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call std::vector<int, std::allocator<int> >::vector()
nop
leave
ret
如您所见,它们也是相同的。
1 海湾合作委员会 8.3
我怀疑你认为你看到的速度差异主要是时机不佳的副产品,而不是真实的。
为了查看生成的结果,我稍微简化了您的代码,仅保留以下内容:
#include <vector>
template <typename T>
struct Foo
{
std::vector<T> data_;
Foo() = default;
};
template <typename T>
struct Bar
{
std::vector<T> data_;
Bar() {};
};
int main() {
Foo<int> f;
Bar<int> b;
}
然后我把那个 on Godbolt 放在一起,以便于查看生成的代码。
gcc 9.2 似乎为两个 ctors 生成相同的代码,在两种情况下看起来都是这样的:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call std::vector<int, std::allocator<int> >::vector() [complete object constructor]
nop
leave
ret
Clang 生成的代码略有不同,但(同样)两者相同 类:
push rbp
mov rbp, rsp
sub rsp, 16
mov qword ptr [rbp - 8], rdi
mov rdi, qword ptr [rbp - 8]
call std::vector<int, std::allocator<int> >::vector() [base object constructor]
add rsp, 16
pop rbp
ret
英特尔 icc 几乎相同,为两者生成此代码 类:
push rbp #8.5
mov rbp, rsp #8.5
sub rsp, 16 #8.5
mov QWORD PTR [-16+rbp], rdi #8.5
mov rax, QWORD PTR [-16+rbp] #8.5
mov rdi, rax #8.5
call std::vector<int, std::allocator<int> >::vector() [complete object constructor] #8.5
leave #8.5
ret
虽然我同意其他人的观点,即在禁用优化的情况下查看性能收效甚微,但在这种情况下,似乎即使禁用优化也不足以(至少对于这三个编译器)获得不同的代码来构造对象两个 类。我想如果有一些编译器 and/or 优化设置会产生不同的结果,我不会感到非常惊讶,但恐怕我还不够雄心勃勃,无法花更多时间寻找它。
我最近注意到我遇到了性能问题,因为我声明了一个默认构造函数,例如:
Foo() = default;
而不是
Foo() {}
(仅供参考,我需要显式声明它,因为我还有一个可变参数构造函数,否则会覆盖默认构造函数)
这对我来说似乎很奇怪,因为我认为这两行代码是相同的(好吧,只要默认构造函数是可能的。如果默认构造函数是不可能的,第二行代码将产生一个错误,第一个会隐式删除默认构造函数。'不是我的情况!)。
好的,所以我做了一个小测试器,结果因编译器的不同而有很大差异,但在某些设置下,我得到一致的结果,一个比另一个更快:
#include <chrono>
template <typename T>
double TimeDefaultConstructor (int n_iterations)
{
auto start_time = std::chrono::system_clock::now();
for (int i = 0; i < n_iterations; ++i)
T t;
auto end_time = std::chrono::system_clock::now();
std::chrono::duration<double> elapsed_seconds = end_time - start_time;
return elapsed_seconds.count();
}
template <typename T, typename S>
double CompareDefaultConstructors (int n_comparisons, int n_iterations)
{
int n_comparisons_with_T_faster = 0;
for (int i = 0; i < n_comparisons; ++i)
{
double time_for_T = TimeDefaultConstructor<T>(n_iterations);
double time_for_S = TimeDefaultConstructor<S>(n_iterations);
if (time_for_T < time_for_S)
++n_comparisons_with_T_faster;
}
return (double) n_comparisons_with_T_faster / n_comparisons;
}
#include <vector>
template <typename T>
struct Foo
{
std::vector<T> data_;
Foo() = default;
};
template <typename T>
struct Bar
{
std::vector<T> data_;
Bar() {};
};
#include <iostream>
int main ()
{
int n_comparisons = 10000;
int n_iterations = 10000;
typedef int T;
double result = CompareDefaultConstructors<Foo<T>,Bar<T>> (n_comparisons, n_iterations);
std::cout << "With " << n_comparisons << " comparisons of " << n_iterations
<< " iterations of the default constructor, Foo<" << typeid(T).name() << "> was faster than Bar<" << typeid(T).name() << "> "
<< result*100 << "% of the time" << std::endl;
std::cout << "swapping orientation:" << std::endl;
result = CompareDefaultConstructors<Bar<T>,Foo<T>> (n_comparisons, n_iterations);
std::cout << "With " << n_comparisons << " comparisons of " << n_iterations
<< " iterations of the default constructor, Bar<" << typeid(T).name() << "> was faster than Foo<" << typeid(T).name() << "> "
<< result*100 << "% of the time" << std::endl;
return 0;
}
将上面的程序与 g++ -std=c++11
一起使用,我始终得到类似于:
With 10000 comparisons of 10000 iterations of the default constructor, Foo was faster than Bar 4.69% of the time swapping orientation: With 10000 comparisons of 10000 iterations of the default constructor, Bar was faster than Foo 96.23% of the time
更改编译器设置似乎会改变结果,有时甚至会完全翻转。但我不明白的是为什么它很重要?
Foo() = default;
是一个 普通 构造函数。
Foo() {}
是用户定义的构造函数,根据定义,用户定义的构造函数永远不会微不足道,即使它们为空也是如此。
另请参阅:Trivial default constructor and std::is_trivial。
预计当启用编译器优化时,普通构造函数可能比用户提供的构造函数更快。
Foo() = default;
和Foo() {};
是不同的。前者是普通的默认构造函数,而后者是默认构造函数的自定义版本,除了默认的东西外什么都不做。
这可以通过 type_traits 观察到。这样的更改可能会影响 allocation/construction 在模板函数解决方案中选择的例程,从而导致使用完全不同的代码。
虽然这对于默认构造函数应该无关紧要 - 对于副本 constructor/assignment 它可能会发生很大变化。所以 = default
是首选。
该基准测试没有衡量它应该衡量的内容。将 Bar() {};
替换为 Bar() = default;
使 Foo
和 Bar
相同,您将得到相同的结果:
With 10000 comparisons of 10000 iterations of the default constructor, Foo was faster than Bar 69.89% of the time swapping orientation: With 10000 comparisons of 10000 iterations of the default constructor, Bar was faster than Foo 29.9% of the time
这是一个生动的演示,表明您衡量的不是构造函数,而是其他东西。
当您启用 -O1
优化时,T t;
的 for
循环退化为 1:
test ebx, ebx
jle .L3
mov eax, 0
.L4:
add eax, 1
cmp ebx, eax
jne .L4
.L3:
Foo
和 Bar
。也就是说,进入一个简单的 for (int i = 0; i < n_iterations; ++i);
循环。
当您启用 -O2
或 -O3
时,它会被完全优化。
如果不进行优化 (-O0
),您将得到以下程序集:
mov DWORD PTR [rbp-4], 0
.L35:
mov eax, DWORD PTR [rbp-4]
cmp eax, DWORD PTR [rbp-68]
jge .L34
lea rax, [rbp-64]
mov rdi, rax
call Foo<int>::Foo()
lea rax, [rbp-64]
mov rdi, rax
call Foo<int>::~Foo()
add DWORD PTR [rbp-4], 1
jmp .L35
.L34:
Bar
也一样,Foo
替换为 Bar
。
现在让我们来看看构造函数:
Foo<int>::Foo()
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call std::vector<int, std::allocator<int> >::vector()
nop
leave
ret
和
Bar<int>::Bar()
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call std::vector<int, std::allocator<int> >::vector()
nop
leave
ret
如您所见,它们也是相同的。
1 海湾合作委员会 8.3
我怀疑你认为你看到的速度差异主要是时机不佳的副产品,而不是真实的。
为了查看生成的结果,我稍微简化了您的代码,仅保留以下内容:
#include <vector>
template <typename T>
struct Foo
{
std::vector<T> data_;
Foo() = default;
};
template <typename T>
struct Bar
{
std::vector<T> data_;
Bar() {};
};
int main() {
Foo<int> f;
Bar<int> b;
}
然后我把那个 on Godbolt 放在一起,以便于查看生成的代码。
gcc 9.2 似乎为两个 ctors 生成相同的代码,在两种情况下看起来都是这样的:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call std::vector<int, std::allocator<int> >::vector() [complete object constructor]
nop
leave
ret
Clang 生成的代码略有不同,但(同样)两者相同 类:
push rbp
mov rbp, rsp
sub rsp, 16
mov qword ptr [rbp - 8], rdi
mov rdi, qword ptr [rbp - 8]
call std::vector<int, std::allocator<int> >::vector() [base object constructor]
add rsp, 16
pop rbp
ret
英特尔 icc 几乎相同,为两者生成此代码 类:
push rbp #8.5
mov rbp, rsp #8.5
sub rsp, 16 #8.5
mov QWORD PTR [-16+rbp], rdi #8.5
mov rax, QWORD PTR [-16+rbp] #8.5
mov rdi, rax #8.5
call std::vector<int, std::allocator<int> >::vector() [complete object constructor] #8.5
leave #8.5
ret
虽然我同意其他人的观点,即在禁用优化的情况下查看性能收效甚微,但在这种情况下,似乎即使禁用优化也不足以(至少对于这三个编译器)获得不同的代码来构造对象两个 类。我想如果有一些编译器 and/or 优化设置会产生不同的结果,我不会感到非常惊讶,但恐怕我还不够雄心勃勃,无法花更多时间寻找它。