이용가능한 병렬컬렉션은 다음과 같습니다.
- parallel arrays - scala.collection.parallel.mutable.ParArray
- parallel ranges - scala.collection.parallel.immutable.ParRange
- parallel hash maps - scala.collection.parallel.mutable.ParHashMap
- parallel hash sets - scala.collection.parallel.mutable.ParHashSet
- parallel hash tries - scala.collection.parallel.immutable.{ParHashMap, ParHashSet}
- parallel vectors - scala.collection.parallel.immutable.ParVector
병렬컬렉션을 순차 컬렉션으로 바뀌려면 seq메서드는 사용하는데 항상 효율적(O(1))입니다. 단순히 par메서드만 붙혀주면 병렬컬렉션을 사용할 수 있지만 잘 파악이 안되서 몇가지 테스트를 해보았습니다.
scala> (1 to 5) foreach println
1
2
3
4
5
일반적인 컬렉션의 경우 위처럼 동작합니다.
scala> (1 to 5).par foreach println
3
4
5
2
1
병렬컬렉션이기 때문에 위처럼 순서가 달라지는 것을 볼 수 있습니다.(실행할 때마다 달라집니다.) 병렬로 동작하는 건 확인했는데 실제로 빠른가가 궁금해서 테스트해보았습니다. REPL의 테스트는 왠지 정확치 않은것 같아서 main클래스를 만들어서 컴파일 후에 테스트를 했습니다.
첫번째 테스트
object SeqForeach extends App {
val startTime = System.currentTimeMillis
(1 to 10000000).toList foreach(_ > 0)
val endTime = System.currentTimeMillis
println("elapsed: " + (endTime - startTime) + "ms")
}
일반적인 컬렉션을 테스트 하기 위한 클래스입니다. 컬렉션을 사용하기 전후의 시간을 계산해서 출력하였습니다.
object ParForeach extends App {
val startTime = System.currentTimeMillis
(1 to 10000000).toList.par foreach(_ > 0)
val endTime = System.currentTimeMillis
println("elapsed: " + (endTime - startTime) + "ms")
}
병렬컬렉션 용 테스트이고 .par만 붙혀주었습니다.
작은 크기로는 제대로 테스트가 되지 않을것 같아서 컬렉션의 크기를 늘렸더니 Heap 사이즈를 1기가로 늘려야만 테스트가 가능했습니다. 아래는 그 결과입니다.(그래프까지 그리기는 좀 귀찮아서..)
SeqForeach
elapsed: 5916ms
elapsed: 5718ms
elapsed: 5892ms
elapsed: 5653ms
elapsed: 5652ms
ParForeach
elapsed: 8380ms
elapsed: 6536ms
elapsed: 6449ms
elapsed: 6528ms
elapsed: 6399ms
elapsed: 5916ms
elapsed: 5718ms
elapsed: 5892ms
elapsed: 5653ms
elapsed: 5652ms
ParForeach
elapsed: 8380ms
elapsed: 6536ms
elapsed: 6449ms
elapsed: 6528ms
elapsed: 6399ms
테스트 결과가 좀 이상합니다. (ㅡㅡ;;) 예상과는 다르게 병렬컬렉션이 오히려 더 오래 걸립니다. 그래서 컬렉션을 병렬로 만드는데 시간이 걸려서 이런 결과가 나온게 아닐까 의심해서 코드를 다시 작성했습니다.
두번째 테스트
object SeqForeach2 extends App {
val col = (1 to 10000000).toList
val startTime = System.currentTimeMillis
col foreach(_ > 0)
val endTime = System.currentTimeMillis
println("elapsed: " + (endTime - startTime) + "ms")
}
object ParForeach2 extends App {
val col = (1 to 10000000).toList.par
val startTime = System.currentTimeMillis
col foreach(_ > 0)
val endTime = System.currentTimeMillis
println("elapsed: " + (endTime - startTime) + "ms")
}
아까랑 동일하지만 컬렉션을 시간을 재기 전에 생성하고 실제 동작에 대해서만 시간계산을 하였습니다.
SeqForeach2
elapsed: 141ms
elapsed: 146ms
elapsed: 141ms
elapsed: 142ms
elapsed: 172ms
ParForeach2
elapsed: 181ms
elapsed: 169ms
elapsed: 171ms
elapsed: 171ms
elapsed: 198ms
elapsed: 141ms
elapsed: 146ms
elapsed: 141ms
elapsed: 142ms
elapsed: 172ms
ParForeach2
elapsed: 181ms
elapsed: 169ms
elapsed: 171ms
elapsed: 171ms
elapsed: 198ms
컬렉션의 생성시간을 제외하니까 시간을 확 줄어들었지만 마찬가지로 병렬컬렉션이 시간이 더 오래 걸립니다. 그래서 Array로 다시 테스트를 해보았습니다.
세번째 테스트
object SeqSum extends App {
val a = new Array[BigInt](10000000)
for (i <- 0 until a.size) a(i) = i
val startTime = System.currentTimeMillis
a.sum
val endTime = System.currentTimeMillis
println("elapsed: " + (endTime - startTime) + "ms")
}
일반적인 천만개짜리 배열을 만들어서 합계를 구하는 계산을 수행했습니다.
object ParSum extends App {
val a = new Array[BigInt](10000000)
for (i <- 0 until a.size) a(i) = i
var b = a.par
val startTime = System.currentTimeMillis
b.sum
val endTime = System.currentTimeMillis
println("elapsed: " + (endTime - startTime) + "ms")
}
동일한데 par만 붙혔습니다. 그 결과는 아래와 같습니다.
SeqSum
elapsed: 1055ms
elapsed: 1041ms
elapsed: 1034ms
elapsed: 1040ms
elapsed: 1075ms
ParSum
elapsed: 857ms
elapsed: 899ms
elapsed: 866ms
elapsed: 824ms
elapsed: 837ms
elapsed: 1055ms
elapsed: 1041ms
elapsed: 1034ms
elapsed: 1040ms
elapsed: 1075ms
ParSum
elapsed: 857ms
elapsed: 899ms
elapsed: 866ms
elapsed: 824ms
elapsed: 837ms
이번에는 병렬컬렉션이 약간 더 빠르게 나옵니다.(정확한 이유는 모르겠지만 리스트의 foreach로 테스트를 하면서 뭔가 다른 영향이 있던게 아닐까 추측할 뿐입니다.) 좀 더 자료를 찾아보니 Scala에 Benckmark trait가 있다는 것을 알게 되었습니다.
네번째 테스트
package Test
import scala.testing.Benchmark
class TestCollection[T <: AnyRef](col :T, f: T=> Unit) extends Benchmark {
override def run = f(col)
}
그래서 다른 소스를 참고하여 위와같은 테스트 클래스를 만들었습니다. Benchmark 트레이트를 상속받고 run 메서드를 오버라이드해서 메서드 바디를 구현하면 이 클래스를 이용해서 runBenchmark(noTimes: Int)메서드실행하면 noTimes만큼 run메서드를 수행하고 밀리초로 수행시간을 List로 리턴해 줍니다. 파라미터는 2개를 받게 만들었습니다. 첫번째는 테스트할 클래스이고 두번째는 수행할 로직을 담고 있는 함수입니다. 여러가지 컬렉션을 받을 수 있도록 하기 위해서 covariance를 사용했습니다.(covariance를 사용할 일이 생길줄이야 ㅡㅡ;;) 사실 모든 컬렉션을 받도록 하고 싶었는데 어떻게 해야하는지 몰라서(Collection의 최상위 타입이 따로 없는것 같아서) 그냥 AnyRef를 사용했습니다.
import Test._
import scala.collection.GenIterable
object TestList extends App {
val totalRuns = 10
val list = List.range(0, 9999999)
def func(col:GenIterable[Int]) { col.foreach(_ > 0) }
println("Time to run Normal Collection: " + new TestCollection(list, func).runBenchmark(totalRuns))
println("Time to run Parallel Collection: " + new TestCollection(list.par, func).runBenchmark(totalRuns))
}
이제 벤치마크용 클래스가 있으므로 일반적인 컬렉션과 병렬 컬렉션을 한꺼번에 테스트하도록 만들었습니다. 앞에서 컬렉션을 생성하고 func함수에 foreach를 수행하도록 만들어서 new TestCollection에 넘겨주었고 총 10번을 수행하도록 하였습니다.
Time to run Normal Collection: List(144, 151, 118, 140, 118, 144, 120, 140, 118, 177)
Time to run Parallel Collection: List(183, 149, 97, 140, 96, 149, 97, 161, 106, 137)
Time to run Parallel Collection: List(183, 149, 97, 140, 96, 149, 97, 161, 106, 137)
사실 앞에서 테스트 했던것과 거의 동일한 수행을 Benchmark를 이용해서 한 것인데 아까와는 다르게 병렬 컬렉션이 좀더 빠르게 나옵니다. 일부 더 느린 경우도 생기긴 하지만 병렬이라고 절대적으로 빠른건 아닐텐니 전체적으로는 좀더 빠르게 나옵니다.(아까와는 왜 다른 결과인지 잘 모르겠습니다.)
다섯번째 테스트
import Test._
import scala.collection.parallel.mutable._
object TestSum extends App {
val totalRuns = 10
val a = new Array[BigInt](10000000)
for (i <- 0 until a.size) a(i) = i
def func(col:Array[BigInt]) { col.sum }
def func2(col:ParArray[BigInt]) { col.sum }
println("Time to run Normal Collection: " + new TestCollection(a, func).runBenchmark(totalRuns))
println("Time to run Parallel Collection: " + new TestCollection(a.par, func2).runBenchmark(totalRuns))
}
이번에 위에서 테스트한 sum 테스트를 TectCollection을 이용해서 다시 작성했습니다.
Time to run Normal Collection: List(1040, 857, 817, 811, 828, 844, 817, 851, 853, 863)
Time to run Parallel Collection: List(867, 854, 828, 833, 819, 826, 788, 828, 810, 832)
Time to run Parallel Collection: List(867, 854, 828, 833, 819, 826, 788, 828, 810, 832)
거의 비슷하긴 하지만 그래도 병렬 컬렉션이 약간(?)이나마 더 빠른것 같긴 합니다.
내부 구현이나 병렬컬렉션에 대해서 깊게 알지는 못해서 더 많은 테스트는 어렵기 때문에 이렇게 사용한다 정도로만 봐주시면 될것 같습니다.
그 밖에 자료
Scala Days 2010에서 Aleksandar Prokopec가 발표한 Scala Parallel Collections영상에서 병렬컬렉션이 어떻게 동작하는지 설명하고 있는데 영어 발음이 이상해서(좋아도 잘 못알아듣지만) 알아듣기 힘들기는 하지만 설명하는 내용은 봐줄만 합니다. 저 영상에서는 벤치마크 결과가 병렬컬렉션이 압도적으로 높게 나오는데 테스트 PC의 환경탓인지 벤치마크 코드를 공개하진 않아서 비교해 보지는 못했네요. 그리고 스칼라가 구현한 병렬컬렉션에 대한 기술리포트인 A Generic Parallel Collection Framework가 공개되어 있습니다.
[참고 자료]
Scala 2.9.0 Parallel Collections
Parallel Arrays in Scala
어디서 읽었는지는 정확히 기억은 안나는데 패러럴 컬렉션은 내부적으로 더그 레아 아저씨의 Fork/Join 을 사용하는거 같아.. ( 조금 더 강화해서 쓴다고 했던가? )
하지만 JDK 6에서는 극히 일부만이 들어가 있는 상황이고, 7부터 본격적으로 지원이 되는거라 그거에 따른 이유도 있을거 같더라... OpenJDK7 설치하고 한번 해보면 어떨까나?
그리고 병렬처리가 가능한 경우에는 처리하지만 그렇지 않으면 일반 순차처리랑 별 다르지 않다고도 했고... 스레드 기반 처리작업시에 유용(?)하다고도 했어.
눈으로 보고 싶으면 스레드 처리를 하는 테스트를 만들어보면 됨!
아하~ 그렇군... 자바 7이 있었네....
그정도 정보면 굳이 테스트는 안해봐도 되겠네..(저거 하는것도 힘들었음...)
저는 두 번째 방법으로 햇을때는 병렬이 더 빠르게 나왔습니다.
그리고 프로그램을 계속 돌릴때마다 heap사이즈를 늘려줘야되더군요...
첫번째 테스트결과가 저는
seq - 10900ms, par - 12888ms
두 번째
seq - 109ms, par - 46ms
세 번째
seq - 2810ms, par - 5772ms
글 쓰신분이랑 너무 다른것같아서요 제가 뭐 잘못했을까 싶어서요...
이클립스에서 스칼라오브젝트 만들고 그안에 소스 붙여넣었거든요
heap사이즈 늘리는건
-Xmx512m
-XX:+UseParallelGC
이렇게 했구요. 조언 부탁드립니다.
글이 par가 처음 도입된 2년정도 전의 내용이라 현재 버전의 Scala에서 얼마나 달라졌는지는 정확히 알지 못합니다. 글에도 썼든이 par의 구현 부분은 정확히 알지 못하고 최근에 Scala를 쓰고 있지 않아서 정확한 도움을 못드릴것 같습니다.
스칼라 유저그룹( https://groups.google.com/forum/#!forum/scala-korea ) 쪽에 질문을 올리시면 좀더 도움을 받으실 수 있을겁니다.
감사합니다^^
죄송하지만 혹 첫번째 테스트에서
foreach(_ > 0) 는 어떻게 돌아가는 건가요?
찾아봐도 잘 이해가 안되네요
_는 * 처럼 쓰인다고 알고있어요
Scala에서 _ 는 다양한 용도로 쓰여서 많이 헷갈리는 부분이기도 합니다.
위 테스트에서 _ > 0는 그냥 루프만 회전하도록 하기 위한거라서 확인해 보시기 쉽게 루프 돌면서 println 으로 순서대로 출력해 보도록 바꿔 보겠습니다.
(1 to 10).toList foreach( println _ )
이를 실행하면 1~10까지 출력이 됩니다.
이 코드를 변경하면
(1 to 10).toList foreach( (x) => {println(x);} )
여기서 첫 파라미터를 바디에서 한번만 사용하는 경우에는 _로 대체할 수 있습니다.
(1 to 10).toList foreach( {println(_);} )
여기서 불필요한 괄호등을 제거하면 아래처럼 쓸 수 있습니다.
(1 to 10).toList foreach( println _ )
비슷한 원리로 ( _ > 0)도 파라미터 하나를 받아서 이 값이 0보다 큰지를 검사해서 리턴하도록 작성한 것입니다. 오래되서 가물가물하지만 _를 하나쓰면 첫 파라미터로 하나 더 쓰면 두번째 파라미터로 매핑될 겁니다.