Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

emplace_back vs. push_back for primitive types

Tags:

c++

c++11

vector

I'm wondering if the emplace_back and push_back methods of std::vector behave any different when using primitive scalar types, such as std::uint32_t or std::uint8_t. Intuitively, I would guess that, after compilation, both variants would lead to the same bytecode here:

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]);
    }        
}

Please correct me if that should be already wrong...

Now, where I'm starting to struggle is when I'm asking myself what happens if the types of the "list" and vector don't match:

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

The std::uint8_t elements will be converted to std::uint32_t when putting them into the vector (using emplace_back or push_back), so I'm wondering if that triggers some "constructor" to be called? In that case, would emplace_back be more efficient, because it would avoid to construct a temporary object that would be copied? Or are these implicit conversions that don't make any difference, and emplace_back and push_back will behave the same?

So, I'm asking myself, and you: For primitive types like these, do emplace_back and push_back always behave similarly?

As a vague guess I'd say "probably yes", but I have not enough knowledge about C++ internals to reliably answer this for myself. I'd be happy to learn how things work in this case - thanks a lot in advance!

like image 448
volzotan Avatar asked Jan 08 '18 21:01

volzotan


2 Answers

GCC compiles both versions of the code to the same resulting assembly (Godbolt.org):

#include<vector>

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

vs

#include<vector>

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

Both result in the following assembly:

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

As you might have deduced, this is not behavior you can depend on when dealing with types that have more complex constructing/copying/moving behaviors, but for primitive types, the difference is negligible.

Having said that, there's one situation where there might be a difference:

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

vec.push_back(seed);

vs

vec.emplace_back(seed);

In a (properly optimized) compiler, the two assembly codes will probably be identical, but you'll get different narrowing warnings (or errors, if you force warnings to cause compilation failure) from the compiler. The latter is more likely to give a difficult-to-diagnose warning message, as the error will originate from inside <vector> instead of inside whatever .cpp file made the call.

like image 142
Xirema Avatar answered Oct 24 '22 20:10

Xirema


The Google guidelines as published on the Abseil site: https://abseil.io/tips/112 are that you should prefer to use push_back as it is more readable.

Worrying about the implicit conversions for built in types seems like premature optimisation; chances are your compiler will optimise the conversion away anyway.

like image 41
James Sharpe Avatar answered Oct 24 '22 18:10

James Sharpe