Jak prawidłowo i wydajnie uniknąć kopiowania obiektu ?

0

Witam,

robię trochę testów szybkości działania dla poniższego kodu. W jaki sposób prawidłowo uniknąć kopiowania obiektu przy dodawaniu go do vectora ? Dopiero rozgryzam zasadę działania r-value reference

class Test
{
public:

	Test() : _a(0), _str("")
	{
		//std::cout << "empty constructor\n";
	}

	Test(const int &a, const std::string &str) : _a(a), _str(str)
	{
		//std::cout << "constructor parametrised with int a = " << a << std::endl;
	}

	const Test(const Test &other)
	{
		_a = other._a;
		_str = other._str;
		//std::cout << "copy constructor\n";
	}

	Test(Test &&other): _a(other._a), _str(other._str)
	{
		//std::cout << "move constructor\n";
	}
	
	const Test &operator=(const Test &other)
	{
		//std::cout << "assignment operator\n";
		this->_a = other._a;
		return *this;
	}

	~Test()
	{
		//std::cout << "destructor running\n";
	}
private:
	int _a;
	std::string _str;
};

int main()
{
	int kIterations = 500000;
	std::string kString = "jakis tekst do powielania";
	std::vector<Test> vec;
	std::vector<std::unique_ptr<Test>> vPtrs;
	auto start = std::chrono::steady_clock::now();
	
	for (int i = 0; i < kIterations; i++)
	{
		vPtrs.push_back(std::make_unique<Test>(i, kString));
	}
	auto duration = std::chrono::duration_cast<std::chrono::milliseconds>
		(std::chrono::steady_clock::now() - start);

	std::cout << "make_unique: " << duration.count() << " ms\n";
	
	start = std::chrono::steady_clock::now();
	for (int i = 0; i < kIterations; i++)
	{
		vec.push_back({ i, kString });
	}
	duration = std::chrono::duration_cast<std::chrono::milliseconds>
		(std::chrono::steady_clock::now() - start);
	
	std::cout << "copy constructor: " << duration.count() << " ms\n";

	start = std::chrono::steady_clock::now();
	for (int i = 0; i < kIterations; i++)
	{
		vec.push_back(std::move(Test(i, kString)));
	}
	duration = std::chrono::duration_cast<std::chrono::milliseconds>
		(std::chrono::steady_clock::now() - start);

	std::cout << "move + copy: " << duration.count() << " ms\n";

	return 0;
}

oczywiście std::move rzutuje tylko dodawany obiekt na rvalue, reszta jest kopiowana
flaga /O2
wyniki to:

make_unique: 563 ms
copy constructor: 2331 ms
move + copy: 1357 ms

2

Jako, że pola klasy nie wymagają specjalnego traktowania, powinieneś stosować "rule of zero".
Przykładowo nie popsułbyś wtedy "move constructor":

Test(Test &&other): _a(other._a), _str(std::move(other._str))
{}

Poza tym, źle używasz std::move. Jak już masz r-value to std::move jest zbędne. Np tu:

vec.push_back(std::move(Test(i, kString))); // Źle!

Na dodatek brakuje ci:

std::vector<Test> vec;
vec.reserve(2 * kIterations);

Wtedy unika się kopiowania, związanego z realokacją większego bloku pamięci.

0

Dzięki za wskazówki

Wcześniej miałem

Test(Test &&other): _a(std::move(other._a)), _str(std::move(other._str))
{ }

ale nie byłem pewny poprawności i przed dodaniem posta usunąłem rzutowanie. Poczytam w wolnej chwili o "rule of zero".

Jeszcze pytanie odnośnie testowanych metod: która wersja dodawania obiektu do kontenera jest bardziej... poprawna ?
I czy da się uniknąć kopiowania w inny sposób niż przez użycie unique_ptr ?

0
MarekR22 napisał(a):

Poza tym, źle używasz std::move. Jak już masz r-value to std::move jest zbędne. Np tu:

vec.push_back(std::move(Test(i, kString))); // Źle!

Nadmiarowe move() raczej nie powinno zaszkodzić.

#include <iostream>
using namespace std;

class Test
{
public:
    Test() { cout << "Test()" << endl; }
    ~Test() { cout << "~Test()" << endl; }
    Test(const Test&) { cout << "Test(const Test&)" << endl; }
    Test& operator=(const Test&) { cout << "Test& operator=(const Test&)" << endl; }
    Test(Test&&) { cout << "Test(Test&&)" << endl; }
    Test& operator=(Test&&) { cout << "Test& operator=(Test&&)" << endl; }
};

int main()
{
    Test test1;
    Test test2 = move(move(move(move(move(move(move(move(test1))))))));
}

VS2017:

Test()
Test(Test&&)
~Test()
~Test()

EDIT: a jednak, kiedy mamy coś takiego:

Test test3 = move(Test());

to ma miejsce przenoszenie

Test()
Test(Test&&)
~Test()
~Test()

którego nie ma rzecz jasna tutaj:

Test test3 = Test();
Test()
~Test()

czyli jest różnica między jednym nadmiarowym move a brakiem move, ale każde dodatkowe move już nic nie robi (RVO, copy ellision).

std::vector ma metodę emplace_back() która służy do dodawania obiektu do kolekcji bezpośrednio w kolekcji, bez konieczności kopiowania ani nawet przenoszenia.

5

Nadmiarowe move jak najbardziej może zaszkodzić, uniemożliwiając (N)RVO. Aczkolwiek w przypadku wyżej to po prostu wielokrotny cast, a więc noop.

https://godbolt.org/z/apshaD

A co do samego kontenera, użyłbym emplace_back().

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