【问题标题】:emplace_back vs. push_back for primitive typesemplace_back 与原始类型的 push_back
【发布时间】:2018-01-09 05:45:21
【问题描述】:

我想知道std::vectoremplace_backpush_back 方法在使用原始标量类型(例如std::uint32_tstd::uint8_t)时是否有任何不同。直觉上,我猜想,在编译之后,这两种变体都会在此处产生相同的字节码:

void copyListContent(std::uint8_t * list, std::size_t nElems,
                     std::vector<std::uint8_t> & vec)
{
    vec.clear();
    vec.reserve(nElems);
    for (std::size_t i = 0; i < nElems; ++i)
    {
        //variant 1:
        vec.push_back(list[i]);
        //variant 2:
        vec.emplace_back(list[i]);
    }        
}

如果这已经是错误的,请纠正我......

现在,当我问自己如果“列表”和向量的类型不匹配时会发生什么时,我开始挣扎:

void copyListContent(std::uint8_t * list, std::size_t nElems,
                     std::vector<std::uint32_t> & vec)
{
    //... same code as above      
}

std::uint8_t 元素在放入向量时将被转换为std::uint32_t(使用emplace_backpush_back),所以我想知道这是否会触发一些“构造函数”被调用?在那种情况下,emplace_back 会更有效吗,因为它会避免构造一个将被复制的临时对象?还是这些隐式转换没有任何区别,emplace_backpush_back 的行为会相同?

所以,我问自己和你: 对于此类原始类型,emplace_backpush_back 的行为是否总是相似?

作为一个模糊的猜测,我会说“可能是的”,但我对 C++ 内部的了解不足,无法为自己可靠地回答这个问题。我很乐意了解这种情况下的工作原理 - 非常感谢!

【问题讨论】:

  • 我很乐意了解它的工作原理 - 首先:编译器非常聪明。
  • 这很好,但不是每个人都会接受的论点 :-) 假设你有人告诉你“应该使用 emplace_back,它更有效”......编译器也只是人造的,并且必须有人能够判断对于这些情况,两个函数是否会“总是”产生相同的字节码,或者“不总是”,或者“理论上不总是,但在实践中总是”;-)

标签: c++ c++11 vector


【解决方案1】:

GCC 将两个版本的代码编译成相同的结果程序集 (Godbolt.org):

#include<vector>

void push(std::vector<int> & vec, int val) {
    vec.push_back(val);
}

#include<vector>

void push(std::vector<int> & vec, int val) {
    vec.emplace_back(val);
}

两者都会产生以下程序集:

push(std::vector<int, std::allocator<int> >&, int):
        push    r15
        push    r14
        push    r13
        push    r12
        push    rbp
        push    rbx
        sub     rsp, 24
        mov     rbx, QWORD PTR [rdi+8]
        cmp     rbx, QWORD PTR [rdi+16]
        je      .L2
        mov     DWORD PTR [rbx], esi
        add     rbx, 4
        mov     QWORD PTR [rdi+8], rbx
        add     rsp, 24
        pop     rbx
        pop     rbp
        pop     r12
        pop     r13
        pop     r14
        pop     r15
        ret
.L2:
        mov     r12, QWORD PTR [rdi]
        mov     r14, rbx
        mov     ecx, esi
        mov     rbp, rdi
        sub     r14, r12
        mov     rax, r14
        sar     rax, 2
        je      .L9
        lea     rdx, [rax+rax]
        mov     r15, -4
        cmp     rax, rdx
        ja      .L4
        movabs  rsi, 4611686018427387903
        cmp     rdx, rsi
        jbe     .L19
.L4:
        mov     rdi, r15
        mov     DWORD PTR [rsp], ecx
        call    operator new(unsigned long)
        mov     ecx, DWORD PTR [rsp]
        mov     r13, rax
        add     r15, rax
.L5:
        lea     rax, [r13+4+r14]
        mov     DWORD PTR [r13+0+r14], ecx
        mov     QWORD PTR [rsp], rax
        cmp     rbx, r12
        je      .L6
        mov     rdx, r14
        mov     rsi, r12
        mov     rdi, r13
        call    memmove
.L7:
        mov     rdi, r12
        call    operator delete(void*)
.L8:
        mov     QWORD PTR [rsp+8], r13
        movq    xmm0, QWORD PTR [rsp+8]
        mov     QWORD PTR [rbp+16], r15
        movhps  xmm0, QWORD PTR [rsp]
        movups  XMMWORD PTR [rbp+0], xmm0
        add     rsp, 24
        pop     rbx
        pop     rbp
        pop     r12
        pop     r13
        pop     r14
        pop     r15
        ret
.L6:
        test    r12, r12
        je      .L8
        jmp     .L7
.L9:
        mov     r15d, 4
        jmp     .L4
.L19:
        xor     r15d, r15d
        xor     r13d, r13d
        test    rdx, rdx
        je      .L5
        lea     r15, [0+rax*8]
        jmp     .L4

正如您可能推断的那样,在处理具有更复杂的构造/复制/移动行为的类型时,这不是您可以依赖的行为,但对于原始类型,差异可以忽略不计。

话虽如此,有一种情况可能会有所不同:

std::vector<int16_t> vec;
size_t seed = 0x123456789abcdef;

vec.push_back(seed);

vec.emplace_back(seed);

在(适当优化的)编译器中,两个汇编代码可能相同,但您会从编译器收到不同的缩小警告(或错误,如果您强制警告导致编译失败)。后者更有可能给出难以诊断的警告消息,因为错误将来自&lt;vector&gt; 内部,而不是来自发出调用的任何 .cpp 文件内部。

【讨论】:

    【解决方案2】:

    在 Abseil 网站上发布的 Google 指南:https://abseil.io/tips/112 是您应该更喜欢使用 push_back,因为它更具可读性。

    担心内置类型的隐式转换似乎是过早的优化;无论如何,您的编译器很可能会优化转换。

    【讨论】:

    • 谢谢!我稍微澄清了这个问题:我想知道这两个函数是否保证对于内置原始标量类型的行为与上述函数相似。我问的一个原因是我希望能够用论据来捍卫我的设计决策,而不仅仅是直觉。
    猜你喜欢
    • 2021-10-12
    • 2011-05-17
    • 2014-01-03
    • 2020-09-12
    • 2018-11-15
    • 2019-05-22
    • 2015-05-19
    • 2014-07-06
    相关资源
    最近更新 更多