2017-11-17 14 views
5

(질문에 대한 답변 : Fernando Abrao)Clojure에서 트랜스 듀서로 '주파수'를 어떻게 수행 할 수 있습니까?

나는 Clojure에서 트랜스 듀서의 성능 이점에 대해 듣고 있지만 어떻게 사용해야하는지 잘 모르겠습니다. ,

[ 
    { :samplevalue 1.3, ... }, 
    { :othervalue -27.7, ... }, 
    { :samplevalue 7.5, ... }, 
    { :samplevalue 1.9, ... }, 
] 

내가 각 정수 통에 얼마나 많은 :samplevalue의 가을을보고 싶습니다

은 내가 지금처럼 소수 :samplevalue를 포함 일부지도의 순서를 반환하는 qos/device-qos-range 기능을 말해봐 같은 :

(frequencies 
    (reduce #(if (not (nil? (:samplevalue %2))) 
      (conj %1 (.intValue (:samplevalue %2)))) 
      [] 
      (qos/device-qos-range origem device qos alvo inicio fim))) 

;; => {1 2, 7 1} 

어떻게이 (예 reduce에 의해 반환되는 것과 같은) 중간 데이터 구조를 제거 트랜스 듀서와 빠른 버전으로 전환 할 수 있습니까? 병렬 처리를 위해 여러 코어를 활용할 수있는 코드에 대한 보너스 포인트.

답변

5

(답 신용 :. 렌조 Borgatti (@reborg))

첫째,의는 우리가 나중에 성능 테스트에 사용할 수 있습니다 일부 샘플 데이터를 설정할 수 있습니다. 이 벡터는 동일한 키를 가진 500k 개의 맵을 포함합니다. 값은 시간의 1/5과 겹치고 있습니다.

(def data 
(mapv hash-map 
     (repeat :samplevalue) 
     (concat (range 1e5) 
       (range 1e5) 
       (range 1e5) 
       (range 1e5) 
       (range 1e5)))) 

이제 트랜스 듀서로 변환 해 봅시다. 이 솔루션은 이 아니고 병렬임을 유의하십시오. .intValueint으로 줄였습니다. 동일한 일을합니다. 또한 각지도에서 조건부로 :samplevalue을 가져 오는 것이 (keep :samplevalue sequence)으로 단축 될 수 있으며 이는 (remove nil? (map :samplevalue sequence))과 같습니다. 벤치마킹하려면 Criterium을 사용합니다. 우리가 외부 단계로 frequencies이 시간을 호출하지 않을

(require '[criterium.core :refer [quick-bench]]) 
(quick-bench 
    (transduce 
    (comp 
     (keep :samplevalue) 
     (map int)) 
    (completing #(assoc! %1 %2 (inc (get %1 %2 0))) persistent!) 
    (transient {}) 
    data)) 
;; My execution time mean: 405 ms 

참고. 대신, 우리는 그것을 작업에 포함 시켰습니다. 그리고 frequencies이하는 것처럼, 추가 성능을 위해 일시적인 해시 맵에서 작업을 수행했습니다. 우리는 시드로 일시적인 해시 맵을 사용하고 completing을 호출하여 persistent!을 호출하여 최종 값을 구합니다.

우리는 이것을 평행하게 만들 수 있습니다. 최대 성능을 위해 불변의 Clojure 데이터 구조 대신에 가변적 인 Java ConcurrentHashMap을 사용합니다.

(require '[clojure.core.reducers :as r]) 
(import '[java.util HashMap Collections Map] 
     'java.util.concurrent.atomic.AtomicInteger 
     'java.util.concurrent.ConcurrentHashMap) 

(quick-bench 
    (let [concurrency-level (.availableProcessors (Runtime/getRuntime)) 
     m (ConcurrentHashMap. (quot (count data) 2) 0.75 concurrency-level) 
     combinef (fn ([] m) ([_ _])) ; just return `m` from the combine step 
     rf (fn [^Map m k] 
      (let [^AtomicInteger v (or (.get m k) (.putIfAbsent m k (AtomicInteger. 1)))] 
       (when v (.incrementAndGet v)) 
       m)) 
     reducef ((comp (keep :samplevalue) (map int)) rf)] 
    (r/fold combinef reducef data) 
    (into {} m))) 
;; My execution time mean: 70 ms 

는 여기에서 우리는 병렬 처리를 달성하기 위해 clojure.core.reducers 라이브러리에서 fold를 사용합니다. 병렬 컨텍스트에서 하나의 트랜스 듀서는 무 상태이어야합니다. 또한 ConcurrentHashMapnil을 키 또는 값으로 사용하는 것을 지원하지 않습니다. 다행히도 여기서는 그렇게 할 필요가 없습니다.

출력은 끝에서 불변의 Clojure 해시 맵으로 변환됩니다. 해당 단계를 제거하고 내 컴퓨터에서 ConcurrentHashMap 인스턴스를 사용하여 추가 속도 향상을 수행하면 단계를 제거하면 전체 fold에 대해 약 26ms가 소요됩니다.

편집 2017년 11월 20일는 : 사용자 @clojuremostly 올바르게 대답은 이전 버전의 벤치 마크가 사용되는 것을 의미 동시 해시 맵 인스턴스를 초기화 let 블록, 내부 quick-bench에 호출을 한 것으로 지적 모든 실행에 대해 동일한 인스턴스. quick-bench로 전화를 걸어 let 블록 외부로 이동했습니다. 결과에 큰 영향을 미치지 않았습니다.

+0

두 번째 벤치 마크에서 실행 중에 ConcurrentHashMap을 재사용해야한다고 생각하지 않습니다. – ClojureMostly

+0

@ ClojureMostly - 잘난 척, 고마워! 답변을 업데이트했습니다. 마지막 단락을 참조하십시오. 타이밍은 크게 변하지 않았습니다. –