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;
결론
- 이런 작은(되게 커보이지만) 차이 하나하나를 아느냐 모르느냐에 따라서 성능이 결정되는 것이겠지.
- 많이 알아두자!