항상 flush를 해서 느린 c++ endl

레퍼런스/예제 2020. 8. 18. 23:52

 처음 백준을 하시다 보면, 출력하실 때, 개행을 출력하기 위해서 endl을 많이 쓰는 실수를 합니다. 이게 어떤 문제가 있는지 공식 문서를 하나 하나 해석해 보면서 알아보겠습니다.

 


 먼저 링크를 보시면 아래 문구가 눈에 보입니다.

 

 개행을 stream에 insert를 한 다음에, stream을 flush 한다고 해석할 수 있습니다. 그러면, 이게 얼마나 시간을 많이 먹는 작업일까요? 간단하게 테스트 프로그램을 하나 만들어 보겠습니다.

 

 

 123이라는 문자열을 2000만번 출력하는 프로그램입니다. 그리고, 이 작업을 하기 전에 시간을, 한 후에 시간을 출력함을 알 수 있는데요. 간단하게 시간을 측정하기 위해서, 이 방법도 나름 쓸만합니다.

 

 

 저는 ex를 실행시켜서 출력된 결과를 1.txt에 넣겠습니다. 그리고, 1.txt의 맨 위에 있었던 값과, 맨 아래에 있던 값들을 뽑아 보았는데요. 1597678637에서 1597678598을 빼면 39임을 알 수 있습니다. 2000만개의 123 문자열을 출력하는데 37 ~ 39초 정도 걸렸다는 이야기가 됩니다.

 

 그러면 buffering을 못해서 그런 것일까요? 내부적으로 flush가 호출이 된다고 그러긴 했는데. 글쎄요. 먼저 ./ex >> 1.txt가 수행될 때 어떤 시스템 콜을 날리는지 추적해 보도록 하겠습니다.

 

 "123\n"을 계속 write 한다는 것을 알 수 있습니다. write 연산은 2000만번 하였으니, 대충 2000만번 write를 호출했다고 해도 과언이 아니겠습니다. 그러면 cin이나 cout에 뭔가 버퍼는 있어 보이지만, 개행을 출력하기 위해 endl을 넣으면 file에다가 썼는 데도 불구하고, 좋은 것을 쓰지 못한다. 정도로 이해하면 좋을까요? 자. 그러면 "123\n"을 특정한 파일 데스크립터에 2000만번 print를 하는 프로그램의 성능은 어떻게 될까요?

 

 

 5번째 줄에서 fopen 함수로 파일 포인터를 얻습니다. 그리고 파일 포인터와 연관된 file 디스크립터를 얻어오기 위해, fileno 함수를 이용합니다. 데스크립터까지 얻어왔다면, 그 다음에는, 내부적으로 호출했던 write 함수를 2000만번 호출하면 됩니다.

 

 

 ex2를 실행시켜 보니, 대충 38초 정도의 시간이 나옵니다. 시스템 콜 write가 어떻게 동작하는지는 모르지만, 우리는 하나의 사실을 알 수 있습니다. write를 매우 많이 호출하면 매우 오래 걸린다. 정말 그런지, -c 옵션을 주어서 보겠습니다.

 


 strace에 -c 옵션을 주면, call한 목록과 시간, error를 정리해 줍니다. 평소에 ex2를 실행시켰을 때 보다 오래걸렸다는 것은 염두에 두시면 좋겠습니다.

 

 

 표를 보면, seconds가 있는데요. 2000만개의 call을 하기 위해 73.374592초가 걸렸다는 의미입니다. 그냥, write 때문에 오래 걸렸다고 봐도 무난하겠네요. 그러면 아래 프로그램은 어떨까요?

 

 

 단순히 endl을 '\n'으로 바꾸었습니다.

 

 

 strace에 -c 옵션을 주어서, 시스템 콜과 소요 시간을 정리해 보았습니다. 아까와 비교하면 꽤 큰 차이를 보이는데요. write를 2000만번 호출했다면, endl 대신에 '\n'으로 처리한 다음에 1.txt로 출력을 해 버리면, writev를 단 9765번 호출하였습니다.

 

 

 자세히 보면, '\n'이 입력이 되었는데도 계속 버퍼에 무언가를 받았음을 알 수 있습니다. 물론 버퍼링을 할 때, line buffering을 할 수도 있고, full buffering을 할 수도 있고, 그렇지 않을 수도 있습니다. 중요한 것은, endl을 입력한 경우에, 꽤 크게 버퍼링을 할 수 있는데도, 개행 문자까지만 받고 끊어버리다 보니 시스템 콜인 write 호출이 많아졌고, 그것이 그대로 오버헤드로 돌아온 셈이 됩니다. 이제 다시 이 문서를 읽어 보겠습니다.

 


 문서를 보시면, stream하고 output sequence하고 동기화를 시킨다고 되어 있습니다. 그리고 중간에 보시면, 아래 문구가 있습니다.

 

 stream에서 어딘가로 written을 한다. 그리고 내부적으로는 버퍼로 구현된. 이라는 게 눈에 들어옵니다.

 

 

 endl을 만난 경우에, '\n'을 추가하고, flush를 하는데요.

 

 

 버퍼링 정책이 버퍼가 꽉 찼을 때 출력하는 정책이라 해도, endl을 넣으면 강제로 flush가 됩니다. 이게 얼마나 큰 차이냐. 라고 다시 되물으신다면, 위에서 strace 한 결과를 보여드렸으니, 생략해도 될 듯 싶습니다. 요약하면, 개행을 출력하기 위해서 endl을 쓰기 보다는 '\n'을 쓴다. 정도로만 보시면 되겠습니다.