출처: 인터넷 상의 어딘가…

오랜 상식

기존(~2015)는 전치 증분(++value)와 후치 증가(value++)의 거동의 차이를 올바르게 완전히 이해한 후 전치 증가를 사용해야 한다 라는 것이 C++프로그래머의 기본 자세였다.

각 증가의 표준적인 구현은 아래와 같다.

 //전치 증가
T&
T(operator++() {
 
//여기에서 무엇인가의 증분 처리
 
return*this;//자신의 참조를 반환
}

//후치 증가
T
T(operator++(int) {
 T
old(*this); //증가 전에 복사한 후
 ++*
this;     // 전치 증가를 부른다
 
return old;  // 복사본을 반환
}

후치 증가는 한눈에 전치 보다 느림을 알 수 있다.

전치 증가가 증분 처리 후 단순히 자신의 참고를 반환 하는 것에 비해, 후치 증가에서는 증가 전에 일시 객체를 생성, 그리고 증가 후에는 그 전에 생성한 일시 객체를 값으로 반환 있다.

후치에서는 단순히 객체를 복사하는 만큼 보통으로 생각하면 ‘후치는 느리다’ 라는 것이 지금까지의 인식이었다.

"C++Coding Standards-101의 룰, 가이드 라인 베스트 프랙티스“ 중에서도 특히 후치 증가의 필연성이 없을 때는 망설이지 않고 전치 증가를 사용하는 것을 권장하고 있다.

새로운 주장

게임 엔진 아키텍처 2판" 속의 한 구절을 소개.

그러나 값이 사용되는 경우 CPU의 파이프라인에서 stall을 발생시키지 않으므로 포스트 증분 쪽이 우수하다. 그래서 예비 증분(전치 증가) 동작이 절대 필요한 경우를 제외하고 꼭 포스트 증가를 사용하는 습관을 익히는 것이 좋다.

__게임 엔진 아키텍처 2판 프리 인크리멘트 vs포스트 인크리멘트

기존 주장과 정반대의 주장이 전개된 것이다. 후치 증가가 우수하므로 가능한 한 그것을 써야 한다고 한다.

게임 엔진 아키텍처의 저자는 Uncharted와 The Last of Us의 개발원인 너티독의 엔지니어이다. 이런 그가 후치 증가를 사용해야 한다고 주장함에 큰 화제가 되었다.

후치 증가가 우수한 이유는

전치 증분은 값을 다시 쓰서 반환하므로 증분 처리가 끝날 때까지 반환 값이 결정되지 않는다.

그것은 데이터 의존성을 낳고, 깊은 파이프 라인의 CPU에서는 stall을 발생시킨다.

후치 증가는 증분 처리와 반환 값은 다른 인스턴스이므로 병렬로 처리가 가능.

따라서 stall의 발생이 없는 만큼 후치 증가가 우수.

CPU stall

예를 들면 계산 결과가 레지스터와 메모리에 올라오기 전에 그 레지스터나 메모리를 읽는다면 나중의 명령은 앞의 명령이 끝날 때까지 대기할 수밖에 없다.

이렇게 자동적으로 대기하도록 하는 것을 스톨이라고 하고, 스톨의 원인이 된 것을 하자드 라고 한다.

하자드에는 데이터 의존성 하자드와 구조 의존성 하자드가 있다.

좀 더 생각해 보자

앞으로는 후치 증가로 통일이다! 지금까지 쓴 전치 증가를 모두 치환하자! 라고 서두르기 전에 정말 게임 엔진 아키텍처의 주장이 옳은지는 일고의 여지가 있다.

깊은 파이프 라인에서의 stall 가능성은 데이터 의존성이 있는 한 발생할 수 있는 문제dl다.

이 부분의 주장은 옳다.

그러나 게임 엔진 아키텍처에서는 후치 증가에서 발생하는 복사 비용에 대해서 언급이 없다.

문맥을 해설 하면 "복제 비용 < stall 비용"이란 전제가 있는 것 같다. 이 증가 항 이외에도 게임 엔진 아키텍처에서는 stall에 대한 혐오감을 쓰고 있는 부분이 있어서 저자는 꽤 stall 비용에 나이브 한 것처럼 느껴진다.

실제로 콘솔 게임기의 아키텍처에는 stall의 비용이 늘수록 높은 것도 있다. PS2의 시대는 어쨌든 CPU의 가동 효율을 높이는 코드를 의식하지 않으면 렌더링 폴리곤이 배는 다르다는 상황이었다. XBOX360 때조차 씬 그래프(게임 장면의 렌더링을 효율적으로 관리하는 구조)를 만들어도 그 주사와 판정 비용이 비싸기 때문에 단순히 장면에 있는 모든 객체를 차례로 순서대로 렌더링 한 것이 빨랐다는 경험이 있다.

stall 비용 고찰

stall 이 발생하는 상황을 생각합니다.

stall 은 원래 이 증가뿐만 아니라 데이터 의존성을 가진 모든 상황에서 발생할 우려가 있다. 조건 분기 값의 갱신 등 결과를 기다리지 않으면 앞의 처리가 할 수 없는 경우가 발생할 때 stall 은 일어날 수 있다. 분기 예측 등 stall 이 실질적으로 없어지는 방안도 물론 하고 있지만, 예측이 빗나간 경우에는 사전 준비의 보람도 없이, 역시 stall 한다.

우선 증가 외에 증분 결과를 사용하는 순간에 발생하는 스톨에 대해서인데 단 일행

 ++value;

 

라고 쓸 경우 전치와 후치에서 아무것도 바뀌지 않는다.

다음 행 이후에서 사용 값은 증가 후의 수치이므로 전치/후치 관계 없이 값의 갱신이 완료되지 않으면 아무 것도 시작할 수 없다.

 

int main()
{  
 
int i=5;
 
while(i-)

  {
   
//어떤 처리
 }
   
 
return 0;
}

이 예의 경우 while문의 test expr에 들어간 단계에서 i의 내용이 확정되고 있어서 증가의 결과를 기다리지 않고 다음 처리를 수행할 수 있다. 이러한 상황에서는 전치와 후치에서의 stall에 따른 비용이 달라진다.

증가 속에서 발생하는 stall도에 대해서도 의식할 필요가 있다.

전술대로 stall은 어디든지 발생할 수 있다.

이 stall이 증분 연산 안에서 발생할 경우 증분 연산 때마다 CPU는 스톨한다. 그러나 후치 증가하면 증분 연산의 결과를 기다리지 않고 다음 처리로 옮길 수 있기 때문에 유리하다.

최악의 경우 증가 속에서 stall하고, 증가 밖에서 stall 한다며 stall 이 넘쳐난다.

이번 게임 엔진 아키텍처의 주장도 바로 후치 증가는 증가의 결과를 기다리지 않아도 반환 값의 객체는 결정하고 있으므로 그 객체에 대한 처리는 증가 동작과 병행 하므로 stall 하지 않는다 라는 것이다.

반대로 코드의 흐름 속에서 증가한 후의 결과만을 사용하지 않으면 전치에서도 후치에서도 CPU 비용은 바뀌지 않는다. 만일 증분 연산자 중 70,000 사이클의 처리가 들더라도 증분 후의 값만 사용하지 않으면 전치에서도 후치에서도 동일한 그 부하가 계상되게 된다.

전치/후치 증분 성능 비교

출처: http://resemblances.click3.org/?p=1828 

전형적인 for문을 전치/후치 각각의 증가를 사용하는 형태로 작성.

증분 대상을 unsigned int/iterator/ 적당하게 무거운 편인 클래스의 세가지 각각을 대상.

또 최적화에 의한 반환 값 계산이 사라지기를 생각한 for의 진위 분석에서 증가의 반환 값을 사용하는 것도 준비했다.

이상을 각각 100,000,000번 실행을 한 후 또 10번 반복해서 걸린 시간의 균형을 잡았다.

현실적인 관점

결론을 내기 전에 우리가 마주 하지 않으면 안 되는 현실적인 고려 사항은 4개가 있다.

우선 제1, 일시 객체의 검증 결과이지만 인라인 전개가 통하지 않는 전치 증분에게 어드밴티지가 있는 상황이라면 링크 시 최적화가 통하지 않는 컴파일러에서 사용자 정의 증분 오러레이터 연산자를 cpp쪽에 쓸 필요가 있다.

증분 연산자도 감소 연산자도 클래스를 쓸 때마다 반드시 준비한다(게다가 헤더 파일 말고 cpp 쪽으로!)라는 사람은 차치하고 일반적으로 가장 많이 호출되는 증분 연산자는 내장 int 또는 STL의 반복자에 대해서이지 않을까?

이 경우 역시 아까의 검증에서도 한 것처럼 컴파일러의 멋진 최적화로 전치와 후치의 비용 차이는 지극히 적게 된다.

 

제 2, 증분 연산에 의하여 발생하는 어떤 stall을 회피하려면 증분 전의 값을 사용하는 코드를 유념할 필요가 있다.

예를 들면 증분 연산자를 식 중에 사용하거나 증분 후도 증가 전의 값에 대한 처리를 쓰는 것들.

하지만 가독성 관점에서 본다면 그다지 권장되는 코드가 아니다.

제 3, stall 비용은 피부로 느끼기 어렵다는 문제가 있다.

처리에 직접 관계한 비용은 예를 들면 어셈블러 코드의 출력을 보면 파악할 수 있다.

여분의 개체가 생성되고 있구나, 최적화되고 있구나 라고.

그러나 어떤 코드를 쓸 때 그 부분의 처리에 의해서 CPU에 stall이 발생하느냐는 것은 매우 파악이 어려운 문제이다.

 

제 4, 하지만 그래도 아키텍처 관점에서는 생각해야 한다.

일부의 아키텍처에 대해서는 stall 발생을 억제하지 않으면 제대로 처리가 움직이지 않는다는 경우가 있는 것도 사실이다. 이러한 아키텍처와 마주 하지 않으면 안 된다면 전혀 일체 선택의 여지없이, 가독성 운운 이전에 방침을 정하지 않을 수 없는 경우도 있다.

 

CPU의 아키텍처에 대한 이해와 컴파일러의 움직임, 그리고 현실적인 코드를 쓰는 우리

이 3가지를 감안한 후에 실제로 전치 증가를 쓰느냐, 후치 증가를 쓰느냐, 또 지금의 시대 그런 건 신경 쓰지 않아야 하는지 생각해야 한다.

후치 증가를 쓰고 있음을 지적했을 때 "게임 엔진 아키텍처에 되어 있어서 그냥 사용하는거야"라는 대답을 하는 프로그래머가 늘지 않기 바란다.