멈추지 않고 끈질기게
[C++] std::endl vs "\n" 본문
※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다.
※ 해당 포스팅은 C++ 17 버전을 기준으로 작성되었습니다.
※ 해당 포스팅은 하기 출처들을 참조하였습니다.
- https://scarlettb.tistory.com/5
- https://heroine-day.tistory.com/50
0. 서론
저는 주로 프로그래머스에서 코딩테스트를 연습하곤 했는데, 백준에서도 문제를 풀다보니 콘솔 출력 관련 이슈로 시간 초과가 발생하는 등의 문제가 있었습니다. 이번 글은 해당 문제를 해결하는 과정에서 알게 된 내용 중 하나를 기록하기 위한 포스팅입니다.
1. std::endl 과 "\n"의 차이
문자열 출력 시, 줄바꿈이 필요할 때 std::endl 또는 "\n"을 사용할 수 있습니다. 다만 두 방식에는 약간의 차이가 있는데, std::endl의 경우 줄바꿈만이 아니라 스트림의 버퍼를 비우는 작업(Flush)을 같이 수행합니다. Visual Studio에서도 이에 대한 내용을 확인할 수 있습니다.
버퍼를 비우는 작업은 std::flush() 함수를 통해 임의의 타이밍에 실행할 수 있습니다. 즉, std::endl은 "\n"과 std::flush()를 각각 실행한 것과 같은 작업을 합니다. 또한 std::flush() 함수는 다음과 같이 실행할 수도 있습니다.
// 이하 2줄은 동일한 작업 수행
std::cout << "Hello World!" << std::endl;
std::cout << "Hello World!" << "\n" << std::flush;
2. 실행 시간 측정
실제로 어느정도 차이가 나는지 확인하려면 실행 시간을 측정할 필요가 있습니다. C++의 경우 clock() 함수를 통해 현재 시간을 받아올 수 있고, clock_t라는 자료형으로 저장할 수 있습니다. 시간을 측정하고 싶은 코드의 전후로 clock() 함수를 호출하여 clock_t 변수에 저장해두고, 종료 지점의 시간에서 시작 지점의 시간을 빼면 해당 코드의 실행 시간을 측정할 수 있습니다(ms 단위).
#include <iostream>
#include <ctime>
int main()
{
clock_t start, end;
// std::endl 사용
double time1;
start = clock();
for (int i = 0; i < 100000; ++i)
std::cout << i << std::endl;
end = clock();
time1 = (end - start);
// "\n" 사용
double time2;
start = clock();
for (int i = 0; i < 1000000; ++i)
std::cout << i << "\n";
end = clock();
time2 = (end - start);
// 결과 출력
std::cout << "[time 1] " << time1 << "ms" << std::endl;
std::cout << "[time 2] " << time2 << "ms" << "\n";
}
상기 코드는 0부터 999,999까지 매번 개행을 넣으며 출력하는 코드의 실행 시간을 측정하는 내용입니다. time1에는 std::endl을 이용해서 개행할 경우, time2에는 "\n"을 이용해 개행할 경우의 시간을 저장하고 출력하였습니다. 그런데 실행 결과를 보면 생각보다 극적인 차이는 발생하지 않는 것을 확인할 수 있습니다.
3. 스트림 사용 중의 std::endl vs "\n"
위 결과만 보면 std::endl과 "\n"이 별 차이가 없는 것 아닌가 싶지만, 이 두가지 방식이 극명하게 차이를 보이는 경우가 있습니다. 바로 스트림을 사용해서 입출력을 진행하고 있는 경우입니다. 두 방식의 가장 큰 차이가 스트림의 버퍼를 비우는 작업, 즉 Flush에 있는 만큼 스트림을 이용하는 중에는 해당 방식이 큰 차이를 보이게 됩니다.
(std::cout을 통한 콘솔 출력은 기본적으로 버퍼를 비우는 작업을 수행하여 큰 차이가 나지 않는다고 합니다)
#include <iostream>
#include <fstream>
#include <ctime>
int main()
{
clock_t start, end;
// std::endl 사용
double time1;
std::ofstream file1("file_1.txt"); // 파일 스트림 사용
start = clock();
for (int i = 0; i < 1000000; ++i)
file1 << i << std::endl;
end = clock();
time1 = (end - start);
// "\n" 사용
double time2;
std::ofstream file2("file_2.txt"); // 파일 스트림 사용
start = clock();
for (int i = 0; i < 1000000; ++i)
file2 << i << "\n";
end = clock();
time2 = (end - start);
// 결과 출력
std::cout << "[time 1] " << time1 << "ms" << std::endl;
std::cout << "[time 2] " << time2 << "ms" << "\n";
}
거의 같은 코드지만 반복문 안에서 콘솔 출력 대신 ofstream을 통해 .txt 파일에 기록하는 작업을 수행하도록 했습니다. std::endl 방식과 "\n" 방식을 비교해보면 실행 시간이 무려 4배 이상 차이나는 것을 볼 수 있습니다. 스트림 사용 중의 std::endl은 매 개행마다 불필요한 flush 작업을 실행하여 "\n"에 비해 효율이 굉장히 떨어지게 됩니다. 일부 백준 문제의 경우, 해당 차이만으로 시간 초과 여부가 갈리기도 할 정도입니다.
버퍼를 비우는 작업은 꼭 std::endl이 아니더라도 필요에 따라 자동으로 수행하기도 하고(ex. std::cin을 통해 입력 받기 전), 필요하다면 수동으로 실행할 수도 있으므로 위와 같이 스트림을 통한 입출력이 많은 경우에는 "\n"을 이용한 개행을 사용해야겠습니다.
'C++' 카테고리의 다른 글
[C++][메모용] reserve() vs resize() / 시간 측정(std::chrono) (0) | 2024.10.17 |
---|---|
[C++][메모용] map 관련 정리 (0) | 2024.08.01 |
[C++] 람다 표현식(lambda expression) (0) | 2023.12.05 |
[C++] 스마트 포인터(Smart Pointer) (0) | 2023.11.23 |
[C++] 가상 함수(virtual function) (0) | 2023.03.22 |