모든 경우에 다 통하는 진리는 당연히 아니지만 그동안의 경험을 볼때 중요한 항목들은 다음과 같다.
1. 전역 변수 사용(접근)을 자제하라.
내가 꼽은 성능 저하의 일등 공신은 바로 전역 변수다. 여기서 전역은 문법적인게 아니라 다른 쓰레드에서도 사용할 수 있는 범위를 가진 변수를 말한다. 이 녀석들은 동기화 문제도 있지만 캐쉬에도 큰 영향을 미친다. 아래의 코드는 counter라는 변수의 위치만 다를 뿐이지만 실제로 돌려보면 상상도 못할 큰 차이가 난다(성능을 위해 동기화를 하지 않아도 차이가 많이 난다. 동기화하려고 Interlocked...등을 사용하면 성능차이는 더 크게 벌어진다).
(릴리스 모드에서는 for 루프를 한번에 결과 값으로 바꾸기 때문에, 최적화를 하지 않아야 제대로 된 결과를 얻을 수 있다. 간단한 예제로 컴파일러의 최적화를 피해가는건 어려운 일이다. #pragma optimize 문을 사용하는 것이 가장 확실한 차이를 알 수 있다.)
프로그램이 저렇게 덧셈만 있는 경우는 없겠지만 이런 경우라면 로컬(자동) 변수를 이용해서 쓰레드 합을 구하고 나중에 합산하는게 효율적이다.
2. 쓰레드 개수보다는 코드 길이가 더 큰 영향을 미친다.
쓰레드 개수에 목숨거는 사람들이 있는데, 쓰레드가 한 두개 더 있어서 생기는 오버헤드는 없다고 봐도 무방하다. 그러니까 10개를 8개로 줄이는 건 큰 차이가 없다는 뜻이다(물론 100개랑 8개는 적지 않은 차이가 있다). 그보다는 코드가 적당한 길이를 유지하는게 더 중요하다.
윈도우 서버의 타임 슬라이스(aka Quantum)는 상당히 긴 시간이고 이를 충분히 활용해야만 한다. 주어진 quantum을 다 사용하지도 못하고 쓰레드 실행 코드가 종료되면 OS는 스케쥴링을 다시하고 문맥 전환(context switching)을 일으키게 된다. 이런 일이 빈번하면 좋을게 없으니 몇 줄짜리 쓰레드를 생성했다면, 이걸 다른데 통합할 방법이 없을까 고민해야 한다(이런 과정을 거치다보면 자연스럽게 쓰레드 개수가 줄게된다).
예전에는 IOCP worker thread에서 packet을 받으면 저장만하고 바로 GQCS() 상태로 들어갔는데, 최근 작업에서는 그 안에서 별별걸 다 하고 있다. 블럭만 안된다면 워커 쓰레드 안에서 가능한 많은 일을 처리하도록 만드는게 훨씬 더 효율적이다.
3. 락프리 기법은 만능이 아니다.
보통 락프리 기법이라고 말하면 커널 객체를 사용하지 않고 유저 모드에서 동기화하는 방법인데, 이는 만능이 아니다. CS같은 커널 객체에 비하면 가볍지만 아쉽게도 모든 문제를 해결할 순 없다. 잔머리를 잘 굴려서 락이 필요없도록 만드는게 가장 중요하다(말은 쉽지만 대단히 어려운 기술).
캐쉬(cash말고 cache)에는 프로그램 성능의 희노애락이 모두 녹아 있다고해도 과언이 아니다. 락프리를 믿고 쓰레드 경계를 넘나드는 자료형이 있다면 성능의 대재앙으로 돌아오게 돼 있다. 더구나 락프리는 구현도 어렵고 (최근에는 괜찮은 라이브러리들이 나와 있지만) 잘못쓰면 알아차리기 힘든 버그를 만든다.
락프리로 만들면 충분하지라고 생각하지 말고, 그조차도 없앨 수 있는 방법이 뭐가 있을까 잘 고민을 해야한다. 결국에는 서로에게 신경 안쓰고 돌아가는 core 개수만큼의 쓰레드가 가장 이상적이니까 말이다.
그래서...?
사실 위의 3가지를 잘 지키면 최고 성능의 서버 응용프로그램을 만들 수 있나요?라고 물으면 자신있게 "네"라고 대답 할 순 없겠지만, 최소한 나쁘지는 않을 것이라고 확신한다. 그 이상은 끝없이 병목 지점을 찾아서 제거하고 구조를 바꾸는 근성의 영역이라고 하겠다.
1. 전역 변수 사용(접근)을 자제하라.
내가 꼽은 성능 저하의 일등 공신은 바로 전역 변수다. 여기서 전역은 문법적인게 아니라 다른 쓰레드에서도 사용할 수 있는 범위를 가진 변수를 말한다. 이 녀석들은 동기화 문제도 있지만 캐쉬에도 큰 영향을 미친다. 아래의 코드는 counter라는 변수의 위치만 다를 뿐이지만 실제로 돌려보면 상상도 못할 큰 차이가 난다(성능을 위해 동기화를 하지 않아도 차이가 많이 난다. 동기화하려고 Interlocked...등을 사용하면 성능차이는 더 크게 벌어진다).
(릴리스 모드에서는 for 루프를 한번에 결과 값으로 바꾸기 때문에, 최적화를 하지 않아야 제대로 된 결과를 얻을 수 있다. 간단한 예제로 컴파일러의 최적화를 피해가는건 어려운 일이다. #pragma optimize 문을 사용하는 것이 가장 확실한 차이를 알 수 있다.)
프로그램이 저렇게 덧셈만 있는 경우는 없겠지만 이런 경우라면 로컬(자동) 변수를 이용해서 쓰레드 합을 구하고 나중에 합산하는게 효율적이다.
2. 쓰레드 개수보다는 코드 길이가 더 큰 영향을 미친다.
쓰레드 개수에 목숨거는 사람들이 있는데, 쓰레드가 한 두개 더 있어서 생기는 오버헤드는 없다고 봐도 무방하다. 그러니까 10개를 8개로 줄이는 건 큰 차이가 없다는 뜻이다(물론 100개랑 8개는 적지 않은 차이가 있다). 그보다는 코드가 적당한 길이를 유지하는게 더 중요하다.
윈도우 서버의 타임 슬라이스(aka Quantum)는 상당히 긴 시간이고 이를 충분히 활용해야만 한다. 주어진 quantum을 다 사용하지도 못하고 쓰레드 실행 코드가 종료되면 OS는 스케쥴링을 다시하고 문맥 전환(context switching)을 일으키게 된다. 이런 일이 빈번하면 좋을게 없으니 몇 줄짜리 쓰레드를 생성했다면, 이걸 다른데 통합할 방법이 없을까 고민해야 한다(이런 과정을 거치다보면 자연스럽게 쓰레드 개수가 줄게된다).
예전에는 IOCP worker thread에서 packet을 받으면 저장만하고 바로 GQCS() 상태로 들어갔는데, 최근 작업에서는 그 안에서 별별걸 다 하고 있다. 블럭만 안된다면 워커 쓰레드 안에서 가능한 많은 일을 처리하도록 만드는게 훨씬 더 효율적이다.
3. 락프리 기법은 만능이 아니다.
보통 락프리 기법이라고 말하면 커널 객체를 사용하지 않고 유저 모드에서 동기화하는 방법인데, 이는 만능이 아니다. CS같은 커널 객체에 비하면 가볍지만 아쉽게도 모든 문제를 해결할 순 없다. 잔머리를 잘 굴려서 락이 필요없도록 만드는게 가장 중요하다(말은 쉽지만 대단히 어려운 기술).
캐쉬(cash말고 cache)에는 프로그램 성능의 희노애락이 모두 녹아 있다고해도 과언이 아니다. 락프리를 믿고 쓰레드 경계를 넘나드는 자료형이 있다면 성능의 대재앙으로 돌아오게 돼 있다. 더구나 락프리는 구현도 어렵고 (최근에는 괜찮은 라이브러리들이 나와 있지만) 잘못쓰면 알아차리기 힘든 버그를 만든다.
락프리로 만들면 충분하지라고 생각하지 말고, 그조차도 없앨 수 있는 방법이 뭐가 있을까 잘 고민을 해야한다. 결국에는 서로에게 신경 안쓰고 돌아가는 core 개수만큼의 쓰레드가 가장 이상적이니까 말이다.
그래서...?
사실 위의 3가지를 잘 지키면 최고 성능의 서버 응용프로그램을 만들 수 있나요?라고 물으면 자신있게 "네"라고 대답 할 순 없겠지만, 최소한 나쁘지는 않을 것이라고 확신한다. 그 이상은 끝없이 병목 지점을 찾아서 제거하고 구조를 바꾸는 근성의 영역이라고 하겠다.
TAG 고성능
