编译器可以省略以下副本吗?

Posted

技术标签:

【中文标题】编译器可以省略以下副本吗?【英文标题】:Can the compiler elide the following copy? 【发布时间】:2011-05-26 13:22:54 【问题描述】:

我还是一个新手程序员,我知道过早优化不好,但我也知道复制大量内容也不好。

我已经阅读了复制省略,它是同义词,但是例如 Wikipedia 上的示例让我觉得复制省略只能在要返回的对象在完全构造的同时返回时才会发生。

向量之类的对象呢,通常只有在填充某些东西时才有意义,当用作返回值时。 毕竟,空向量只能手动实例化。

那么,在这种情况下它也可以吗?

为了简洁而糟糕的风格:

vector<foo> bar(string baz)

    vector<foo> out;
    for (each letter in baz)
        out.push_back(someTable[letter]);

    return out;


int main()

     vector<foo> oof = bar("Hello World");

我在使用 bar(vector & out, string text) 时没有真正的麻烦,但上述方式在美学和意图上看起来要好得多。

【问题讨论】:

可以省略。但是请注意,标准确实仍然要求复制构造函数是可访问的(例如非私有的) 【参考方案1】:

例如,***上的例子让我觉得只有当要返回的对象在完全构造的同时被返回时才会发生复制省略。

这是误导(阅读:错误)。问题在于在所有代码路径中仅返回 one 对象,即仅发生潜在返回对象的 one 构造。

您的代码很好,任何现代编译器都可以省略副本。

另一方面,以下代码可能会产生问题:

vector<int> foo() 
    vector<int> a;
    vector<int> b;
    // … fill both.
    bool c;
    std::cin >> c;
    if (c) return a; else return b;

这里,编译器需要完全构造两个不同的对象,只有稍后决定返回哪一个,因此它必须复制一次,因为它不能直接在目标内存中构造返回的对象位置。

【讨论】:

您的反例实际上被我在回答中引用的同一段明确免除了复制省略(但它的另一部分)。仅当返回语句中的表达式是类对象的名称时才允许省略。 编译器可以决定移动返回的向量吗? @user396672 我对 C++0x 并不坚定,但逻辑表明这确实应该是可能的(因为在 return 之后不再需要原始版本)。 这个例子是简单优化的顺便说一句:if (c) swap(a,b); return a; IOW,不用担心你是否需要在设计所有东西时都考虑到 RVO。【参考方案2】:

没有什么可以阻止编译器删除副本。这在 12.8.15 中定义:

[...] 这种复制操作的省略是 以下允许 情况(可以结合 消除多个副本):

[...]

当临时类对象具有 未绑定到参考 (12.2) 将被复制到一个类对象 相同的 cv-unqualified 类型,副本 操作可以省略 构造临时对象 直接进入目标 省略副本

是否确实取决于编译器和您使用的设置。

【讨论】:

这在我看来像是关于该主题的一般答案,例如:“如果编译器足够聪明,可以检测到忽略副本的机会,即使要返回的对象是在返回函数中进行编辑。”,是吗?编辑:啊,那个 sn-p 信息量很大,如果它出现在我阅读的文章中,我会感到羞耻......谢谢! @Erius:这仅取决于两件事:编译器是否足够聪明,以及是否允许这样做。我只能回答第一个,因为我不知道你的编译器和你的设置。 嗯,它是(希望是最新的)MSVC 一个,完全优化,但我现在完全了解编译器智能的部分,所以再次感谢。【参考方案3】:

vector 的两个隐含副本都可以 - 并且经常 - 被消除。命名返回值优化可以消除return out;的return语句中隐含的复制,同时也可以消除oof的复制初始化中隐含的临时性。

在使用这两种优化的情况下,vector&lt;foo&gt; out; 中构造的对象与oof 是同一个对象。

使用诸如此类的人工测试用例更容易测试哪些优化正在执行。

struct CopyMe

    CopyMe();
    CopyMe(const CopyMe& x);
    CopyMe& operator=(const CopyMe& x);

    char data[1024]; // give it some bulk
;

void Mutate(CopyMe&);

CopyMe fn()

    CopyMe x;
    Mutate(x);
    return x;


int main()

    CopyMe y = fn();
    return 0;

复制构造函数已声明但未定义,因此无法内联和消除对它的调用。使用现在相对较旧的 gcc 4.4 进行编译会在 -O3 -fno-inline 处生成以下程序集(过滤以去除 C++ 名称并进行编辑以删除非代码)。

fn():
        pushq   %rbx
        movq    %rdi, %rbx
        call    CopyMe::CopyMe()
        movq    %rbx, %rdi
        call    Mutate(CopyMe&)
        movq    %rbx, %rax
        popq    %rbx
        ret

main:
        subq    $1032, %rsp
        movq    %rsp, %rdi
        call    fn()
        xorl    %eax, %eax
        addq    $1032, %rsp
        ret

可以看出,没有调用复制构造函数。事实上,gcc 甚至在-O0 也执行这些优化。您必须提供-fno-elide-constructors 才能关闭此行为;如果你这样做,那么 gcc 会生成两个对 CopyMe 的复制构造函数的调用——一个在调用内部,一个在调用外部 fn()

fn():
        movq    %rbx, -16(%rsp)
        movq    %rbp, -8(%rsp)
        subq    $1048, %rsp
        movq    %rdi, %rbx
        movq    %rsp, %rdi
        call    CopyMe::CopyMe()
        movq    %rsp, %rdi
        call    Mutate(CopyMe&)
        movq    %rsp, %rsi
        movq    %rbx, %rdi
        call    CopyMe::CopyMe(CopyMe const&)
        movq    %rbx, %rax
        movq    1040(%rsp), %rbp
        movq    1032(%rsp), %rbx
        addq    $1048, %rsp
        ret

main:
        pushq   %rbx
        subq    $2048, %rsp
        movq    %rsp, %rdi
        call    fn()
        leaq    1024(%rsp), %rdi
        movq    %rsp, %rsi
        call    CopyMe::CopyMe(CopyMe const&)
        xorl    %eax, %eax
        addq    $2048, %rsp
        popq    %rbx
        ret

【讨论】:

以上是关于编译器可以省略以下副本吗?的主要内容,如果未能解决你的问题,请参考以下文章

这是常见的优化吗?

CLR是否执行“锁定省略”优化?如果不是为什么不呢?

为什么省略“#include “有时只会导致编译失败?

volatile

已删除的副本分配检测到虚假错误?

C++ 编译器可以重新排序结构中的元素吗