考虑下面的类,主要是为了基准测试而创建的:
class String {
char* data_;
public:
String(const char* arg = "") : data_(new char[strlen(arg) + 1]) { strcpy(data_, arg); }
String(const String& other) : String(other.data_) { }
String(String&& other) noexcept : data_(other.data_) { other.data_ = nullptr; }
String& operator=(String other) noexcept { swap(other); return *this; }
~String() { delete[] data_; }
void swap(String& rhs) noexcept { std::swap(data_, rhs.data_); }
const char* data() const { return data_; }
};
void swap(String& lhs, String& rhs) noexcept { lhs.swap(rhs); }
我尝试比较两个示例与自定义swap
和std::swap
的交换效率。对于自定义swap
,GCC 8.2(-O2
)生成以下x86_64程序集:
mov rax, QWORD PTR [rdi]
mov rdx, QWORD PTR [rsi]
mov QWORD PTR [rdi], rdx
mov QWORD PTR [rsi], rax
ret
这与两个指针的交换完全匹配。然而,对于std::swap
,生成的程序集是:
mov rdx, QWORD PTR [rsi]
mov QWORD PTR [rsi], 0 // (A)
mov rax, QWORD PTR [rdi]
mov QWORD PTR [rdi], 0 // (1)
mov QWORD PTR [rsi], rax // (B)
mov rax, QWORD PTR [rdi] // (2)
mov QWORD PTR [rdi], rdx
test rax, rax // (3)
je .L3
mov rdi, rax
jmp operator delete[](void*)
.L3:
ret
我好奇的是为什么GCC会产生如此低效的代码。指令(1)将[rdi]
设置为零。然后将该零加载到rax
(2)。然后,rax
被测试(3)是否应该调用operator delete
。
如果rax
保证为零,为什么GCC要测试rax
?对于优化器来说,避免这种测试似乎是一个非常简单的案例。
Godbolt演示:https://godbolt.org/z/WNm2if
效率低下的另一个原因是0首先被写入[rsi]
(A),然后被另一个值覆盖(B)。
底线是:我希望编译器为std::swap
和自定义swap
生成相同的机器码,但这并没有发生。这表明即使对于支持移动语义的类,编写自定义交换函数也是有意义的。
2条答案
按热度按时间gpfsuwkq1#
指令(1)将[rdi]设置为零。然后将该零加载到rax(2)中。
但情况并非总是如此,因为rax首先被复制到[rsi],并且不能保证[rsi]和[rdi]是不同的。所以gcc不能做这个假设,它也没有。任何其他编译器也没有。
编辑(摘自评论):如果[rsi]和[rdi]相同,那么[rdi]包含零(来自指令A的“不同”),如果它们部分重叠,这是未定义的行为,编译器可以假设它永远不会发生。但优化者看不到这一点。
这表明编写自定义交换函数即使对于支持移动语义的类也是有意义的。
编写 separate 复制赋值和移动赋值操作符,问题就解决了。这个新奇的通过复制赋值操作符看起来是个好主意,但正如我们所看到的,编译器可能会有一些麻烦来优化它---其他编译器有很多麻烦。看看MSVC或clang生成的程序集就知道了。
tvmytwxo2#
推理最有可能的是,虽然第一个代码示例生成高效的代码,因为在程序和所需的可执行文件之间没有抽象(你正在编写swap)然而,对于第二个,swap是标准库的成员,它可能有这个检查作为安全措施,GCC根本没有优化出来。基本上,它归结为这样一个事实,即标准库旨在成为标准的,这意味着它必须具有这样的安全检查,而为特定任务编写代码将更加优化特定任务,因为您正在为一种情况进行编程,而不是百分之一的可能使用swap的情况。