2014-11-11 2 views
3

Существует немного модифицированный пример из clojure.org/refsClojure коммутируют и изменять производительность

(defn mod-nth [v i f] (assoc v i (f (v i)))) 
(defn run [oper nvecs nitems nthreads niters] 
    (let [vec-refs (vec (map (comp ref vec) 
         (partition nitems (repeat (* nvecs nitems) 0)))) 
     sum #(reduce + %) 
     swap #(let [v1 (rand-int nvecs) 
        v2 (rand-int nvecs) 
        i1 (rand-int nitems) 
        i2 (rand-int nitems)] 
       (dosync 
        (let [temp (nth @(vec-refs v1) i1)] 
        (oper (vec-refs v1) mod-nth i1 inc) 
        (oper (vec-refs v2) mod-nth i2 dec)))) 
     report #(do 
        (prn (map deref vec-refs)) 
        (println "Sum:" 
        (reduce + (map (comp sum deref) vec-refs))))] 
    (report) 
    (dorun (apply pcalls (repeat nthreads #(dotimes [_ niters] (swap))))) 
    (report))) 

(time (run alter 100 10 10 100000)) 

выход образца

([0 0 0 0 0 0 0 0 0 0] [...]) 
Sum: 0 
([15 -14 -8 57 -26 -12 -49 -29 33 -3] [...]) 
Sum: 0 
"Elapsed time: 1995.938147 msecs" 

Вместо замена уникальных номеров я передающий те из одного векторных элементов к другому ,

Эту операцию можно предположить, как коммутативный так что есть еще одно испытание - это то же самое, за исключением commute используется вместо alter

(time (run commute 100 10 10 100000)) 

с выходом образца, как

([0 0 0 0 0 0 0 0 0 0] [...]) 
Sum: 0 
([8 48 -10 -41 -17 -32 -4 50 -31 88] [...]) 
Sum: 0 
"Elapsed time: 3141.591517 msecs" 

Удивительно первый пример работает примерно в 2 seconds, а второй - 3 seconds

Но как упоминание ред in this SO answer

commute представляет собой оптимизированную версию изменить в те времена, когда порядок вещей на самом деле не имеет значения

Как это может быть оптимизирована в то время как требуется больше времени, чтобы сделать ту же работу в этом простой случай? Какова цель commute?

ответ

3

Я использовал VisualVM контролировать clojure.core функций, участвующих при работе в качестве примера с использованием как alter и commute.

alter

alter

commute

commute

Если моя интерпретация результатов правильны, накопленное время, затраченное на каждой функции показывает, что commute на самом деле быстрее, чем alter , Кажется, накладные расходы всех других операций, которые необходимо выполнить для параллельного запуска кода, - это те, которые испортили производительность.

Код бенчмаркинга довольно хитрый, но time иногда вводит в заблуждение. Информация, предоставленная VisualVm, может даже не быть окончательным словом, хотя профилирование и использование таких инструментов, как criterium, может быть лучшим способом убедиться, что результаты заслуживают доверия.

Другим важным фактом является то, что операции, выполняемые внутри блока dosync, не так долго, поэтому даже если один из них повторяет попытку, дополнительное время, которое это требует, не так уж важно. Добавление небольшой задержки внутри dosync делает разницу между повторной попыткой (alter) и не повторной попыткой (commute) более заметной.

(defn mod-nth [v i f] (assoc v i (f (v i)))) 
(defn run [oper nvecs nitems nthreads niters] 
    (let [vec-refs (vec (map (comp ref vec) 
         (partition nitems (repeat (* nvecs nitems) 0)))) 
     sum #(reduce + %) 
     swap #(let [v1 (rand-int nvecs) 
        v2 (rand-int nvecs) 
        i1 (rand-int nitems) 
        i2 (rand-int nitems)] 
       (dosync 
       (let [temp (nth @(vec-refs v1) i1)] 
        (Thread/sleep 1)      ;; This was added 
        (oper (vec-refs v1) mod-nth i1 inc) 
        (oper (vec-refs v2) mod-nth i2 dec)))) 
     report #(do 
        (prn (map deref vec-refs)) 
        (println "Sum:" 
        (reduce + (map (comp sum deref) vec-refs))))] 
    (doall (apply pcalls (repeat nthreads #(dotimes [_ niters] 
              (swap))))))) 

(time (run alter 100 10 10 5000)) 
;= "Elapsed time: 15252.427 msecs" 
(time (run commute 100 10 10 5000)) 
;= "Elapsed time: 13595.399 msecs" 
+0

Так что вопрос: не следует ли использовать 'alter' over' commute', когда операции транзакции крошечные? – Odomontois

+1

Это хороший вопрос. Я бы не осмелился обобщить, хотя лучший способ, который я знаю, чтобы разобраться в таких вещах, пытается и измеряет результаты. Если бы это был я, я бы выбрал в соответствии с характером выполняемых операций (коммутативным и не коммутативным), и только тогда, если это окажется узким местом, попробуйте с другим. –

+1

@ Odomontois, еще одна вещь, о которой следует помнить, заключается в том, что реализация Clojure может оптимизировать случай «коммутирования» в будущем. Вы даете ему больше информации о вашем коде, чтобы что-то делать, поэтому он _able_ должен быть более эффективным, будь он _actually is_ более эффективным в любой заданной точечной версии (для любого заданного блока кода) или нет. Написание кода, основанного на конкретных тестах производительности, может помочь вам при работе с одной версией, но если вы не проводите повторный бенчмаркинг при появлении новой среды выполнения Clojure ... –

1

Это важно понимать, что оптимизация производится commute именно: commute избегает излишне перезапустив код внутри блока, в ситуациях, когда alter нужно будет выбросить результаты.

Constant-фактор накладных расходов между реализациями commute и alter не указано, так что вы видите здесь, не нарушает ни одна часть спецификации Clojure. Тем не менее, поскольку количество времени, затрачиваемого отдельными транзакциями внутри вашего блока dosync, растет, штраф за использование alter, когда вы могли бы использовать commute, будет расти аналогичным образом.

В общем:

  • Microbenchmarks злы (в том смысле, что они поощряют дурные практики, которые не масштабируются для реального использования). Обратите внимание на поведение производительности в реальных сценариях, а не на надуманных тестовых сценариях.
  • Используйте commute в STM Clojure каждый раз, когда сможете.