C/C++의 예외 모델 차이

try { throw } catch {} & do { break } while (false) 에서 트랙백. C++의 예외 처리가 느리기 때문에 do while을 이용한 코드 블럭 탈출 / setjmp/longjmp를 이용한 함수간 예외 전달을 다뤘길래 문제가 있다고 생각하는 점을 지적하려고 포스팅.

C에서는 예외의 발생을 일반적으로 에러 번호를 통해서 전파하며, 특정 블럭의 실행을 끝내기 위해서

do
{
// do some thing
} while ( 0 );

같은 코드 블럭에서 break 문을 사용하는 방법을 많이 쓴다. (goto문과 비슷하지만 label 문제도 없고 상대적으로 보기가 쉬우면서도 C++에서 쓰기도 편하다)  혹은 setjmp / longjmp라는 어떤 의미에서는 악마의 자식(…)을 사용해도 된다. 이건 진정한 의미†의 goto문이기 때문에 C 에서도 매우 주의 깊게 사용해야하고 – 꼭 필요한지 3번은 되묻자 – C++에서는 사실상 사용이 금지 된다.

C++의 경우에는 예외적인 경우가 발생했을 때 말 그대로 예외exception을 생성시키고 이를 던진다throw. 문제는 C++의 경우 C 보다 강력한 표현력을 제공하고 OOP를 지원하기 위한 장치들이 들어가 있다는 것인데 가장 중요한 내용은 바로 stack-unwinding이라고 부르는 현상을 일으킨다는 것이다.(이것 때문에 많이 느려질 수 밖에 없다)

바로 예외가 블럭 스코프를 벗어날 때 해당 블럭에서 생성했던 – auto, register로 선언한 변수 – 들의 소멸자를 일일이 호출해주는 것인데, 이를 위해서 C++  컴파일러는 예외가 생길 수 있는 부분마다 이런 변수들의 위치와 소멸자를 추적해줘야한다. 그렇기 때문에 예외가 발생하게 되면 이런 정보를 토대로 일일이 변수를 찾아서 소멸자를 호출해주게 되는데, 이 과정은 "예외는 정말 가끔 일어나는 경우다" 라는 가정하에 그다지 최적화되지도 않고해서 굉장히 느리다.

트랙백한 글에서 이를 비교해서 문제 삼고 있다.(사실 문제인게 맞다) 그렇지만 do { code block } while( 0 ); 와는 달리 예외는 좀 더 단순하고 생각하기 쉬운 제어 경로를 보여주고, setjmp/longjmp의 문제를 안 일으킨다. setjmp/longjmp를 사용하는 경우 말 그대로 실행 경로만 바뀌기 때문에 stack 위에 쌓여있을 변수들이 소멸되지 않아서(…), 동적으로 할당했거나 혹은 추가한 참조등에 대해서 처리가 되지 않는다. 이런 코드가 있다고 치자.

{
    scope_based_lock( lock_name );// scope를 벗어나면 소멸자에서 lock을 해제한다.

    if( exception_condition )   longjmp( jmp_buf_var );
}

이런 코드인 경우에 longjmp는 재앙이다. 특정 리소스에 대한 lock이 해제되지 않는다. 그렇기 때문에 – 이 경우 말고도 메모리 할당 / 스마트 포인터의 참조 포인터 처리 등 소멸자에서 처리하는 부분이 있으면 – 여러가지 문제로 인해서 C++에서는 longjmp의 사용이 사실상 금지된다.

반면에 저 상황에서 longjmp가 아니라 예외를 던졌다면, stack-unwinding에 의해 소멸자가 적절히 호출되고 리소스 lock이 해지되지 않는 일은 발생하지 않는다. 예외를 주의깊게 정말 예외로 처리해야할 상황 에서 썼다면 예외 처리로 인한 속도 문제는 큰 무언가가 아니다. 암달의 법칙을 기억하라. 실제로 예외가 발생해서 해당 부분의 코드가 사용되는 시간은 실제 가동 시간의 아주아주아주아주 작은 일부분이다.  그러니 안심하고 C++ 친화적인 예외exception을 사용하자.

*

† setjmp로 특정 위치를 기록하고, longjmp를 호출하면 setjmp를 호출했던 장소로 즉시 이동한다. 같은 함수 혹은 스코프 안에서 setjmp/longjmp가 불릴 필요가 없기 때문에 함수 간에도 이동이 가능하다(…).

Published by

rein

나는 ...

6 thoughts on “C/C++의 예외 모델 차이”

  1. Pingback: Purewell.BIZ
  2. 궁금해서 테스트해 봤음. throw-catch를 실제로 거치는 것만큼 흠좀무하게 느린 건 아닌데, 그래도 try{} 안으로 들어갔다 나오기만 해도 약간 부하가 있네.

    ‘서버에서 패킷 하나 받을 때마다는 참을만하지만 이미지 프로세싱 할 때 도트 하나마다 쓰기에는 너무 무거울듯한’ 느낌

  3. 뭐 그건 어쩔 수 없지. Activation record에다가 추가로 뭔갈 더 박는다고 했으니까.(근데 구현마다 다르겠지? -_-;; )

    실제로 내가 짜는 것 중에 하나는 패킷 받을 때마다는 좀 큰 단위에서 try & catch 하고있긴한데 별로 느려지는 것 같지 않더라. 결정적으로 그게 예외 자체가 바이너리 미스맷치 때 빼고는 일어난 적이(…)

  4. 전 처음 배울 때부터 “setjmp 등은 있다는 것만 알아두고, 웬만한 에러 체크는 직접 해라. 정말 안될 것 같은 것만 throw – catch로 쓰라.” 고 배워서 왜 그런지는 전혀 생각해 본 적이 없었는데 – 이제 알게 되네요 ;ㅁ;

  5. 근데 C 쪽에서는 좀 쓰긴하더라. 예전에 VNAP만들때 PNG변환 코드를 짜야했는데, 예제 코드에서 메모리 할당처리 할 때 setjmp로 할당 전에 셋팅해놓고 할당하다 실패하면 되돌아가서 다 해제하고 에러 반환하더라.

Leave a Reply