C\\C++

[C/C++] 표준입출력 함수의 장점과 단점

아몬드통 2023. 1. 21. 16:32

윤성우의 tcp/ip 책 15장 내용 정리

표준입출력 함수의 장점과 여러 시스템 함수

표준입출력 함수란?

  • 표준입출력 함수는 단어 그대로 C언어에서 제공하는 표준입출력 함수이다. 
  • 종류에는 printf, scanf, fputs, fgets 등등이 있다.
  • 여기서 말하는 표준의 기준은 "모든 운영체제(컴파일러)가 지원하도록 ANSI C에서 정의했다." 를 말한다.
  • 괄호로 표현했지만, 정확히는 어떤 OS에서 C++ 컴파일러가 작동이 가능하면 모두 사용가능한 함수를 표준함수라고 할 수 있다.

 

표준입출력 함수의 장점

  • 이식성이 좋다.
  • 버퍼링을 통한 성능 향상에 도움이 된다.

첫번째, 이식성이 좋다는 말을 위에서 설명한 표준이라는 말로 전부 설명이 된다.

두번째, 성능에 도움이 된다. 이것을 풀어서 설명하는 것이 이 글의 목적이다.

 

표준입출력함수의 데이터 입출력 통신 구조.

 

버퍼링이란?

  • 사전적 정의로는 "정보의 송수신을 원활하도록 하기 위해서 정보를 일시적으로 저장하여 작업의 처리 속도 차이를 흡수시키는 방법" 이라고 설명되어있다.
  • 넷플릭스를 예를 들어보자.
    • 넷플릭스 영화 시청을 데이터 관점에서 보면 넷플릭스 서버가 사용자 컴퓨터로 데이터를 전송하는 것이 전부이다.
    • 이때 영화는 1초 재생에 10m byte의 데이터가 필요하다고 가정해보자.
    • 서버의 전송속도가 20m/s라면 끊김없는 재생이 가능하다.
    • 하지만 전송속도가 5m/s라면 사용자는 0.5초를 재생 0.5초를 로딩을 계속해서 반복할 수밖에 없다.
    • 이런 상황을 막기위해서 버퍼링을 사용한다.
  • 요즘은 버퍼링이라는 말보다는 로딩이라는 말이나 O의 형태로 화면 가운데서 돌아가는 그림을 더 많이 사용하지만 이것들이 바로 버퍼링이다.
 

표준입출력 함수와 linux 시스템 함수의 성능 비교

  • 표준입출력함수 말고도 linux os에서 자체적으로 제공하는 입출력 함수들이 존재한다.
  • read, write 가 여기에 해당하는데 비표준이므로 다른 os에서 사용이 불가능 할 수도 있고, 함수 자체의 버퍼가 없다.
  • linux 시스템을 사용한다면 둘다 사용이 가능하므로 크게 상관없지만 버퍼가 있고 없고의 차이로 인해서 파일, 소켓 입출력에 성능차이가 발생한다.
  • 버퍼와 버퍼링이 어떤 기능을 해주길래 이런 성능차이가 발생하는 것일까?

표준입출력 함수와 시스템 함수의 데이터 전송 방법

  • 위 그림으로 간단히 설명해보자. file(socket)에 어떤 데이터를 작성 비용은 100으로 아주 비싸다.
  • 그렇다면 100byte 데이터를 3byte로 나눠서 file에 write 했을때 아래 read함수는 총 34번의 함수를 호출해야하고 비용은 3400이다.
  • 이번에는 fputs함수로 file에 데이터를 3byte씩 나눠서 전송해보자. 
  • fputs함수는 표준입출력이므로 버퍼를 가지고 있다. 버퍼의 크기는 15byte이고 이 버퍼로 데이터를 전송하는 비용은 1이다.(실제로 비용이 낮다.)
  • fputs를 이용해서 100byte를 파일에 작성 비용은 fputs_buffer에 100byte를 전부 보내는 비용34(5*1*6+4*1) + fputs_buffer가 15씩 꽉 찼을 때 file에 보내는 비용700(7*100) 총 734이다.(여기는 함정이 하나 있다. 아래 설명)
  • read와 fputs는 동일하게 100byte데이터를 3byte씩 전송했는데 비용의 아치는 3400과 734로 꽤 많은 차이가 발생한다.
  • 물론 예시를 들어서 설명한 것이기 때문에 실제로 이런 차이가 날지 어떨지는 모르지만 기본 개념상으로는 이렇다.
  • 되게 간단하게 설명했지만 실제 컴퓨터 상에서 io, 입출력은 비용이 아주 높은 작업이다. 그러므로 어떤 프로그램이나 운영체제가 성능을 높이기 위해서는 이 io를 어떻게 처리하냐에 따라서 성능이 나뉘기도 한다.

 

실제 코드 테스트

while(fgets())	//테스트 1
	fputs(); 
    
while(read())	//테스트 2
	write();
  • 실제로 테스트를 해봤다.
  • 테스트 1은 표준입출력 함수
  • 테스트 2는 시스템 함수
  • 작은 파일사이즈일때는 큰 차이가 없지만 파일크기가 커지면 커질 수록 많은 차이를 보인다.
  • 1Gbyte 파일이라고 가정했을때 테스트 1은 1분안쪽으로 테스트 2는 30분? 이상 걸렸다.
  • 위에서 말했지만 io는 꽤 비용이 많이 드는 작업이라는 것을 느낄 수 있었다.

 

while(fgets())	//테스트 3
	write(); 
        
while(read())	//테스트 4
	fputs();
  • 테스트를 하다가 문득, 그러면 함수를 하나씩만 바꾸면 어떨까? 해서 테스트를 해봤다.
  • 같은 파일일때 테스트 3은 170초 테스트 4는 83초가 걸렸다.
  • 개인적으로는 읽는 작업보다는 쓰는 작업이 훨씬 비용이 높다? 라고 결론내렸다.

 

표준입출력 함수의 단점

  • 위의 fputs를 사용하면서 단점이 있다고 했는데 그건 fputs_buffer에서 file로 데이터를 전송하는 기준의 문제이다.
  • 100을 3으로 나눠서 5번에 걸쳐 fputs_buffer에 15를 저장한 후 다시 file에 15를 전송하는 형태로 저장하고 있다.
  • 이때 fputs_buffer에서 file로 작성하는 기준은 15가 가득 찼을때라고 말할 수 있다.
  • 근데 마지막 데이터를 보낼때는 데이터의 크기가 10이기 때문에 15가 전부 차지 않는다. 이럴때는 file에 10을  전송해야하는지 말아야하는지 컴퓨터는 판단을 할 수가 없다.(따로 조건이나 기능을 추가하면 되겠지만 기본적으로는 안된다.)
  • 그래서 90까지의 데이터가 전송되고 나머지 10의 데이터는 전송이 될수도 안될수도 있다.
  • 이런 문제를 해결하기 위해서 fflush라는 버퍼를 비워라(데이터를 file로 보내라)라는 명령의 함수를 호출한다.
  • 이렇게 되면 데이터를 보내지 않는 것은 막을 수 있지만, fflush라는 작업이 중간에 하나 더 발생해서 표준입출력 함수의 버퍼를 이용하는 장점이 발휘되지 못할 가능성도 존재한다.
  • 그 외에 양방향 통신이 쉽지 않다, 파일디스크립터를 FILE 구조체의 포인터로 변환해야한다. 는 단점도 있다.

 

FILE 구조체의 버퍼

  • 위의 그림으로 설명했는데, fputs, fgets 이런 표준입출력 함수를 사용하면 버퍼가 따로 있다고 하는데 어디에 버퍼가 있는건지 몰랐다.
  • 실제 file의 버퍼나 소켓버퍼같은 것은 결국 os에서 관리하기 때문에 프로그래머가 어떻게 할 수 있는 영역은 아니라서 상관없지만, 함수를 호출하는데 os에서 버퍼가 따로 생기는건가? 라는 의문점이 들어서 조금 찾아보니 표준입출력함수의 버퍼는 결국 FILE 구조체의 배열이었다.
  • 아래 FILE 구조체를 보면 알겠지만 여러개의 변수들이 있고, 내 생각에는 아마도 _base가 버퍼가 아닐까 생각된다.
  • 그래서 이 FILE 버퍼에 데이터를 넣는 비용이 "낮다"라고 표현 할 수 있었던 것이다. 그냥 배열로 복사하는 단순 작업일테니까 말이다.
typedef struct _iobuf
{
  char	*_ptr;
  int	 _cnt;
  char	*_base;
  int	 _flag;
  int	 _file;
  int	 _charbuf;
  int	 _bufsiz;
  char	*_tmpfname;
} FILE;

 

결론

  • 이런 작은(되게 커보이지만) 차이 하나하나를 아느냐 모르느냐에 따라서 성능이 결정되는 것이겠지.
  • 많이 알아두자!