신입 직원에게 프로그램 교육을 위해서 과제를 내줬는데, 프로그램이 이해 불가능한 성능을 보여서 같이 원인을 찾아봤다. 프로그램이 죽도록 느린 이유는 다름아닌 rand_s().

내 경우 난수 생성을 할 때 보통은 stdlib의 rand()를 쓰고, 프로젝트에서는 메르센-트위스터를 사용한다. rand()는 정말 빠르고, 메르센-트위스터도 속도로 인해 나를 고민하게 만든 적은 없다. 물론 둘은 안전하지 않다고 알려져 있다. 그러니까 성깔 있는 놈한테 걸리면 다음 난수를 예측당할 수도 있는 것이다. 그러나 MMO 서버의 특성상 동시 다발적으로 나 외의 수 많은 사람들에 의해서 (심지어 NPC도) 난수가 생성되기 때문에 사실상 안전하다고 봐도 된다(온라인 포커 게임이나 릴게임 같은 경우에는 실제로 분석해서 맞추는 성깔 있는 놈들이 진짜 있다).

MSDN에 의하면 rand_s()는 cryptographically secure random numbers를 생성한다고 한다. 물론 그를 위해서 막대한 대가를 치를테고 말이다. rand_s()의 득과 실을 파악하고 상황에 맞게 사용하자. 닭 잡는데 소잡는 칼은 필요 없으니 말이다.

신입 직원에게 내준 프로그램은 보안따위는 전혀 신경 쓸 일이 없었다.


TAG rand_s
Posted by 조성경 트랙백 0 : 댓글 0
모든 경우에 다 통하는 진리는 당연히 아니지만 그동안의 경험을 볼때 중요한 항목들은 다음과 같다.

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가지를 잘 지키면 최고 성능의 서버 응용프로그램을 만들 수 있나요?라고 물으면 자신있게 "네"라고 대답 할 순 없겠지만, 최소한 나쁘지는 않을 것이라고 확신한다. 그 이상은 끝없이 병목 지점을 찾아서 제거하고 구조를 바꾸는 근성의 영역이라고 하겠다.
Posted by 조성경 트랙백 0 : 댓글 3

이제 윈도우와 리눅스의 우열을 가리는 것은 사실상 의미가 없다. 이미 윈도우 서버를 잘 쓰고 있다면, 그걸 리눅스로 바꾸는 행동은 삽질에 가깝다. 그냥 구인하기 쉬운 OS를 도입하면 된다.

그러나, 윈도우 서버에는 큰 단점이 하나 있고 이때문에 도입 불가능한 분야가 실제로 있다. 그게 바로 '윈도우 업데이트'다. 더 정확히 말하면 업데이트 후 리부팅이다. 한달에 두세번씩 강제적으로 하는 리부팅이 분야에 따라서는 치명적일 수 있다. 물론 업데이트따위는 깔끔하게 무시하고 버티는 방법도 있기는한데, 이는 더 큰 문제를 초래할 수도 있으니 가능하면 업데이트는 다 하는게 좋다.

윈도우 서버가 리눅스의 거센 공세를 막고 세를 계속 확장(사실 이미 하향세)하려면 이 업데이트 문제를 반드시 해결해야한다. 실무에서만 10년 가까이 윈도우만 쓴 나도 짜증나서 바꿀까?하는 생각이 들때가 있으니 말이다.

Posted by 조성경 트랙백 0 : 댓글 0