티스토리 뷰

C++/General

BOOL을 쓰면 안되는 이유

엘키 2008. 1. 11. 14:29

첫째, 성능 문제, 지적하신 분들의 공통된 주장이 바로 bool은 BOOL에 비해 윈도우와 같은 32bit 환경에서 느리기 때문에 사용하지 않는다는 것입니다. bool이 BOOL보다 느리다죠. 맞습니다. 레지스터나 I/O 처리 등이 32bit 즉, 4bytes데이터에 최적화 되어 있기 때문에 char, unsigned char등과 같이 1byte 자료형은 4bytes size의 자료형에 비해 내부적으로 추가적인 처리가 필요합니다.

 

그런데 bool은 대개의 컴파일러에서 1byte로 처리를 하며 VC++에서도 int는 4bytes, bool은 1byte의 size를 가집니다. 따라서 bool은 BOOL(=int)에 비해 느립니다.

 

그러나 이러한 성능을 운운하는 것은 소위 말하는 '이른 최적화'라고 지적하고 싶습니다. 다시 말씀 드리자면 코딩 단계에서 감안할 필요가 없는 쓸데없는 속도 최적화라는 것입니다.

 

과연 int와 bool의 대입 연산이 얼마나 속도에서 차이가 나는지 확인하기 위해 간단한 테스트 소스를 아래와 같이 만들었습니다.

 

//int func()

bool func()

{

  int x = rand() - rand();  /// 2)

 

  if(x > 0)

    return true;// return 1;

 

  else

    return false;// return 0;

}

 

 

 

int main()

{

 

  //int ret;

  bool ret; 

  srand(time(NULL));

  for (int i = 0; i < 1000000; ++i)

  {

    ret = func();  /// 1)

  }

  return 0;

}

 

 

위의 코드를 프로파일러(DevPartner의 performance analysis)를 이용하여 func()함수의 리턴값을 ret변수에 할당하는데 걸리는 시간을 측정하였습니다.(참고로 VC++6.0을 사용하였고 제 컴퓨터의 사양은 pentium4 1.8Ghz입니다. 물론 최적화 옵션은 모두 꺼놓은 상태입니다.)

 

이 때 1)구분의 실행 시간이 func()및 ret을 int형으로 했을 경우 약7,344msec이 나왔고 bool형으로 했을 경우 약7,355msec이 나왔습니다. 약 11msec 정도 int가 빠르게 나옵니다. 그런데 여기서 이 1)구문의 실행 시간 대부분은 2) 구문에서 발생한다는 사실을 쉽게 추측할 수 있으며 실제로 프로파일링 결과도 그렇습니다.(즉, 11msec의 차이는 대부분 rand()함수 수행 시 발생된 오차 범위에 해당한다는 뜻입니다.)그렇다면 결국 bool과 int의 차이는 1백만 번 정도의 수행에도 큰 영향이 없다는 사실을 알 수 있습니다.

 

사실 이런 테스트의 필요성조차 느낄 필요없이 성능을 이유로 실제 목적에 맞지 않는 데이터 형을 이용한다는 자체가 넌센스입니다. 다소 비약적인 예를 들자면, 참,거짓을 나타내야 하는 부분에서 성능 때문에 bool 대신 int를 사용해야 한다는 주장은 부동 소수점 값이 필요한 부분에서 성능 때문에 double 대신 int를 써야 한다는 주장과 다를 바가 없습니다.

 

정말로 중요한 것은 bool 대신 int를 써야 할 정도로 최적화를 해야 한다면 C++를 사용해서는 안됩니다. 왜냐하면 C++의 객체들은 그런 관점에서 본다면 정말로 느려 터지기 짝이 없는 것들이기 때문입니다.

 

세상에 이놈의 객체라는 것들은 메모리 할당하는 시간도 아까워 죽겠는데 거기에다가 시키지도 않은 생성자 함수를 호출하는 여유까지 부리는 나쁜 놈들이기 때문입니다. 특히나 객체를 배열로 선언하는 경우에는 정말 어처구니 없게도 C는 구조체 배열을 초기화할 때 memset을 사용하면 깔끔하게 끝나는 것을 이놈의 C++는 각 객체 별로 똑같은 생성자 함수를 반복해서 호출 해줘야 합니다.(여기서 혹시 C++에서도 그냥 memset사용하면 되는데...라고 하실 분들이 계실까 봐 언급하고 넘어가자면 클래스에 memset(), memcpy() 등을 사용하는 것은 비 표준 행위입니다.) 따라서 배열의 사이즈에 따라 몇십 배에서 심하게는 몇 천, 몇만 배 느려지고 맙니다. 당연히 STL의 vector나 string은 감히 꿈도 꾸면 안됩니다.(vector가 속도가 빠르다고 하지만 당연히 배열보다는 느립니다. 그리고 string은 그런 vector보다도 느립니다.) 모든 것은 배열을 이용해야 하며 생성자 대신 memcpy(), memset()등을 이용해야 합니다. 그리고 스트림 객체 대신 printf()나 sprintf()등을 사용해야 합니다. 대신 C++를 통해 우리가 얻게 되는 많은 장점들, 특히 안정성은 상당 부분 희생을 해야 합니다. 스택 오버 플로우의 위험 속에서 맘을 졸여야 하며 printf()같은 가변 인자 함수를 사용할 때면 언제나 잘못된 타입의 인자를 실수로 넣었을 경우 발생하게 되는 오류 사항에 대해 전적으로 책임져야 합니다.

 

만약 그런 불안감을 감수하면서까지 성능에 연연하고 싶지 않다고 하신다면 그냥 C++를 사용하면 됩니다. 배열 대신 vector를 사용하면 되며 printf(), sprintf() 대신 stream 객체를 사용하고 BOOL 대신 bool을 사용하면 됩니다.

 

 

 

둘째, Win32 API는 애초에 bool형이 없었기 때문에 BOOL이라는 재정의 타입을 만든 것이지 성능이나 기타 다른 장점이 있어서 bool 대신 int를 BOOL로 재정의하여 사용한 것이 아닙니다. 만약 bool 형이 C에도 존재했다면 당연히 bool을 이용했을 것입니다.

 

마지막으로, 또 다른 분께서 BOOL 반환 타입을 갖는 API들은 모두 TRUE/FALSE를 반환하도록 되어 있다고 하셨는데...그런 내용이 어디에 나와 있는지 알려 주시면 감사하겠습니다. 제가 본 어느 Win32 API 관련 문서에도 BOOL형 반환 타입 API의 리턴 값은 참이면 nonzero, 거짓이면 zero이다 라고 되어 있지 TRUE/FALSE를 반환한다고 되어 있지 않습니다.

 

혹시 경험상 지금까지 사용해본 BOOL반환형 API들은 다 참이면 1(TRUE)를 리턴 하더라...라고 하신다면 맞습니다. 저 역시 제가 사용해본 BOOL반환형 API들은 모두 참이면 1, 거짓이면 0을 리턴 했습니다. 따라서 BOOL반환형 API들은 모두 성공 시 TRUE를 리턴 합니다. 그리고 한 가지 덧붙이자면 제가 사용해 본 컴파일러들은 모두 int의 데이터 사이즈가 4bytes였고 long 역시 4bytes였습니다. 그러니 sizeof(int) == sizeof(long)이라고 주장해도 되겠습니까?(참고로 표준에서는 long이 최소한 int보다 크거나 같아야 한다 라고 규정되어 있습니다.)

 

제가 다소 비야냥이 섞인 표현을 한 이유는 그런 주장(경험상 그러하더라...라고 하는 주장)은 프로그래머가 절대적으로 피해야 할 사고 방식이기 때문입니다.(물론 저도 그런 실수를 많이 합니다. 그래서 디버깅하느라 야근을 많이 했습니다.)

 

극단적으로 말해서 현존하는 모든 BOOL반환형 API를 모두 테스트해보았더니 모두 참일 때 TRUE를 반환하더라...라고 하더라도 "참이면 TRUE를 반환한다."라고 주장하면 안됩니다...앞으로 나올 API들이 TRUE가 아닐 수도 있으며 혹시 테스트한 윈도우 버전 외에 다른 버전에서는 TRUE가 아닐 수 있기 때문입니다. 결국 MS에서 정책적으로 BOOL 반환 API들은 참이면 TRUE를 반환한다 라고 규정하지 않는 이상(그리고 그러한 사실을 MSDN과 같은 개발자용 문서에 문서화하지 않는 이상)에는 우리 같은 소시민 프로그래머들은 TRUE라고 주장해서는 안 된다는 것입니다.

 

설사 MS에서 TRUE를 반환한다 라고 규정하더라도 BOOL은 데이터 형 자체가 TRUE/FALSE외의 값을 가질 수 있기 때문에 문제가 여전히 남아 있습니다. 하지만 이건 C함수의 어쩔 수 없는 한계이기 때문에 기존에 이미 만들어진 Win32API는 별 수 없지만 적어도 VC++에서 윈도우 프로그래밍을 할 때 프로그래머가 직접 만드는 함수에서는 BOOL을 사용해서는 안되며 더 나아가 기존의 API들을 우리가 C++에서 다룰 때는 이런 불안정성을 제거하는 의미에서 bool 변수로 받도록 처리하자는 것이 제 주장입니다. 비록 bool 변수로 BOOL값을 받게 되면 컴파일러 경고가 발생할 수 있지만 이런 경우는 아래와 같이 해당 경고를 제거할 수 있습니다.

 

bool ret = FALSE != IsWindow(hWnd); /// 여기서 TRUE == IsWindow(hWnd)하면                                           /// 안됩니다!!!

 

그리고 이런 경고는 사실 우리에게 우리의 의도가 혹시 잘못된 것은 아닌지 다시 한번 상기시켜주는 고마운 역할을 합니다.

 

어쨋든 BOOL을 다룰 때는 TRUE가 있다는 것을 머리 속에서 지우시기 바랍니다. (단지 FALSE와 FALSE가 아닌 것들이 있을 뿐입니다...)

 

그런데 여기서 또 한가지 결정적인 문제점이 하나 있으니 바로 MFC의 수많은 멤버 함수들이 BOOL을 사용한다는 사실입니다...이 점은 저도 심히 유감스럽지만 그렇다고 그 수많은 가상 함수들을 일일이 어찌 처리할 수 없기 때문에 눈물을 머금고(사실 눈물을 머금지는 않고 그냥 그러려니 하고) 그냥 사용합니다. 사실 MFC가 훌륭한 클래스 라이브러리이긴 하지만 워낙 오래된 것들이고 C함수들을 랩핑하여 처리하기 때문에 최근의 훌륭한 C++ 코딩 스타일 및 권고사항에 반하는 것들이 많이 있습니다. 어찌됐든 저도 MFC가 생성하는 함수들의 리턴 값까지 어떻게 별도로 처리하지는 않습니다. 때문에 앞선 글에서 절대 BOOL을 사용하지 말라는 제 주장은 까막님의 말씀처럼 "가급적 사용하지 말라" 혹은 "적어도 우리가 직접 만들어야 하는 함수에서는 절대 BOOL을 사용하지 말자" 라고 수정해야 할 것 같습니다.

 

게다가 "BOOL을 절대 사용하지 말자"라고 주장한 이전 글을 쓴 직후에 제가 프로그래밍을 하면서 RasGetEntryDialParams()라는 API를 사용하게 되었는데 공교롭게도 그 API의 세 번째 파라미터가 BOOL*를 받도록 되어 있었습니다. 하지만 파라미터의 의미상 int를 사용하면 뭔가 어색하고 게다가 MSDN에 해당 파라미터는 TRUE아니면 FALSE를 받게 되어 있다라고 명시되어 있었기 때문에 그냥 BOOL을 사용하였습니다.

 

어찌 쓰다 보니 글이 너무 길어졌습니다. 성급한 결론을 내리지는 않겠습니다. 다만 지금까지 특별한 이유 없이 BOOL을 사용하셨다면, 혹은 unsigned char를 써야 할 곳에 아무 생각없이 signed char를 사용했다면, 무심코 char 배열에 데이터를 집어 넣기 위해 sprintf()나 strcpy() 등을 사용하셨다면 제 글을 읽고 한번쯤 방어적인 코드가 어떤 것인가 에 대해서 생각하는 계기가 되셨으면 합니다.

'C++ > General' 카테고리의 다른 글

오류의 원인  (0) 2008.01.14
윈도우 가상 키 코드(Virtual Key Code)테이블  (0) 2008.01.12
TCHAR to the rescue!  (0) 2008.01.11
static을 이해하자  (3) 2008.01.10
오버 라이딩 한 함수의 호출 문제  (0) 2008.01.10
댓글