为什么 move ctor 比 copy ctor 慢?
Why is move ctor slower than copy ctor?
我有下面的代码来测试std::string
class的copy ctor和move ctor,结果让我大吃一惊,move ctor是~1.4 比复制 ctor 慢 1 倍。
据我了解,move-constructing不需要分配内存,对于std::string
的情况,move-constructed对象中可能有一个内部指针直接设置为被移动对象的内部指针,它应该比为缓冲区分配内存然后在复制构造时从对象复制内容更快。
代码如下:
#include <string>
#include <iostream>
void CopyContruct(const std::string &s) {
auto copy = std::string(s);
}
void MoveContruct(std::string &&s) {
auto copy = std::move(s);
//auto copy = std::string(std::move(s));
}
int main(int argc, const char *argv[]) {
for (int i = 0; i < 50000000; ++i) {
CopyContruct("hello world");
//MoveContruct("hello world");
}
return 0;
}
编辑:
从两个函数的集合中,我可以看到 MoveConstruct
有一个 std::remove_reference
class 模板的实例化,我认为这应该是罪魁祸首,但我是不熟悉汇编,谁能详细说说?
以下代码是在 https://godbolt.org/ 上使用 x86-64 gcc7.2 反编译的:
CopyContruct(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
push rbp
mov rbp, rsp
sub rsp, 48
mov QWORD PTR [rbp-40], rdi
mov rdx, QWORD PTR [rbp-40]
lea rax, [rbp-32]
mov rsi, rdx
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
lea rax, [rbp-32]
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()
nop
leave
ret
MoveContruct(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&&):
push rbp
mov rbp, rsp
sub rsp, 48
mov QWORD PTR [rbp-40], rdi
mov rax, QWORD PTR [rbp-40]
mov rdi, rax
call std::remove_reference<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>::type&& std::move<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)
mov rdx, rax
lea rax, [rbp-32]
mov rsi, rdx
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&&)
lea rax, [rbp-32]
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()
nop
leave
ret
编辑2:
事情变得有趣了,我把std::string
改成了std::vector
,正如@FantasticMrFox在评论中提到的,结果正好相反,MoveConstruct
快了~1.9
倍比 CopyConstruct
,似乎 std::remove_reference
不是罪魁祸首,但这两个 class 的 优化 可能是。
编辑3:
以下代码在 MacOS 上使用 Apple LLVM 版本 8.0.0 (clang-800.0.42.1) 编译,优化标志为 -O3。
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 11
.globl __Z12CopyContructRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
.align 4, 0x90
__Z12CopyContructRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE: ## @_Z12CopyContructRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
pushq %rbx
subq , %rsp
Ltmp3:
.cfi_offset %rbx, -24
movq %rdi, %rax
leaq -32(%rbp), %rbx
movq %rbx, %rdi
movq %rax, %rsi
callq __ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEC1ERKS5_
movq %rbx, %rdi
callq __ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEED1Ev
addq , %rsp
popq %rbx
popq %rbp
retq
.cfi_endproc
.globl __Z12MoveContructONSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
.align 4, 0x90
__Z12MoveContructONSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE: ## @_Z12MoveContructONSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp4:
.cfi_def_cfa_offset 16
Ltmp5:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp6:
.cfi_def_cfa_register %rbp
subq , %rsp
movq 16(%rdi), %rax
movq %rax, -8(%rbp)
movq (%rdi), %rax
movq 8(%rdi), %rcx
movq %rcx, -16(%rbp)
movq %rax, -24(%rbp)
movq [=12=], 16(%rdi)
movq [=12=], 8(%rdi)
movq [=12=], (%rdi)
leaq -24(%rbp), %rdi
callq __ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEED1Ev
addq , %rsp
popq %rbp
retq
.cfi_endproc
这种微基准测试通常具有误导性,因为它没有测试您认为它测试的东西。
但是,对于您的情况,我可以解释您所看到的测量结果最可能的原因。
std::string
,在所有现代实现中,使用一种叫做 "small buffer optimization" 或 SBO 的东西。 (@FantasticMrFox 在关于使用享元的评论中的断言是错误的。我认为除了空字符串之外,没有任何流行的实现使用过享元。他的意思是写时复制,过去 GNU 的标准库使用它,但是GNU 切换了,因为兼容的 C++11 字符串不能使用 COW。)
在这个优化中,一些space被保留在字符串对象内部,用于存储短字符串并避免为它们分配堆。
这意味着字符串的复制和移动构造函数大致如下实现:
copy(source) {
if source length > internal buffer capacity
allocate space
copy source buffer to my buffer
}
move(source) {
if source uses internal buffer {
copy source buffer to my buffer
set source length to zero
set first byte of source buffer to zero
} else {
steal source buffer
}
}
如您所见,移动构造函数有点复杂。它也比某些实现中的优化更优化,但一般逻辑保持不变。
所以对于小的缓冲区字符串(我怀疑你正在测试的那个字符串适合你的特定实现),复制的工作量就更少了,因为不需要重置源字符串。
但是当您打开完全优化时,编译器可能会识别出一些死存储并将其删除。 (当然,编译器可能只是删除了您的整个基准测试,因为它实际上并没有做任何事情。)
当 I feed your code to clang or gcc with -O3
我从 clang 得到:
main: # @main
mov eax, 50000000
.LBB0_1: # =>This Inner Loop Header: Depth=1
add eax, -25
jne .LBB0_1
xor eax, eax
ret
和 gcc:
main:
xor eax, eax
ret
我确实将函数放在匿名命名空间中,以消除必须自行导出函数的噪音。但是 main 正在被完全优化掉。
微基准通常具有误导性。
我有下面的代码来测试std::string
class的copy ctor和move ctor,结果让我大吃一惊,move ctor是~1.4 比复制 ctor 慢 1 倍。
据我了解,move-constructing不需要分配内存,对于std::string
的情况,move-constructed对象中可能有一个内部指针直接设置为被移动对象的内部指针,它应该比为缓冲区分配内存然后在复制构造时从对象复制内容更快。
代码如下:
#include <string>
#include <iostream>
void CopyContruct(const std::string &s) {
auto copy = std::string(s);
}
void MoveContruct(std::string &&s) {
auto copy = std::move(s);
//auto copy = std::string(std::move(s));
}
int main(int argc, const char *argv[]) {
for (int i = 0; i < 50000000; ++i) {
CopyContruct("hello world");
//MoveContruct("hello world");
}
return 0;
}
编辑:
从两个函数的集合中,我可以看到 MoveConstruct
有一个 std::remove_reference
class 模板的实例化,我认为这应该是罪魁祸首,但我是不熟悉汇编,谁能详细说说?
以下代码是在 https://godbolt.org/ 上使用 x86-64 gcc7.2 反编译的:
CopyContruct(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&):
push rbp
mov rbp, rsp
sub rsp, 48
mov QWORD PTR [rbp-40], rdi
mov rdx, QWORD PTR [rbp-40]
lea rax, [rbp-32]
mov rsi, rdx
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
lea rax, [rbp-32]
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()
nop
leave
ret
MoveContruct(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&&):
push rbp
mov rbp, rsp
sub rsp, 48
mov QWORD PTR [rbp-40], rdi
mov rax, QWORD PTR [rbp-40]
mov rdi, rax
call std::remove_reference<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>::type&& std::move<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)
mov rdx, rax
lea rax, [rbp-32]
mov rsi, rdx
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&&)
lea rax, [rbp-32]
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()
nop
leave
ret
编辑2:
事情变得有趣了,我把std::string
改成了std::vector
,正如@FantasticMrFox在评论中提到的,结果正好相反,MoveConstruct
快了~1.9
倍比 CopyConstruct
,似乎 std::remove_reference
不是罪魁祸首,但这两个 class 的 优化 可能是。
编辑3:
以下代码在 MacOS 上使用 Apple LLVM 版本 8.0.0 (clang-800.0.42.1) 编译,优化标志为 -O3。
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 11
.globl __Z12CopyContructRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
.align 4, 0x90
__Z12CopyContructRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE: ## @_Z12CopyContructRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
pushq %rbx
subq , %rsp
Ltmp3:
.cfi_offset %rbx, -24
movq %rdi, %rax
leaq -32(%rbp), %rbx
movq %rbx, %rdi
movq %rax, %rsi
callq __ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEC1ERKS5_
movq %rbx, %rdi
callq __ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEED1Ev
addq , %rsp
popq %rbx
popq %rbp
retq
.cfi_endproc
.globl __Z12MoveContructONSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
.align 4, 0x90
__Z12MoveContructONSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE: ## @_Z12MoveContructONSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp4:
.cfi_def_cfa_offset 16
Ltmp5:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp6:
.cfi_def_cfa_register %rbp
subq , %rsp
movq 16(%rdi), %rax
movq %rax, -8(%rbp)
movq (%rdi), %rax
movq 8(%rdi), %rcx
movq %rcx, -16(%rbp)
movq %rax, -24(%rbp)
movq [=12=], 16(%rdi)
movq [=12=], 8(%rdi)
movq [=12=], (%rdi)
leaq -24(%rbp), %rdi
callq __ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEED1Ev
addq , %rsp
popq %rbp
retq
.cfi_endproc
这种微基准测试通常具有误导性,因为它没有测试您认为它测试的东西。
但是,对于您的情况,我可以解释您所看到的测量结果最可能的原因。
std::string
,在所有现代实现中,使用一种叫做 "small buffer optimization" 或 SBO 的东西。 (@FantasticMrFox 在关于使用享元的评论中的断言是错误的。我认为除了空字符串之外,没有任何流行的实现使用过享元。他的意思是写时复制,过去 GNU 的标准库使用它,但是GNU 切换了,因为兼容的 C++11 字符串不能使用 COW。)
在这个优化中,一些space被保留在字符串对象内部,用于存储短字符串并避免为它们分配堆。
这意味着字符串的复制和移动构造函数大致如下实现:
copy(source) {
if source length > internal buffer capacity
allocate space
copy source buffer to my buffer
}
move(source) {
if source uses internal buffer {
copy source buffer to my buffer
set source length to zero
set first byte of source buffer to zero
} else {
steal source buffer
}
}
如您所见,移动构造函数有点复杂。它也比某些实现中的优化更优化,但一般逻辑保持不变。
所以对于小的缓冲区字符串(我怀疑你正在测试的那个字符串适合你的特定实现),复制的工作量就更少了,因为不需要重置源字符串。
但是当您打开完全优化时,编译器可能会识别出一些死存储并将其删除。 (当然,编译器可能只是删除了您的整个基准测试,因为它实际上并没有做任何事情。)
当 I feed your code to clang or gcc with -O3
我从 clang 得到:
main: # @main
mov eax, 50000000
.LBB0_1: # =>This Inner Loop Header: Depth=1
add eax, -25
jne .LBB0_1
xor eax, eax
ret
和 gcc:
main:
xor eax, eax
ret
我确实将函数放在匿名命名空间中,以消除必须自行导出函数的噪音。但是 main 正在被完全优化掉。
微基准通常具有误导性。