병목은 어디에?

2월 말부터 시작해서, 게임 리소스를 네트워크 너머로 배포하는 유틸리티를 작성하고 있다. C# 공부를 겸해서 틈틈이 짜고 있는데, 요 며칠간은 어떤 골치거리에 매여있는 터라 주로 성능 평가만 했는데, 병목현상이 생기는 곳이 좀 예상치 못한 곳이더라.

개념적으로 보내는 쪽(이하 S), 받는 쪽(이하 R) 두 개의 entity로 구성되며, 대략 다음과 같이 동작한다

  1. S에서 보낼 디렉터리 전체의 요약 정보 — 이름 크기 md5 hash — 를 담은 Merkle tree를 만든다.
  2. R 쪽에서도 이걸 구하고 이 tree를 비교해서 어떤 파일이 없고, 어떤 파일이 다른지 판단한다.
  3. R에서 없는 파일의 전체, 혹은 다른 파일의 일부를 요구한다.
  4. S에서 이런 파일의 전체/일부를 전송한다
  5. R과 S에서 전체 디렉터리의 Merkle tree를 다시 구한다; 단 이번에는 다른 hash 함수를 쓴다. 그리고 이 root node만 전송한다.
  6. 이걸 비교해서 전체 파일 전송이 성공했는지 판단한다.

처음 짠 코드에서 병목은 3,4 였고, 이 부분을 .net framework의 TPL을 이용해서 복수 request / 복수 sendfile 형식으로 처리하는 식으로 디스크 / 네트워크 사용률을 높여서 해결했다. 근데 오늘 결과를 측정해보니, 예상외의 장소에서 병목이 나오더라.

3.2 GiB / 1700 파일 정도인 모 프로그램의 설치 디스크를 통째로 전송해봤는데, 대략 11분이 걸렸다. S, R에서 각각 그리고 동시에 Merkle tree를 계산하는 게 약 40초 걸린다. 다음은 전송(3, 4)이 약 5분 걸리더라. 그리고 마지막으로 5단계에서 양쪽에 각각 6분 정도(……….)의 시간이 걸리더라. 16bytes hash를 생성하는 MD5대신에 좀 더 큰 48 bytes (384bits) hash를 생성하는 SHA384를 썼는데 시간이 대략 Orz.

SHA384가 실제로는 SHA 512의 truncated 버전이라, 좀 느릴 거라 생각하긴 했는데, 생각보다 너무 느리다. 거의 똑같이 계산하는데 MD5는 1분도 안 걸리고, SHA384(512?)는 6분이 걸리다니 Orz.

다른 해시를 쓰거나, Merkle tree를 계산할 때 병렬로 – 아마 work-stealing queue를 만들고, I/O 와 CPU 계산에 들어가는걸 고려한 스레드 수를 써서 – 계산해야 하려나?