본문 바로가기

Programming/CPP11&14

[C++11] RValue Reference(2)

반응형

3. Move Semantics

RValue Reference는 Move Semantics를 통해서 성능 향상이 가능합니다.

객체의 리소스를 다른 객체로 이동시켜주는 것이 Move Semantics입니다.

왜 기존의 복사가 아닌 이동을 사용해야 되는가에 대한 이유는 다음과 같습니다.

RValue가 표현식 이후에 어디에서도 참조할 수 없는 임시적인 값이기 때문에 복사 대신 이동을 하는 것입니다.

LValue라면 리소스의 이동을 해버리면 원본이 훼손되는 문제가 있지만 RValue는 그런 문제가 생기지 않습니다.

이 Move Semantics의 이점은 복사 생성자, 복사 대입 연산자 등에서 발생하는

불필요한 리소스의 할당 및 복사, 해제의 과정을 생략할 수 있다는 점입니다.

STL의 vector를 통해서 자세히 알아보도록 하겠습니다.

vector의 경우 일반적으로 가용량(capacity로 size와는 다릅니다.)이 꽉찬 상태에서

새로운 객체가 추가될 경우에 다음과 같은 동작이 일어납니다.

1. 기존 가용량의 2배 크기의 메모리 공간을 할당

2. vector 내의 기존 객체들을 복사 생성자를 통해 복사한 이후에 기존 객체 제거

3. 새로운 객체 추가

만약 객체 내부에 동적으로 메모리를 할당하는 부분이 있으면

복사 생성자는 동일한 크기의 새로운 메모리 영역을 할당하고 값을 복사할 것입니다.

이러한 과정은 상당한 낭비라고 할 수 있습니다.

기존의 객체들이 복사 이후에 소멸되어 다른 곳에서 참조가 불가능한  RValue의 특성을 갖기 때문에

메모리의 포인터를 전달만 해주면 불필요한 메모리의 할당과 해제를 줄일 수 있습니다.

Move Semantics는 이러한 불필요한 오버헤드를 줄여줍니다.

예제 코드를 보도록 하겠습니다.(MSDN 참조)

#include <iostream>
#include <algorithm>
#include <vector>

class MemoryBlock
{
public:
	// Simple constructor that initializes the resource.
	explicit MemoryBlock(size_t length) : _length(length), _data(new int[length])
	{
		std::cout << "Constructor Length : " << _length << std::endl;
	}
	// Destructor.
	~MemoryBlock()
	{
		std::cout << "Destructor Length : " << _length << std::endl;
		if (_data != NULL)
		{
			std::cout << "Deleting resource." << std::endl;
			// Delete the resource.
			delete[] _data;
		}
	}
	// Copy constructor.
	MemoryBlock(const MemoryBlock& other) : _length(other._length), _data(new int[other._length])
	{
		std::cout << "Copy Constructor Length : " << _length << std::endl;
		//std::copy(other._data, other._data + _length, _data);
		memcpy_s(_data, _length, other._data, _length);
	}
	// Copy assignment operator.
	MemoryBlock& operator=(const MemoryBlock& other)
	{
		std::cout << "Copy assignment operator Length : " << other._length << std::endl;
		if (this != &other)
		{
			// Free the existing resource.
			delete[] _data;
			_length = other._length;
			_data = new int[_length];
			//std::copy(other._data, other._data + _length, _data);
			memcpy_s(_data, _length, other._data, _length);
		}

		return *this;
	}
	// Retrieves the length of the data resource.
	size_t Length() const
	{
		return _length;
	}

private:
	size_t _length; // The length of the resource.
	int* _data; // The resource.
};

int main()
{
	std::vector<MemoryBlock> vTemp;

	vTemp.push_back(MemoryBlock(50));
	std::cout << "====================" << std::endl;
	vTemp.push_back(MemoryBlock(100));
	std::cout << "====================" << std::endl;
	vTemp[0] = MemoryBlock(75);
	std::cout << "====================" << std::endl;

	return 0;
}

int* 멤버에 동적으로 메모리를 할당하는 MemoryBlock이라는 클래스입니다.

아직 Move 생성자와 Move 대입 연산자가 없는 상태입니다.

일반적인 Copy 생성자, Copy 대입 연산자가 존재합니다.

main 함수에서는 RValue 객체를 2번 넣어주고 새로운 RValue 객체를 0번에 대입해 줍니다.

코드 사이사이에 구분선을 출력하도록 했습니다.

결과는 다음과 같이 출력됩니다.

처음 push_back()을 통해서 복사 생성자를 호출하게 됩니다.

새로운 메모리를 할당하고 임시 객체인 MemoryBlock(50)은 소멸자에서 메모리를 해제하고 있습니다.

두 번째 부분에서는 50이 다시 복사와 삭제가 발생하게 됩니다.

이 상황이 capacity가 1인 상황에서 새로운 값이 추가로 들어온 경우입니다.(메모리를 재할당해야 하는 상황)

Copy 대입 연산자 역시 불필요한 동적 메모리의 복사 및 해제가 계속되고 있습니다.

4. Move 생성자와 Move 대입 연산자

Move 생성자와 Move 대입 연산자를 MemoryBlock에 추가해 보겠습니다.

// Move constructor.
MemoryBlock(MemoryBlock&& other) : _data(NULL), _length(0)
{
	std::cout << "Move Constructor Length : " << other._length << std::endl;
	*this = std::move(other);
}
// Move assignment operator.
MemoryBlock& operator=(MemoryBlock&& other)
{
	std::cout << "Move assignment operator Length : " << _length << std::endl;
	if (this != &other)
	{
		// Free the existing resource.
		delete[] _data;

		_data = other._data;
		_length = other._length;

		other._data = NULL;
		other._length = 0;
	}

	return *this;
}

먼저 Move 대입 연산자를 보도록 하겠습니다.

Copy 대입 연산자와 동일하게 먼저 자기자신을 대입하는 경우를 막고 있습니다.

그 이후에 현재 메모리를 먼저 해제해 줍니다.

여기까지는 Copy 과정과 동일하게 진행되지만 그 아래 부분은 메모리 할당 대신에 포인터를 직접 넘겨줍니다.

그리고 소멸자에서 잘못된 포인터에 대한 해제 등을 막기 위해서 적절하게 처리(NULL과 0을 대입)해 줍니다.

NULL처리를 하지 않으면 2번 해제하려고 해서 에러가 발생됩니다.

메모리의 할당과 복사, 해제(other의 메모리의 해제가 발생하지 않습니다.)의 과정이 사라졌습니다.

other의 경우는 소멸자를 통해서 사라지는데, 이 때 메모리 해제가 발생하지 않습니다.

Move 생성자는 std::move()라는 새로 생긴 표준 라이브러리를 사용하여 Move 대입 연산자를 호출했습니다.

std::move()는 단순히 RValue Reference의 형식으로 변환해주는 역할을 합니다.

std::move()는 <utility>에 포함되어 있습니다.

주의해야 할 점은 LValue를 std::move()를 사용해서 RValue Reference로 바꾸면

이후에 LValue값을 보장할 수 없게 됩니다.

결과는 다음과 같이 출력 됩니다.

결과가 비슷하지만 실제로는 Move 생성자와 Move 대입 연산자에서 메모리의 할당과 복사가 이뤄지지 않는다는 점,

임시 객체의 소멸 과정에서 메모리 해제가 발생하지 않는다는 점에서 크게 성능 향상을 가져옵니다.

동적 할당 메모리 뿐만 다른 리소스에 대해서도 적용이 가능합니다.

생성자와 대입 연산자 외에도 일반 함수 역시 RValue Reference를 전달받도록

오버로드가 가능하기 때문에 필요에 따라서 오버로딩된 함수를 작성할 수 있습니다.

Move Semantics는 현재 STL에도 적용되어 있기 때문에 Visual Studio 2010 이상을 사용하면

추가적인 작업 없이도 성능의 향상을 기대할 수 있습니다.

반응형