원글: https://arca.live/b/programmer/62348855



C++을 I/O 최적화 하는데 쓰겠다!

Python으로 아주 빠른 서비스를 만들겠다!



그.. 그래 너 말이 맞아..


오늘 쓴 글의 참고자료

피드백 언제나 환영

https://nodejs.org/ko/docs/guides/dont-block-the-event-loop/


이번에 소개할 글은 간략하게, CPU-Intensive, I/O-Intensive 두개의 차이에 대해 이야기하려 함

이번엔 비전공자는 뒤로가기 눌러주길 바람 궁금하면 더 봐도 되고.


그리고, 찾아보니 CPU-bound, I/O-bound 라는 표현이 더 많더라고

근데 Node에서 intensive라는 표현을 썼으니 나도 그 표현 쓸거임 ㅋㅋ


아마 Node로 프로젝트를 진행중인 친구들이나, 혹은 웹 관련 수업을 들으면서 Node를 쓰는 친구들이라면

한번쯤은 Node는 event loop는 single thread 지만, 내부 v8 engine은 multi thread고 thread pool을 가지고 있다..

뭐 그런 이야기들을 들었을거야. 전공자 친구들은 알다시피, 운영체제가 thread를 생성하는데엔 한계가 있어.

만약 20만명이 동시에 요청을 날렸는데 그 요청만큼 thread를 만들면 서버가 뻗어버릴걸

그래서 thread pool 개념이 있고, event loop 개념이 있고 뭐 그런거임 근데 오늘 할 이야기는 저 이야기를 할려던게 아님


바로 I/O-intensive 작업과 CPU-intensive 작업을 구분해서 어떤걸 최적화하는지 결정하는 이야기임


일단, 저걸 구분하는 쉬운 방법은 너 코드에서 시간복잡도가 산출되느냐임.

이를테면,

DB에 쿼리를 날리는 작업이 있다고 치자. 웬만하면, 한줄로 끝나고 DB에서 이뤄지는 작업은 니 코드에 영향을 못줌.

왜? DB에서 동작하니까. 너의 코드가 아니라. 그리고 DB에 통신을 요청하는 작업, 즉 Network를 쓰는 I/O 작업임.


근데 for문을 써서 너가 DB에서 가져온 데이터를 검사한다? for문의 복잡도에 따라 O(N)도 될 수 있고 O(N^2)도 되겠지

물론, 아마 그렇게 짰으면 교수님이나 조교가 니 대가리를 깰것이다. 아니면 너의 팀원이. 안깼다고? 그럼 뭔가 단단히 잘못 돌아가는거임

그리고 string 비교도 O(N)이다 여기서 N은 string 길이고 보통은 작은길이의 string만 쓰다보니 눈에 안띌 뿐임

그뿐만 아니라 모두의 친한 친구 regular expression도 최악의 경우는 O(2^m) 이다. 여기서 m은 regular expression의 길이임

왜 이런 시간복잡도가 나오냐고? 형식언어와 오토마타를 듣고 오면 알 수 있다. 여기서 설명하면 너무 길어져!


좀더 친근한 예시를 들면 BOJ의 모든 문제는 CPU-Intensive임, BOJ에서 C/C++이 유리한것도 그러한 이유임.

Rust도 유리한데 너가 공부하기엔 자료가 너무 적을거임..


알고리즘의 구현이라는것 자체가 I/O가 stdin 하나로 주어지는 상황 자체가 다른 언어가 같은 알고리즘이라는 가정하에 C/C++을 못이기는 거임.

그래서 BOJ 에서는 python에게는 +10s 제한을 java에게는 +2s 제한을 주는거임. 애시당초 언어의 성능을 보는게 아니라

알고리즘을 얼마나 잘짜는지 보는거니까.


각설하고, 너가 웹서버를 구현하는 상황이라면 과연 C++이 항상 유리할까?

물론, 너가 v8 engine 같은걸 혼자서 구현할 수 있는 괴물이라면 비추를 누르고 조용히 뒤로가기를 누르면 된다.


근데, 들어봐 웹 서버라는 것이 이 I/O-intensive를 설명하기 굉장히 좋은 예시임

일단 Network. 이게 I/O임 요청이 들어오면 일단 읽기 위해 CPU를 재워놓고 읽는다고.

근데 이런 요청이 한두개면 모르겠는데 100개 1000개 아니 10만개씩 들어옴.

이거 관리를 C++로 하려고 하면 니 대가리가 깨질것이다.


아닌데요? 쉬운데요? 뒤로가기 누르라고!!


아무튼, 여기서 포인트는 CPU를 재운다는 점임, CPU가 자는 동안은 오직 읽기/쓰기 속도의 문제지

CPU가 일하는게 하나도 없음. 즉, 이 부분은 진짜 언어의 성능이랑 상관없다는 뜻임


근데 이제 다시 CPU-Intensive 작업으로 돌아와서.

만약 DB에서 충분히 할 수 있는 작업을 굳이 N개 데이터로 읽어들여서 뭔가 이상한짓을 한다 해보자.

굳이 Node 에서 for 를 O(N)번 돌린다던지, 뭐 머리 썼다 쳐서 O(log N)으로 줄인다던지 하더라도 엄청 멍청한 짓을 한다?

과연 올바를까? DB에서 O(log N) 혹은 O(N)에 맡기고 너의 코드에서는 O(1)에 작업을 끝내는 게 당연히 이득일것이다.

 

그것을 넘어서 DB에서도 처리 못하고 v8 engine이 지원하는 C++ addons 에서도 너가 원하는 기능을 지원하지 않는다면

너는 그걸 순수 js 로 구현하는것이 맞을까? C++로 너만의 addons를 만드는것이 맞을까?


이런것을 판단하는 것이, CPU-intensive 와 I/O-intensive 의 구분임.

그냥 간단하게 Network든 Pipeline이든, File-I/O 든 뭔가 I/O가 발생해서 CPU가 대기하는 거면 무조건 I/O-intensive다.

반대로 시간복잡도가 계산이 되는 작업, 이를테면 for문이나 while문이 들어간다던지 문자열을 비교한다던지.. regular expression으로 문자열 패턴 매칭을 한다던지.. 이러한 일련의 작업이 CPU를 사용하는 CPU-intensive 한 작업임.


당연한 이야기겠지만, CPU-intensive 한 작업은 언어가 기계어에 가까우면 가까울수록, 알고리즘이 빠르면 빠를수록 빠르다.

반면, I/O-intensive는 그냥 진짜 하드웨어 바인드임, 이걸 소프트웨어로 빠르게 한다는건 있을 수 없고(아 물론 DB를 더 성능 좋은 DB로 바꾼다.. 이런건 있을 수 있는데 그것도 결국 너가 다루는 데이터의 특성과 DB의 특성을 잘 이해하고 있어야 할 수 있는 행동이고. 이 부분은 결국 CPU-intensive한 작업을 들여다 봐야함)


보통은 캐시서버를 둔다거나, 네트워크 성능 자체를 올린다거나, 서버를 늘리고 분산으로 입력을 받는다거나 이런식으로 부하를 줄이는 방향으로 최적화를 하게 됨.

파일 I/O가 필요한 작업이면, 아예 데이터를 전부 램에다 올려놓고 관리하거나, 디스크를 겁나 비싼걸 쓰거나.


일반적인 서버는 CPU-intensive 한 작업이 거의 없으니 하드웨어를 업그레이드하는게 굉장히 도움이 되지만

대용량으로 다양한 서비스를 하기 시작하면 본격적으로 CPU-intensive한 작업이 많아지면서, 저런것들을 너가 신경쓰게 된다.


물론, 이때 RPC 라는 개념이 있기 때문에, CPU-intensive 한 작업은 다른 사람이 처리해줄 수 있어.

근데 저 RPC조차 아까워서(저것도 일단은 I/O는 I/O라) wrapper를 써서 그냥 한 프로세스 내에서 다른 언어끼리(Go - C++, Python - C++) 통신하는 케이스도 있어


이건 통신이라고 보기엔 애매하고 그냥 메모리 읽는 방법을 공유한다고 생각하면 됨(Tensorflow 를 예시로 들면, 내가 정의한 텐서는 float 배열이고, dimension은 몇차원이고 shape는 어떻게되고 포인터 시작위치는 어디고.. 이런걸 공유하는걸 말함).

뭐 어차피 이런 부분은 나중가서 배우게 될테니.. 지금은 몰라도 되겠다.


물론 GO같은 언어는 저런 CPU-intensive 한 작업을 어느정도 커버할 수 있다.

그럼에도 불구하고 구글은 0.1% 라도 성능을 올리면 그게 다 돈이다보니 오늘도 욕을 박으면서 C++을 쓴다..


근데, Rust만으로 서비스를 구성한 애들도 있다!

궁금해서 검색해봤는데 디스코드가 Rust기반으로 서비스 하나를 뚝딱 구성했다더라

심심하면 읽어봐.

https://discord.com/blog/why-discord-is-switching-from-go-to-rust


긴글 읽어줘서 고마워.