티스토리 뷰

C++11에서는 기존 C++98 복사 생성과 다르게 이동 연산(move)이 생겼는데, 개념적인 설명은 다른 블로그나 Youtube에서 잘 설명하고 있었습니다. 그런데, 개념만으로는 조금 부족하다고 생각이 들었습니다. C++에 능숙하지 않거나, 다른 프로그래밍 언어에 익숙하지 않으신 분들이라면 이해도 잘 안 되는 이걸 왜 써야 하나.. 필요성을 못 느낄 수 있다고 생각합니다. 어려워서 안 쓸 수 있는 문제점이 있는데, 그래서 실제로 복사 연산과 이동 연산의 실행시간을 비교해보면서 성능에 대해 살펴보고 써야 하는 이유를 한 번 찾아봅시다.


우선 복사, 이동 연산을 살펴보기에 앞서 간단히 함수 호출, 클래스 생성 시 내부적으로 어떻게 실행되는지부터 살펴봅시다.

 

#include <iostream>
#include <string>
#include <utility>
using namespace std;

class Item
{
	public:
		Item(){
			cout << "Default\n";
		}
		/*Item(string str_) :str(str_) {
			cout << "Default\n";
		}*/
		Item(const Item &rhs) {
			cout << "Copy\n";
			this->str = rhs.str;
		}
		Item(Item &&rhs)noexcept {
			cout << "Move\n";
			this->str = move(rhs.str);
		}
		~Item() {
			cout << "Destructor\n";
		}
		void setStr(const string &rhs){
			this->str = rhs;
		}
		friend ostream& operator<<(ostream &os, const Item& rhs);

	private:
		string str;
};

ostream& operator<<(ostream &os, const Item& rhs)
{
	return os << rhs.str;
}

void FuncValue(Item s)
{
	Item FuncStr(s);
	cout << FuncStr<<'\n';
	return;
}
void FuncLValue(Item &s)
{
	Item FuncStr(s);
	cout << FuncStr << '\n';
	return;
}
void FuncRValue(Item &&s)
{
	Item FuncStr(move(s));
	cout << FuncStr << '\n';
	return;
}

int main()
{
	Item s;
	s.setStr("Hello");
	cout << "\n\n";

	cout << "Value\n";
	FuncValue(s);
	cout << '\n';

	cout << "L - Value\n";
	FuncLValue(s);
	cout << '\n';

	cout << "R - Value\n";
	FuncRValue(move(s));
	
	cout << "\n\n";

	return 0;
}

 

간단히 생성→함수 호출하는 코드입니다. 위의 코드를 실행시켜보면,

 

실행결과

위와 같이 나옵니다.

보면 하.... 이게 뭐야..라고 생각하실 것 같은데(당연), 하나하나 살펴보겠습니다.

 

위 코드의 main함수를 보면, 실험에 사용할 인스턴스 1개를 생성하고,

값을 넘겨주어 함수를 호출하고 있습니다.(FuncValue)

 

1. 값 전달 

복사 과정

이런 순서로 복사가 총 2번 이루어집니다.

복사는 복사 생성에 의해 정의됩니다.

매개변수가 인자에 의해 '생성'되고, 지역변수(FuncStr)가 매개변수(s)에 의해 '생성'되기 때문입니다.

 

여기서는 string이 복사되고 있는데, 크기가 큰 class의 생성이라면 부하가 클 것으로 예상됩니다.

 

2. L-value 참조 전달

매개변수가 참조형입니다.

main의 지역변수가 가리키는 것을 같이 가리키게 됩니다.

 

그림으로 보는 편이 이해가 쉽습니다.

이렇게 매개변수 쪽에서 복사가 일어나지 않기 때문에 1번의 Copy가 발생하는 것을 알 수 있습니다.

 

3. R-Value 이동 전달

R-value는 '복사'가 아닌 '이동'을 합니다. 그래서 이동하고 나면 이동한 변수는 값을 잃게 됩니다.

 

값을 이동(복사)하는데 드는 시간과 메모리 측면에서 차이를 보입니다.

 

값 전달, 왼값 참조 전달과는 다르게 Copy가 1번도 일어나지 않습니다.

 

 

이렇게 값, 왼값, 오른값 전달을 간단하게 살펴보았습니다.

이제, 실제로 이게 얼마나 차이가 날지 살펴보겠습니다.

 

특히, 값 복사는 확실히 부하가 크다는 것을 알 수 있습니다.

복사 - 이동 연산이 얼마나 차이가 나는지를 중점으로 보겠습니다.

 

 

4. 성능 분석

복잡한 클래스를 사용하면 좋겠으나, 시간이 오래 걸리는 String의 복사를 위주로 테스트해보겠습니다.

(간단한 프로그램이 아닌 실제로  쓰이는 프로그램들의 클래스는 상속의 상속의 상속..이라 꽤나 크기가 큽니다. 꼭 상속받았다고 해서 큰 거는 아니지만..)

 

String의 복사가 일어나도록 환경을 만들었습니다.

String은 내부적으로 String을 모두 순회하면서 복사할 것이기 때문에,

String의 길이가 길 수록 시간은 더 오래 걸립니다.

실제로 실험에는 string의 길이가 8글자인데, 9글자로 늘리면 시간이 15% 정도 증가하는 걸 확인할 수 있었습니다.

 

실험 환경은

Win 10 Home,

Visual studio 2017,

x84(32bit) Release모드 

 

다음은 실험에 사용한 코드입니다.

#include <iostream>
#include <string>
#include <utility>
#include <chrono>
#include <thread>
using namespace std;

constexpr long long COUNT = static_cast<const long long>(10e8);

class Item
{
	public:
		Item(){}
		Item(const Item &rhs) {
			this->str = rhs.str;
		}
		Item(Item &&rhs)noexcept {
			this->str = move(rhs.str);
		}
		~Item() {
		}
		void setStr(const string &rhs){
			this->str = rhs;
		}

	private:
		string str;
};

void FuncLValue(Item &s)
{
	Item FuncStr(s);
	return;
}
void FuncRValue(Item &&s)
{
	Item FuncStr(move(s));
	return;
}

int main()
{
	chrono::system_clock::time_point start = chrono::system_clock::now();

	//this_thread::sleep_for(chrono::milliseconds(100));
	for (long long i = 0; i < COUNT; ++i) {
		Item item;
		item.setStr("CopyData");
		
        //FuncLValue(item);
		FuncRValue(move(item));
	}
	chrono::duration<double> sec = chrono::system_clock::now() - start;
	printf("%.9lf(s)", sec.count());
	return 0;
}

총 10억 번 반복하고

Length가 8인 문자열을 생성-> 복사/이동-> 파괴하는 순서로 진행됩니다.

 

결과는 다소 충격적!

 

y축은 시간을 나타내고, x축은 n번째 실행, 즉 횟수를 나타냅니다.

 

각 7번씩 실행해보았다.

복사 연산의 경우는 평균 21.70초

이동 연산의 경우는 평균 14.25초

 

약 35% 정도 이동 연산이 복사 연산보다 빨랐습니다.

실험에 사용된 클래스는 달랑 string 1개만 가지고 있어서 오히려 차이가 적게 나야 정상인데,

string 1개로 이만큼의 차이는.. 정말 큰 것 같습니다.

 

※ Int 값 1개를 갖는 class로 복사/이동을 하니 모두 10억 번 기준으로 0.0000001(복사), 0.0000000(이동) 이처럼 엄청 빠른 시간을 나타내고 있고, 위 실험의 시간은 대부분 string 복사 시간이었다는 것을 유추할 수 있습니다.

 

 

참고로 x64, 64bit로도 진행했는데, 이때는 복사가 더 빨리 이루어져서 그런지 이만큼의 차이는 보이지 않았다. 그래도 10~20%의 성능 향상이 있었음.

 

5. 결과

이동 연산이 복사 연산보다 무조건 빠르다! 는 아니라고 했던 것 같다. 그래도 대부분의 상황에서 10~30% 정도의 속도 향상이 있다는 걸 보니 정말.. 이동 생성을 할 수 있다면 꼭 해야겠다고 느꼈다. 결과보고 정말 깜짝 놀란..

 

실험에 문제점이 있다면 언제든지 댓글 부탁드립니다.

댓글
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Total
Today
Yesterday