멈추지 않고 끈질기게

[C++] std::endl vs "\n" 본문

C++

[C++] std::endl vs "\n"

sam0308 2024. 7. 23. 14:52

※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다.

 

※ 해당 포스팅은 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에서도 이에 대한 내용을 확인할 수 있습니다.

사진 1. std::endl 설명

 

 버퍼를 비우는 작업은 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";
}

사진 2. 실행 시간 - 콘솔 출력

 

 상기 코드는 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";
}

사진 3. 실행 시간 - 파일 스트림

 

 거의 같은 코드지만 반복문 안에서 콘솔 출력 대신 ofstream을 통해 .txt 파일에 기록하는 작업을 수행하도록 했습니다. std::endl 방식과 "\n" 방식을 비교해보면 실행 시간이 무려 4배 이상 차이나는 것을 볼 수 있습니다. 스트림 사용 중의 std::endl은 매 개행마다 불필요한 flush 작업을 실행하여 "\n"에 비해 효율이 굉장히 떨어지게 됩니다. 일부 백준 문제의 경우, 해당 차이만으로 시간 초과 여부가 갈리기도 할 정도입니다.

 

 버퍼를 비우는 작업은 꼭 std::endl이 아니더라도 필요에 따라 자동으로 수행하기도 하고(ex. std::cin을 통해 입력 받기 전), 필요하다면 수동으로 실행할 수도 있으므로 위와 같이 스트림을 통한 입출력이 많은 경우에는 "\n"을 이용한 개행을 사용해야겠습니다.