push_back szybszy niż emplace_back?

0

Napisałem prosty test, żeby sprawdzić o ile szybsza jest operacja emplace_back od push_back.
Ku mojemu zdziwieniu wynik to:
push_back: 1048
emplace_back: 1150
Dlaczego tak jest? Kod poniżej.

#include <vector>
#include <iostream>
#include <time.h>


class Foo
{
private:
	int x, y;
	double z;
public:
	Foo(int x, int y, double z) : x(x), y(y), z(z) {}
};

int main() 
{
	constexpr size_t N = 1000000;
	std::vector<Foo> vec;
	std::vector<Foo> tab;
	vec.reserve(N);
	tab.reserve(N);
	//PUSH_BACK
	clock_t startPush = clock();

	for (size_t i = 0; i < N; ++i)
		tab.push_back(Foo(4, 2, 1.2));

	clock_t stopPush = clock();
	std::cout << "push_back: " << stopPush - startPush << std::endl;

	//EMPLACE_BACK
	clock_t startEmplace = clock();

	for (size_t i = 0; i < N; ++i)
		vec.emplace_back(4, 2, 1.2);

	clock_t stopEmplace = clock();
	std::cout << "emplace_back: " << stopEmplace - startEmplace << std::endl;
	return 0;
}

6

Po pierwsze, nie spodziewałbym się żadnej istotnej różnicy między push_back(Foo(1,2,3)) a emplace_back(1,2,3), bo ten push_back i tak przyjmuje Foo&& co dalej kompilator może zoptymalizować do tego samego kodu maszynowego co emplace_back.

Po drugie, 1048 a 1150 to nie jest istotna różnica - czy utrzymuje się ona za każdym razem? Co gdy N jest 100 razy większe? Co gdy pętle w kodzie są w odwrotnej kolejności?

Po trzecie, utworzenie czy nawet skopiowanie tak małego obiektu Foo to nie jest jakiś wyczyn, zwłaszcza że jego przesunięcie (move) jest tym samym co skopiowanie. Z kolei ciągłe powiększanie wektora ciągnie za sobą alokacje nowych bloków i kopiowanie całego bufora - w efekcie możesz mierzyć nie to co wydaje ci się że mierzysz (bo konstrukcja obiektów Foo może być tu drobnym procentem całkowitego czasu działania pętli).

Po czwarte, mierzysz w trybie debug czy release? Pomiary w debug są bezużyteczne.

U mnie jest:
// debug
push_back: 677
emplace_back: 824

// release
push_back: 15
emplace_back: 15

(czasami 14, czasami 16 - w każdym razie widać że różnica jest pomijalna)

3

Przy takich trywialnych danych, które pięknie się kopiują, to trudno o porównanie
Zamień z na stringa i zobacz co się stanie:

#include <vector>
#include <iostream>
#include <time.h>
#include <string>

class Foo
{
private:
    int x, y;
    std::string z;
public:
    Foo(int x, int y, const std::string &z) : x(x), y(y), z(z) {
    }
};

int main() 
{
    constexpr size_t N = 1000000;
    std::vector<Foo> vec;
    std::vector<Foo> tab;
    vec.reserve(N);
    tab.reserve(N);
    //PUSH_BACK
    clock_t startPush = clock();

    for (size_t i = 0; i < N; ++i)
        tab.push_back(Foo(4, 2, "du*a"));

    clock_t stopPush = clock();
    std::cout << "push_back: " << stopPush - startPush << std::endl;

    //EMPLACE_BACK
    clock_t startEmplace = clock();

    for (size_t i = 0; i < N; ++i)
        vec.emplace_back(4, 2, "du*a");

    clock_t stopEmplace = clock();
    std::cout << "emplace_back: " << stopEmplace - startEmplace << std::endl;
    return 0;
}

2

Dokumentacja podaje, że obydwie operacje są w czasie stałym, ("amortized constant") chyba że trafimy na reallokację jakiegoś dużego wektora.

0

Wynik może zależeć od kompilatora, standardu oraz flag.
Załóżmy że użyłeś GCC oraz std >= c++11 (bez żadnej flagi do optymalizacji), w takim wypadku różnicy w wydajności nie ma żadnej bo mówimy o tej samej operacji :)
https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/stl_vector.h#L1201
https://godbolt.org/z/7KTzj7 (polecam pobawić się flagami i zobaczyć na asembler)

 call    std::vector<Foo, std::allocator<Foo> >::push_back(Foo&&)

Jeśli chcesz przeprowadzić prawdziwy test wydajności obu metod, użyj jakiegoś frameworka (np. gbench), wyeliminujesz w ten sposób wiele małych głupot które mogą wpłynąć na wynik końcowy ;)

1 użytkowników online, w tym zalogowanych: 0, gości: 1