printf vs. cout (수정 및 추가) by object

일러두기: 이 실험 결과는 Visual C++에서 컴파일하고 (즉, printf, cout의 구현이 MSVC의 구현) 윈도우에서 돌렸을 때의 실험 결과이다. 다른 운영체제나 다른 라이브러리를 사용하면 결과는 사뭇 다를 수 있다. 예로, 리눅스에서 gcc로 컴파일했을 때는 cout과 printf의 차이가 지금 소개된 결과에 비해 훨씬 적은 차이만을 보이기도 한다. 경우에 따라 cout이 빠를 때도 있었다. 그러나 내가 해본 실험 결과에서는 조금이라도 printf 쪽이 빠르기는 했다.


  1. cout/printf 생성 코드 비교: http://minjang.egloos.com/1888099
  2. 64비트 정수 출력시 주의할 점: http://minjang.egloos.com/1925078

심심해서 직접 printf와 cout의 수행속도를 비교해봤다. 매우 정확하게 하려면 CPU 시뮬레이터에다 넣어서 싸이클 단위로 측정할 수도 있는데, 그건 너무 귀찮어서 다른 방법으로 테스트했다. 동일한 내용을 찍는 printf, cout 프로그램을 만들고 이 프로그램이 실행될 때 얼마나 많은 인스트럭션들이 실행되는지를 조사해봤다. 가정은 실행된 인스트럭션이 많으면 수행시간도 길다라는 것인데, x86 한 인스터럭션의 레이턴시는 제각각이어서 이런 가정이 완벽히 옳은 것은 아니다. 그래서 메모리 읽고 쓰기도 몇 번이나 일어났는지도 카운트 해봤다. 그래도 정수 나눗셈이나 부동소수점 계산이 있는 것이 아니므로 “실행된 명령어의 개수 = 수행 시간” 이라 봐도 큰 문제는 없다.

단순히 컴파일러가 printf, cout 을 호출하기 위해 생성한 코드의 크기만 비교하는 것은 수행 속도를 가늠하기에는 불충분하다. 이건 순전히 정적인 코드 크기이고, 속도를 가늠하려면 실제 런타임 때, printf, cout 내부에서도 얼마나 많은 코드가 수행이 되는지를 알아야만 한다. printf 같은 경우 내부에서 큰 루프를 돌면서 타입을 파싱하는 작업을 하기 때문에 호출 코드 양은 적어도 출력하는데는 많은 명령이 필요할 수 있기 때문이다. 테스트를 위해서 인텔에서 만든 PIN이라는 dynamic binary instrumentation 툴을 이용하였다. PIN 킹왕짱이라는…

Test1: C++ cout

  • 타입을 신경 쓰지 않아도 되므로 버그를 줄일 수 있다.
  • 버퍼오버플로우와 같은 문제도 별로 없다.
  • C++ 스트림의 여러 막강한 기능들을 쓸 수 있다.
  • 호출하는데 필요한 코드양이 많다.
  • 수행시간도 좀 느릴 것 같다.

Test2: C/C++ printf

  • C언어라 어디서도 사용이 가능하다.
  • 호출하는데 필요한 코드양이 적다.
  • 타입 맞추기가 힘들다. 플랫폼마다 상이한 printf에 따라 %?가 바뀔 수 있다.
  • 버퍼오버플로우 등 보안취약점이 있다.
  • 수행시간이 좀 더 빠를 것 같다.

x86 플랫폼이고 컴파일러는 VC++ 2008 최적화 옵션을 두고 테스트 했다. 정확하게 말해 완전히 같은 내용을 출력해주는 코드는 아니다. 부동소숫점의 프리시전을 조절하는 것이 까다로워 대충 코딩했다.

#include <stdio.h>
#include <iostream>
#include <iomanip>
#include <windows.h>
using namespace std;

int a = 1999;
char b = 'a';
unsigned int c = 4200000000;
long long int d = 987654321098765;
long long unsigned int e = 1234567890123456789;
float f = 3123.4578f;
double g = 3.141592654;

extern "C" void Test1()
{
// 기존에 각 행마다 endl로 한 것을 마지막 제외 “\n”로 바꿈
cout
<< "a:" << a << “\n”
<< "a:" << setfill('0') << setw(8) << a << “\n”
<< "b:" << b << “\n”
<< "c:" << c << “\n”
<< "d:" << d << “\n”
<< "e:" << e << “\n”
<< "f:" << setprecision(6) << f << “\n”
<< "g:" << setprecision(10) << g << endl;
}

extern "C" void Test2()
{
fprintf(stdout,
"a:%d\n"
"a:%08d\n"
"b:%c\n"
"c:%u\n"
"d:%I64d\n"
"e:%I64u\n"
"f:%.2f\n"
"g:%.9lf\n",
a, a, b, c, d, e, f, g);
fflush(stdout);
}

int main()
{
//DWORD A, B;
//DWORD start = GetTickCount();
//for (int i = 0; i < 10000; ++i)
// Test1();
//A = GetTickCount() - start;

//start = GetTickCount();
//for (int i = 0; i < 10000; ++i)
// Test2();
//B = GetTickCount() - start;
//
//cerr << A << endl;
//cerr << B << endl;
return 0;
}

완벽하게 하려면 Test1, Test2 함수 내의 명령어 개수만 헤아려야 한다. 그런데 그게 좀 까다로와 전체 프로그램의 명령어 개수를 헤아렸다. 그리고 텅빈 main 함수를 가진 프로그램의 명령어 개수를 헤아려 대충 가늠했다. 두 액체의 무게를 비교하는데 용기 무게를 빼는 것과 같은 이치.

 

결과

1.1. Test1: C++ cout을 한 번 수행했을 때:

  • 실행된 총 명령어 개수: 106,743개
  • 그 중에서 메모리 읽기 33회, 쓰기 99회
  • Test1 함수 내의 명렁어 개수 (즉, 호출하는데 들어간 정적인 명령어 개수): 168개

1.2. Test2: C/C++ printf를 한 번 수행했을 때:

  • 실행된 총 명령어 개수: 17,274개 (1.1.에 비해 약 6배)
  • 그 중에서 메모리 읽기 8회, 쓰기 19
  • Test2 함수 내의 명렁어 개수: 33개

그리고 Test1과 Test2를 각각 10,000번씩 루프 돌렸을 때도 테스트 해봤다. 이 경우는 for를 수행하는데 필요한 오버헤드도 고려된 경우.

2.1. Test1*10,000 결과:

  • 실행된 총 명령어 개수: 423,234,439개 (결과 1.1.의 3900배)
  • 메모리 읽기/쓰기: 320,000, 980,000회 (약 9700, 9900배)
  • 호출하는데 필요한 명령어 개수: 1,640,000개 (약 9800배)
  • 수행시간: 52초

2.2. Test2*10,000 결과:

  • 실행된 총 명령어 개수: 164,800,800개 (결과 1.2.의 9500배, 2.1에 비하면 약 2.6배)
  • 메모리 읽기/쓰기: 70,000, 180,000회 (약 8700, 9500배)
  • 호출하는데 필요한 명령어 개수: 270,000개 (약 8100배)
  • 수행시간: 13초 (약 4배 빠름)

Discussion

생각보다 그 차이는 심했다. 컴파일러 최적화를 했지만 함수 한번 호출하는데 필요한 명령어 개수는 C++ cout 프로그램이 printf보다 6배 정도 더 필요했다. 호출하는데 필요한 명령어 개수도 많은 연산자 오버로딩 코드로 인해 많음이 당연하고 (약 5배), 역시 많은 메모리 연산과 명령어가 필요했다 (약 5배).

그런데 이 함수들을 각각 1만번씩 돌렸을 때는 재밌는 결과가 나왔다. cout의 비용이 printf에 비해 약 2.6배 정도만 많았다. 한 번 호출할 때 들었던 6배의 비용에 비하면 많이 줄었다. 2.2.을 보면 printf 함수는 1만 번 반복 시 수행된 명령 개수가 한 번 호출하는데 들어간 비용의 1만배에 근접하게 나온다. 반면, cout은 고작 4천배 밖에 되지 않았다. 의아해서 cout 함수를 100번을 돌려서 비교해보니, 약 40배가 나왔다. 1만 번 반복시 4천배와 비교하면 정확하게 루프 반복 회수에 비례했다. 당연히 이렇게 나와야 하는 결과.

따라서 cout은 한 번 호출하는데 필요한 고정 비용이 상당히 크다는 것을 알 수 있다. printf/cout의 루프 횟수에 따른 명령어 개수 그래프를 그리면, 기울기는 2.6배 차이가 나고, y 절편은 약 6배 차이나는 그래프가 그려질 것이다. 참고로 루프를 관리하는데 필요한 명령의 개수는 미미하다. 최적화 없이 ++을 1만번 수행하는 루프는 약 10만개의 명령어를 실행한다. 결과 2에서 나온 명령어 개수들이 약 억 단위임을 보면 무시해도 별 문제 없음.

마지막으로 만번 반복 호출 시 수행시간을 측정해보면 printf가 약 3~4배 빠름을 알 수 있다. 실행된 명령어 개수 차이 보다는 실제 차이가 좀 더 남을 알 수 있다.

요약하면 한 번 호출하는데 필요한 cout의 비용은 printf에 비해 6배이나 호출이 반복될 때는 그 차이가 줄었다. 그렇다고 하더라도, 위 printf/cout과 같이 복잡한 내용을 출력할 경우에는 수행하는데 필요한 명령어 개수는 cout이 약 2.5배 이상 많이 들었다.

따라서 각자 잘 알아서 적절히 printf와 cout을 사용할 것.


핑백

  • 미친병아리가 삐약삐약 : 2008년 06월 23일.. 2008-06-23 17:30:20 #

    ... 정도의 능력을 가지고 있을까? Open Source 기반의 차트 프로그램 여~ 좋은 프로그램들 많구만.. 특히 PHP/SWF Charts, 이거 아주 맘에든다.. printf vs. cout (수정 및 추가) 세상에 공짜는 없다.. 편한만큼 다른 것을 희생하게 된다.. 요즘 컴퓨터는 성능이 좋아 이 정도 희생은 가능하다.. 하지만, 정말로 성능이 중요한 경우에는 ... more

  • art.oriented : 네, 정말 C는 C++보다 빠릅니다. 2008-07-12 09:01:18 #

    ... 문은 CPU에서 처리하기 힘들어 성능 저하에 주요 요소가 될 수 있기 때문에 그렇게 각종 매크로로 메세지 맵을 처리한 것이다. 세상에 공짜는 없는 법이다. printf와 cout을 비교한 글에서도 비용과 편의성의 tradeoff를 잘 살펴보았다. 버추얼 함수도 이런 것이다. 최종 결론:가상 함수의 비용을 잘 이해하고 적절히 이용하자. ... more

  • art.oriented : 스택오버플로우: 새로운 개발자 커뮤니티의 대안 2009-12-03 21:35:05 #

    ... 1. printf vs. cout: 이 글에는 매우 멍청한 오류가 있었다. 저 글에서 나는 printf가 cout보다 몇 배 빠르다고 주장하였다. 그런데 저 실험 데이터는 오직 윈도우/Visual ... more

  • art.oriented : VC++ STL의 멍청한(?) string 2010-02-27 16:46:28 #

    ... 까?혹시 저의 궁금증에 명쾌한 설명을 해주실 분이 계신가요? 그리고 다른 STL 구현의 string은 어떻게 행동하는지 알려주시면 대단히 고맙겠습니다. 안 그래도 VC++의 C++ cout은 무지하게 느리고 hash_map 구현도 매우 멍청한데 string 까지 나를 실망 시킬 것인가… 보너스 비슷한 이야기로 위 그림에서 ca, cb는 같은 내용의 스 ... more

덧글

  • Hide_D 2008/06/15 22:51 # 답글

    유연성과 강력한 기능의 반대 급부로군요
  • nVec 2008/06/15 23:27 # 답글

    엄청난 차이네요. 시간상으로만 봐도 5배나 차이가 나다니..
    그런데 궁금한게 있는데요 endl과 n의 동작이 동일하게 수행되나요?
  • 네글자군 2008/06/16 00:49 # 삭제 답글

    흥미로운글 잘 보고 갑니다 ^^;
  • jong10 2008/06/16 01:55 # 삭제 답글

    // nVec님, endl은 flush 기능도 추가되지 않나요??
  • object 2008/06/16 04:29 # 답글

    endl은 코드를 살펴보니 flush가 추가되어있네요.

    원래 내용을 조금 보강했습니다. 1회 호출은 약 6배 정도 차이나지만 1만번 반복했을 때는 명령어 개수는 2.6배 정도로 차이가 주네요.
  • rein 2008/06/16 11:41 # 삭제 답글

    차이가 도저희 무시할 수 없는 수준이네요 -_-;;;

    cout -> (restricted/optimized) cout or printf 같은 코드 생성기가 있으면 좋겠네요;;;
  • 방준영 2008/06/16 13:44 # 삭제 답글

    전통적인 cout 스타일의 단점을 하나 더 추가하면, 메시지 국제화가 엄청나게 힘들어집니다. 예를 들어 cout을 쓴 프로그램의 메시지를 국제화하려면

    #if defined(EN_US)
    cout << "My name is " << myName << endl;
    #elif defined(KO_KR)
    cout << "내 이름은 " << myName << "입니다" << endl;
    #elif defined(JA_JP)
    ...
    #endif

    처럼 되는데, 메시지가 많아질 수록 코드도 비례해서 지저분해집니다(ㅌ.ㅌ). 오히려 printf()를 쓰면 gettext 툴로 깔끔하게 문자열을 추출하고 관리할 수 있어서 국제화가 훨씬 용이한 장점이 있습니다.

    cout의 유연성을 유지하면서 국제화까지 고려한다면 boost::format()을 사용할 수 있습니다.
  • object 2008/06/16 22:06 # 답글

    보통 윈도우 프로그래밍할 땐 대부분이 printf 스타일이라서 리소스 파일에서 스트링 읽어와서 손 쉽게 처리가 되었는데 cout은 다 파편화 되어있으니 문제가 좀 더 심각해지겠군요.
  • felucca 2008/06/17 00:14 # 삭제 답글

    심심해서 직접 printf와 cout의 수행속도를 비교해봤다 => ... 심심한데 왜 저런 걸 하는거에요 ㅋㅋ
  • object 2008/06/17 04:12 # 답글

    정말 심심해서....
  • nagne 2008/06/17 23:12 # 삭제 답글

    예전에는 정말 빠른게 최선이다 여겼지만, 갈 수록 적은 노력으로 큰 효율을 가질 수 있는게 최고 인 듯 합니다.
    언제나 노가다를 줄이는게 최고이기에,
    저는 cout 이 printf에서 출력 type (%d 같은) 을 맞춰 줄 필요가 없어서 편하게 느껴질 때도 있습니다.

    제 생각은 cout은 printf와 비교하기 보다는 차라리 java와 성능비교는 어떨까요? 물론 c++이 빠르다는 것은 대부분 인지하고 있는 사항입니다만, 언어안에 녹아있는 의미(? 철학이라해야하나...)는 java쪽이 가깝다고 보여집니다.
  • 하늘맘 2008/06/24 13:26 # 삭제 답글

    뭐..거의 99.9% C만 쓰고 있기 때문에..printf만 사용하고 있어서, cout은 생소합니다만, 좋은 내용 보고 갑니다..^^
  • 이세희 2008/11/04 10:49 # 삭제 답글

    블로그에 담아갈게요~~
댓글 입력 영역