암달의 법칙은 전체 작업중, 0 이상 1 이하의 P 만큼 해당하는 부분에서 S배 만큼 성능이 향상되었을때 전체 작업의 최대 성능 향상의 상한은 정해져있다는 이론으로 1 / ((1 - P) + P / S) 만큼 성능이 향상된다. 만약 전체 작업의 50% 만큼 해당하는 부분이 2배로 빨라져도 실제 성능 향상은 약 1.333333...배가 된다. 그런데 이 법칙은 이론상 최대의 개선 효율을 가정한 것이고 대부분 그마저도 못한 경우가 훨씬 많다. 그 어떤 분야에도 적용되는 아주 간단한 이론이지만 많은 경우가 멀티스레드 프로그램의 성능 향상을 예상하는데 사용된다. 그 말은 멀티스레드로 동작하는 프로그램을 작성하기로 한 이상은 이 저주를 피해갈 방법이 없다. 안타깝지만 아직 이 저주를 깬 사람은 없다. 그러나 거의 깨질 것 같이 근접한 사람은 있었다. 단순히 상한을 정해놓은 것이 아니라 계산으로 나오는 결과이기 때문에 완전히 깨지는건 불가능하지만 근접하는 경우는 충분히 가능하다. 그리고 최대한 근접한 성능을 내는 프로그램이 멀티스레드 프로그램이 낼 수 있는 최대 성능 향상률이 되겠다. 왜 깨는것은 불가능하지만 근접하는건 가능한지 알아본다.


정렬 알고리즘을 예로 들어보자... 어떤 아주 큰 크기의 배열이 있고 이 배열을 1스레드로 정리하는데 코어 하나를 100%로 사용하여 약 0.1초가 걸린다고 하자. 그런데 어떤 멍청한 프로그래머가 이 알고리즘을 개량하여 n개의 스레드로 동작할 수 있게 만들었다. 그러나 코어 사용률은 n분의 1이 되었고 이론상 각 코어가 n분의 1 만큼의 작업만 처리하면 되므로 전력 사용량이 줄면서 속도는 같아야 하지만 이상하게 전력 소모량은 별로 나아지지 않았고 대신 속도가 왕창 느려졌다. 왜 이런 결과가 나왔을까? 조별과제는 고통의 시간이라는 것을 아는가? 사람이 많을수록 배가 산으로 가기도 하고 아예 하늘을 날기도 한다. 대부분의 정렬 알고리즘은 1스레드로만 돌아가는게 원칙이다. 그런 특성을 무시하고 멀티스레드로 개조했고 스레드간의 처리 순서와 상태 제어를 위한 함수를 아주 많이 사용했다. 게다가 이런 제어 함수들은 빠르게 실행되지도 않는다. 제멋대로 스레드가 대기해야 하는 일이 생기고 알고리즘이 계속 중간에 실행이 멈춘다. 이건... 이렇게 복잡한 알고리즘을 감당할 수 있는 프로그래머에게 오히려 칭찬을 보내야 하는게 아닐까...? (성능은 일단 둘째로 치자) 당연하지만 멀티스레드는 이런식으로 접근하면 안된다.


사실 내 컴퓨터는 왠만한 슈퍼컴퓨터보다 더 빠르다. 단, 코어당 성능에 한정해서다. 슈퍼컴퓨터는 코어의 개별 성능이 낮은 대신 전력과 발열을 줄여 더 많은 코어를 집적 가능하게 한다. 그래서 개인용 컴퓨터는 제아무리 돈을 투자해봤자 소켓당 64코어가 최대고 그게 컴퓨터의 최대 성능이다. 그리고 많은 코어가 높은 클럭으로 동작하니 전력 소모와 발열은 어마어마하다. 반대로 슈퍼컴퓨터는 보드당 소켓이 여러개 있기도 하고 전력 소모와 발열이 억제되어 있으며 그런 보드 하나가 수십, 수백, 수천이 모여서 하나의 거대한 컴퓨터가 된다. 다만 코어당 클럭이 낮아서 빠르게 계산은 불가능하므로 위에 설명한 가상의 알고리즘을 이런 슈퍼컴퓨터로 구동시킨다면 더 느려지는걸 넘어서 차라리 사람이 직접 정렬하는게 더 빠르게 될지도 모르겠다...


그거 그러면 그냥 스레드만 무지막지하게 늘리면 되는거 아님? 그렇지가 않아서 문제다. 코어와 스레드는 1대1 대응하지 않으므로 코어가 적어도 더 많은 스레드를 만들어낼 수 있다. 코어보다 스레드가 많아도 부담이 적은 이유는 코어는 충분히 빠르고 모든 스레드가 항상 계산이 필요하진 않기 때문이다. 일반 개인용 컴퓨터에서 작동하는 스레드중에 많은 수는 무언가 이벤트가 발생하기를 기다리는데 대부분의 시간을 보낸다. 그런 스레드는 데이터나 신호가 준비되기 전까지는 동작하지 않기 때문에 아주 일부분의 스레드가 항상 계산이 필요해도 부담이 적은 것이다. 그리고 엄청 강도높은 계산을 요구하는 게임이나, 수치연산 등의 프로그램이 아니면 일정량 이상의 코어 사용시간이 필요하지 않고 알아서 대기에 들어가거나 사용시간이 다 되서 자동으로 대기 상태로 전환된다. 그렇지 않고 계산 시간이 항상 필요한 스레드가 많아지면 시스템 전체의 연산 성능이 감당하는 한 속도가 느려지진 않지만 넘어가는 순간부터는 점점 느려지게 된다...


그럼 멀티스레드 프로그램은 어떻게 접근해야하는게 맞는걸까? 다시 위의 정렬 알고리즘을 예로 들어서... 거대한 데이터를 스레드 개수만큼 나누어서 분배하고 일괄 처리하는 알고리즘을 새로 만드는 대신, 알고리즘과 데이터 크기는 그대로 두고 스레드 개수에 맞춰서 데이터를 많이 준비한다. 속도가 빨라지는 것을 기대할 수는 없지만 처리량이 스레드 개수만큼 배가 된다. 그렇지 않고 싱글스레드로만 돌렸다면 아무리 데이터의 양 대비 시간복잡도가 로그적으로 증가하는 알고리즘을 사용해도 더 느려졌을 것이다. 즉, 전체 작업을 n분의 1로 스레드에 배분하는 알고리즘보다 원래 알고리즘을 그대로 사용하고 대신 일감을 많이 준비하는쪽이 성능 향상에 좋은 영향을 끼치는 것이다. 알고리즘을 수정한다는 것은 전보다 더 복잡해진다는 것이고 그냥 돌려도 성능이 떨어지는데 멀티스레드로 돌려버리면 스레드간의 간섭으로 성능이 더 떨어질 것이다.


이쯤되면 컴퓨터의 성능이 감당한다면 최대한 많은 스레드를 투입하는것이 성능 향상에 도움이 될 것처럼 보인다. 유감스럽게도 암달의 법칙은 절대 깨질 수가 없다고 이미 선을 그었다. 완전 병렬적으로 작동하지 않는 이상은 전체 작업 시간중에 아주 조금은 싱글스레드로만 돌아가야하는 부분이 있을 것 같다. 그 부분을 0으로 만들지 않고서는 언젠가 스레드를 더 투입해도 성능 향상이 미미해지는 선이 반드시 존재한다. 만약 병렬화율이 95%에 달한다고 해도 2스레드는 1.9배로 거의 두배속이지만 4스레드는 3.4배, 8스레드는 5.9배로 6배에도 못미친다. 특히 계산 속도가 중요한 알고리즘일수록 병렬화 가능성이 낮은데 이는 실시간 물리 엔진을 구현하는데 많은 스레드를 사용하지 않으려는 이유이기도 하다.


결론은 멀티스레드 프로그램을 작성할때는 고려해야 할 부분이 몇가지 있다는 것으로 마친다. 첫째로 싱글스레드로 돌렸을때 그 어떤 방법으로도 더 이상의 성능 개선을 기대할 수 없을 정도로 무거운 작업인지 판단해야 한다. 알고리즘을 최적화할만한 부분이 있는지 고려한다. 엄청나게 거대한 배열 하나를 정렬해야하기 때문에 싱글스레드로 몇초가 걸릴 정도라면 그건 그냥 느려도 싱글스레드로 돌리자. 그렇지 않고 중소규모의 배열이 매우 많다면 멀티스레드로 돌려볼만하다. 다만 거대한 배열을 분할해서 정렬하는 것은 좋은 생각이 아니며 의도한대로 정렬되지 않는다. 두번째는 멀티스레드 알고리즘이다. 작업에 참여하는 스레드의 개수가 많아질수록 실제 처리량보다 오버헤드가 기하급수적으로 커진다면 그런 알고리즘은 절대 쓰면 안된다. 위에서 설명한 정렬 알고리즘을 멀티스레드로 개선하는 것으로 전혀 절대로 좋은 생각이 아니다. 첫번째 고려사항처럼 엄청나게 거대한 배열을 이런 이상한 알고리즘으로 돌리려는 시도는 하지 않는게 좋다.


마지막으로 멀티스레드 프로그램은 버그를 잡기가 굉장히 어렵다. MP 개발이 가능하면서 성능 향상을 기대할 수 있을 정도의 실력을 가진 프로그래머는 흔치 않다. 나도 정말 오랜 시간동안 연구했고 결국 암달의 저주는 깨지 못했다. 대신 산더미처럼 쌓인 데이터 속에서 유일하게 얻어낸 것은 잦은 데드락과 코어 덤프뿐이다.


일단 이 글은 멀티스레드의 장단점을 가리기보단 멀티스레드 처리의 현실을 보여주고자 쓴 글이라 영 희망이 없어 보이지만 다음에 이어서 글을 써볼때는 멀티스레드가 희망이 있는 분야들을 한번 알아보고 예제 코드도 어느정도 작성해볼 '계획'.