Clojure에서는 병렬성(Parallelism)을 지원하는 라이브러리인 Reducers가 있다. Reducers를 이용하면 쓰레드를 이용해 동시에 데이터에 작업을 수행하기 때문에 속도 면에서 이득을 볼 수 있다. Reducers를 이용한 장점은 쓰레드 사용하는 것 외에도 중간 데이터를 만들지 않는다는 또 다른 장점도 있다. 이것은 나중에.


아래 예제는 reduce 함수를 써서 구할 수 있는 결과를 fold를 이용해 속도를 올린 것이다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(require '[clojure.core.reducers :as r])
 
(def nums (vec (doall (range 1000000))))
 
(defn mean-combiner
  ([] {:count 0 :sum 0})
  ([acc cur] (merge-with + acc cur)))
 
(defn mean-reducer
  [acc cur]
  (-> acc
      (update-in [:count] inc)
      (update-in [:sum] + cur)))
 
(defn get-mean
  [nums]
  (r/fold mean-combiner mean-reducer nums))
 
(defn get-mean-r
  [nums]
  (reduce mean-reducer {:count 0 :sum 0} nums))
cs


fold 함수를 제대로 사용하기 위해서는 foldable collection인 vector나 map을 입력해야 한다. 그렇지 않을 경우 non-parellel reduce로 폴백해 실행한다. 결과는 reducible이라는 형태로 리턴하기 때문에 값을 이용하기 위해서는 into 함수나 r/foldcat 함수를 써야 한다. 


위 코드를 실행하면 아래와 같은 결과를 볼 수 있다. 

user=> (time (get-mean nums))

"Elapsed time: 143.152352 msecs"

{:count 1000000, :sum 499999500000}


user=> (time (get-mean-r nums))

"Elapsed time: 390.559946 msecs"

{:count 1000000, :sum 499999500000}


첫번째 실행은 r/fold를 사용한 예이며 아래는 reduce를 사용한 예이다. 응답시간을 보면 fold의 장점을 확실히 알 수 있다. 단순한 경우이지만 데이터가 크고 복잡한 연산이 많을 경우 큰 성능 상의 이점은 더욱 분명해질 것이라고 본다.


fold는 combiner 함수와 reducer 함수를 컬렉션과 함께 인자로 받는데 이 때 reducer 함수는 보통(default) 512개의 단위로 나뉜 컬랙션 조각에 호출되는 함수이고 combiner 함수는 reducer 함수의 결과에 호출돼 결과를 하나로 합치는 함수이다. 예를 들어 입력 데이터가 10개, 데이터 2개마다 쓰래드를 호출한다고 가정하면 reducer함수는 5개 조각에서 각각 실행돼 5개의 결과를 만들어 낸다. 이 5개의 결과가 combiner 함수에 의해 하나의 결과로 합쳐진다. 


위 예제에서는 mean-reducer라는 함수가 각 조각을 이용해  {:count 512 :sum: SOME_VALUE} 라는 결과를 만들어낸다. 조각의 개수 만큼 만들어진 결과에는 mean-combiner 함수가 호출되는데 이 때 merge-with 함수를 써서 맵 프로퍼티들에 + 함수를 호출해 값을 합치게 된다.


좀 더 자세한 내용은 아래의 링크를 참고하세요.


https://clojuredocs.org/clojure.core.reducers/fold

http://clojuredatascience.com/posts/2015-09-12-parallel-folds-reducers-tesser-linear-regression.html



Posted by 코딩새싹
,