Pages

August 25, 2014

Scala's parallel collections (병렬 콜렉션)에 관한 소회

스칼라 2.9 버전에서 부터 parallel collections가 소개되었다. 그리고 기존에 존재하는 모든 collections을 쉽게 parallel 버전으로 만드는 법도 같이 소개 되었는데 기존의 collection 에 par 함수를 호출하는 것만으로도 쉽게 변환이 가능하다.
예를 들어 순차적인 버전의 아래 내용은
scala> (1 to 5) foreach println
1
2
3
4
5
병렬 버전을 사용하면 결곽값이 다음과 같이 바뀐다.
scala> (1 to 5).par foreach println
1
4
4
3
5
(실행 할때 마다 값은 변경 될 수 있다.)
이런 병렬 콜렉션을 사용하면서 얻을 수 있는 이점을 알아 보기 이전에 이 구조가 가진 문제점을 먼저 파악해보도록 하겠다.

컨트롤이 가능하지 않다.

처음에 병렬 컬렉션의 결과를 접했을 때 가장 먼저 알고 싶었던 것은 스레드가 생성이 되어 작업을 처리했다는 것은 명백한데 얼마나 많은 스레드가 이 연산에 관여 되었는지를 알 수 없다는 것은 큰 문제로 보였다. 위의 경우 1부터 5까지 5개의 숫자를 프린트하는 구문인데 이렇게 5개의 아이템을 다루는 경우 5개의 스레드가 생성이 되는 것인지? 만일 1부터 10,000까지를 처리하는 구문이라면 10,000개의 스레드가 생성이 되는 것인지? 불분명하다. 정답은 스레드 풀을 사용한다 인데 그 스레드 풀에는 도대체 몇개의 스레드가 들어 있는 것인지 어느것도 명확하게 드러나는 것이 없다. 만약 스레드 풀에 10개의 스레드가 있는데 11번째 아이템을 처리를 할 순서가 되었다면 그 처리는 블럭이 되는 것인지? 만약 블럭이 되는 구조라면 어떤 것을 먼저 처리하고 어떤 것을 나중에 할지 설정이 가능한가? 에를 들어 아주 오래 걸리는 작업 하나가 빨리 처리되는 다른 여러개의 작업이 시작되지 못하도록 막고 있다면 어떻게 하나?
이러한 컨트롤의 부재가 실제 병렬 프로그래밍에서는 큰 위험요소가 된다. 따라서 이 병렬 컬렉션의 사용을 아주 심플한 위의 예제와 같은 수준 정도의 사용으로 그치고 만다면 병렬 컬렉션을 통해 얻을 수 있는 퍼포먼스의 이득은 아주 미미할 것이다.

특효에 대한 환상

과거 온라인에서 병렬 콜렉션이 동작하지 않는다는 불평들을 몇몇 접할 수 있었는데 그들의 코드를 살펴보면 대부분 알고리즘이 병렬처리가 불가능하게 짜여져 있음을 알 수 있었다. 그리고 그들 조차 그것을 인식하지 못하고 있던가, 아니면 par 함수가 마법과 같이 그것을 병렬적으로 동작하도록 만들어 줄 것이라고 믿고 있었다.
scala> Set(1,2,3,4,5) mkString(" ")
res1: String = 5 1 2 3 4

scala> Set(1,2,3,4,5).par mkString(" ")
res2: String = 5 1 2 3  4

scala> Set(1,2,3,4,5).par mkString(" ")
res3: String = 5 1 2 3 4

scala> (1 to 6).par mkString(" ")
res4: String = 1 2 3 4 5 6

scala> (1 to 6).par mkString(" ")
res5: String = 1 2 3 4 5 6
par 버전의 예제를 반복하여 실행하여 보아도 결과는 똑같음을 알 수 있다. 뭔가 이상해 보인다. 이번 예제에는 Set 을 사용하였고 Set 내에 들어 있는 아이템의 순서는 상관이 없다고 선언한 것이나 마찬가지인데 그 결과는 항상 똑같음을 볼 수 있다. 그리고 순차적 버전도 마찬가지로 항상 똑같은 결과를 출력한다. 이것은 명백히도 모든 연산이 병렬화되지 않음을 의미한다. (예를 들어 fold 같은 것들) 그러나 그렇게 되어야 할 것 처럼 보인다. 여기서 왜 그렇게 되지 않는지 자세히 설명하지는 않겠다. 그냥 그것이 folds함수나 set, 스칼라의 상속 구조 그리고 mkString함수의 명세에 따라 병렬처리가 불가능하다는 것을 이해하였으면 좋겠다. 그러나 여기서 중요한 점은 병렬 콜렉션에 대한 잘못된 환상이 비 직관적인 결과에 이를 수 있다는 점이다.

결론

기존 콜렉션에 대한 처리를 par함수를 통해 병렬 버전으로 바꾸어 넣는 것은 명백한 실수라고 생각한다. 병렬 처리 연산은 순차적 콜렉션에 들어 맞지 않는 연산 집합을 가지고 있고 모든 콜렉션이 par를 지원하지도 않으며 이 기능에 신경을 쓰지 않는 다른 사람들에게 부담을 안겨줄 위험을 가지고 있다.
그리고 이 병령 컬렉션의 추가로 인해 스칼라는 내가 생각하기에 중요한 프로그래밍 언어와 API의 일반적인 률을 위반했다고 생각한다. 그 률은 '쓰이지 않을 것에 비용을 지불하지 말라' 라는 규칙인데 병렬 콜렉션이 단지 Jar파일에 몇MB 추가 된 것으로 그치지 않고 언어의 복잡성과 앞으로 이 언어를 유지 보수할 사람들에게 큰 짐을 안겨주엇다고 생각한다. 앞으로 누군가 순차적 콜렉션을 수정하고자 하면 그 수정 사항이 병령 콜렉션에게 영향이 없는지 검증을 해야 할 것이고 그 반대도 그렇다.
스칼라 2.9는 꽤 최근에 발표된 버전이고 실제 세계에서 어느 정도의 이점을 얻고 있는지 정량적으로 파악이 안되는 것은 당연하다. 그러나 내가 예언하건데 용감한 개발자가 병렬 콜렉션의 기능들을 자신의 코드 베이스를 수정하는 노력을 기울여 얻을 수 있는 이득은 매우 적을 것이다. 그리고 더 나아가 콜렉션 내부를 루프를 돌면서 스레드를 생성하는 것은 이미 빠르게 동작하는 아주 적은 수의 요소를 가진 콜렉션에 대해 컨택스트 스위칭, 메모리 낭비, 캐시 미스등 많은 부정적인 사이드 이펙트를 낳게 될 것이다. 여기서 분명히 밝히지만 나는 어떤 정량적인 테스트를 하지 않은 상태로 얘기하는 것이라 내가 완전히 틀릴 수도 있다.
이런 문제점들 때문에 병렬 콜렉션의 추가에 대해 흥분하지 않는 이유이며 누군가 이것의 유용함에 대해 나의 무지를 깨우쳐줄 수 있으면 좋을것 같다.
여기서 내가 추천하는 병렬 콜렉션의 사용방법은
  • 병렬 콜렉션 버전과 순차적 콜렉션 버전을 완전히 분리하여 다른 용도로 사용하는 것을 추천한다.
  • Excutor 프레임웍을 구현하는 스칼라 래핑을 제공하여 저 수준의 병렬화를 구현하였으면 한다. 스레드 풀 사이즈도 조절이 가능하고 스레드 풀 자체도 변경이 가능하며 라이프 사이클등 모든 것을 컨트를 할 수 있는 형태로 말이다. 만약 시도한다면 몇 백 라인 정도로 이를 구현할 수 있을 것이며 par를 통해 가능한 것 보다 훨씬 더 유용할 것이다.

No comments:

Post a Comment