C++ 개발자들 사이에서 전위 증감(++i)과 후위 증감(i++)의 차이에 대해 흔히 알려진 몇 가지 통념이 있습니다. 가장 대표적인 것은 다음과 같습니다.
++i가i++보다 성능상 우위에 있다.i++는 내부적으로 임시 객체를 생성한 뒤 증가시키고 기존 값을 반환하는 반면,++i는 참조를 반환하므로 효율적이다.sizeof연산자의 처리 방식에 대한 오해.
이러한 주장들이 실제 어셈블리 수준에서 어떻게 구현되는지 Win10 x86 환경을 기준으로 분석해 보겠습니다.
1. 단독 증감 연산 시의 비교
단순히 변수 값을 1 증가시키는 코드에서 두 연산자의 차이를 확인해 봅니다.
int val = 0;
val = sizeof(val);
val++;
++val;
위 코드의 어셈블리 출력 결과는 다음과 같습니다.
// int val = 0;
mov dword ptr [val], 0
// val = sizeof(val);
mov dword ptr [val], 4
// val++;
mov eax, dword ptr [val]
add eax, 1
mov dword ptr [val], eax
// ++val;
mov eax, dword ptr [val]
add eax, 1
mov dword ptr [val], eax
여기서 주목할 점은 sizeof의 처리입니다. 어셈블리 코드를 보면 함수 호출(call) 없이 즉시 4라는 상수가 대입됩니다. 이는 sizeof가 컴파일 타임에 결정되는 연산자임을 보여줍니다.
또한, 단독으로 사용된 val++와 ++val의 어셈블리 명령어는 완전히 동일합니다. 따라서 기본 자료형을 단독으로 증감시킬 때는 성능 차이가 전혀 없음을 알 수 있습니다.
2. 수식에 참여하는 경우의 분석
결과값을 다른 변수에 할당하는 상황을 가정해 보겠습니다.
int data = 0;
data = sizeof(data);
int result_a = data++;
int result_b = ++data;
이 경우의 어셈블리는 다음과 같이 차이가 발생합니다.
// int result_a = data++;
mov eax, dword ptr [data] // 현재 data 값을 eax에 복사
mov dword ptr [result_a], eax // 복사된 값을 result_a에 할당
mov ecx, dword ptr [data]
add ecx, 1
mov dword ptr [data], ecx // data 값을 1 증가
// int result_b = ++data;
mov eax, dword ptr [data]
add eax, 1
mov dword ptr [data], eax // data 값을 먼저 1 증가
mov ecx, dword ptr [data]
mov dword ptr [result_b], ecx // 증가된 값을 result_b에 할당
사용되는 레지스터의 순서만 다를 뿐, 실행되는 명령어의 총량은 비슷합니다. 후위 연산(i++)이 임시 변수를 스택에 생성한다는 통념과 달리, 컴파일러는 레지스터를 활용하여 효율적으로 값을 전달합니다.
3. L-value와 R-value의 특성
C++에서 ++i = 5;는 컴파일되지만 i++ = 5;는 오류를 발생시킵니다. 이는 전위 연산이 증가된 변수 자체(L-value)를 반환하는 반면, 후위 연산은 증가 전의 값(R-value)을 반환하기 때문입니다.
만약 후위 연산이 대입 연산이 가능하다고 가정한다면, 어셈블리 상에서는 특정 레지스터에 값을 넣은 뒤 그 레지스터가 연산되는 식의 무의미한 동작이 발생하게 될 것입니다. 컴파일러는 이러한 논리적 모순과 무의미한 연산을 방지하기 위해 이를 문법적으로 차단합니다.
4. 클래스 멤버 및 연산자 오버로딩
사용자 정의 클래스에서 연산자를 오버로딩하는 경우를 살펴봅니다.
class Counter {
int count = 10;
public:
void operator++(); // 전위
void operator++(int); // 후위
};
void Counter::operator++() {
count++;
}
void Counter::operator++(int) {
++count;
}
이 코드에 대한 어셈블리는 다음과 같습니다.
// operator++() 내의 count++;
mov eax, dword ptr [this]
mov ecx, dword ptr [eax]
add ecx, 1
mov edx, dword ptr [this]
mov dword ptr [edx], ecx
// operator++(int) 내의 ++count;
mov eax, dword ptr [this]
mov ecx, dword ptr [eax]
add ecx, 1
mov edx, dword ptr [this]
mov dword ptr [edx], ecx
사용자 정의 클래스에서도 내부적으로 멤버 변수를 다룰 때 추가적인 임시 객체 생성 없이 레지스터를 통해 직접 연산이 이루어짐을 확인할 수 있습니다. "클래스 객체에서는 무조건 전위 연산자가 유리하다"는 속설은 복잡한 객체 복사 생성자가 호출되는 특수한 상황에서 기인한 것이며, 현대적인 컴파일러 최적화 환경에서는 그 차이가 미미하거나 없는 경우가 많습니다.
결론적으로, 기본 타입의 경우 두 연산자의 성능 차이는 없으며, 어셈블리 수준에서 프로그램은 불필요한 부하를 임의로 추가하지 않습니다.