I have taken Larry's "Game of Life" example that he originally posted
here:
http://groups.google.com/group/clojure/msg/fdfc88f1ba95bdee
...and updated it to use all the CPU's your JVM has access to. My
first attempts ran into the classic map -> pmap slowdown. My next
attempt had too much dosync, in that there were many threads but they
were all waiting for each other.
I finally rewrote the calc-state routine to batch the units of work
into meaty sizes, and to minimize the dosync granularity. It gets the
batches of new-state together, then goes and applies these in batches.
In both events I use pmap.
Now it is running really fast on my 4-core Intel i7. You will really
notice a difference at larger grid sizes. On my i7 it keeps the 8 (due
to hyperthreading) "cpus" busy at about 60% when I run a huge grid.
I also updated paint-cells with a type hint that greatly reduces
reflection in that performance critical code.
I am very new to clojure and would appreciate feedback. I am concerned
I may have overcomplicated things with my usage of the map/pmap form.
I am guessing there may be a simpler way to write what I did.
Note at the start it will automatically select how many "available-
procs" you have. Try tweaking this on your hardware to see how it
impacts performance. Watching the threads with a profiler is
interesting.
Here is is:
(def cells (ref {}))
(def running (ref false))
;(def x-cells ( * 32 4))
;(def y-cells ( * 48 4))
(def x-cells 32)
(def y-cells 32)
(def range-cells (for [x (range x-cells) y (range y-cells)] [x y]))
(def length-range-cells (count range-cells))
(def cell-size 10)
(def life-delay 0)
(def life-initial-prob 3)
(def available-procs (.. java.lang.Runtime getRuntime
availableProcessors))
(defn determine-initial-state [x y]
(= 0 (rand-int life-initial-prob)))
(defn determine-new-state [x y]
(let [alive (count (for [dx [-1 0 1] dy [-1 0 1]
:when (and (not (= 0 dx dy))
(cells [ (mod (+ x dx) x-cells)
(mod (+ y dy) y-cells)]))]
:alive))]
(if (cells [x y])
(< 1 alive 4)
(= alive 3))))
(defn update-batch-of-new-cells [new-cells list-of-batches]
(dosync
(dorun (map #(commute new-cells assoc (first %) (second %))
list-of-batches))
))
(defn calc-batch-of-new-cell-states [cell-state batch-cells]
(doall (map
#(let [new-cell-state (cell-state (first %) (second %))]
[[(first %) (second %)] new-cell-state])
batch-cells)))
(defn calc-state [cell-state]
(let [new-cells (ref {})]
(dorun (pmap #(update-batch-of-new-cells new-cells %)
(pmap #(calc-batch-of-new-cell-states cell-state %)
(for [cpu (range available-procs)] (take-nth available-
procs (drop cpu range-cells))))))
(dosync (ref-set cells @new-cells))))
(defn paint-cells [#^java.awt.Graphics graphics]
(doseq [[[x,y] state] @cells]
(doto graphics
(. setColor (if state Color/RED Color/WHITE))
(. fillRect (* cell-size x) (* cell-size y) cell-size cell-
size))))
(defn toggle-thread [#^JPanel panel button]
(if @running
(do (dosync (ref-set running false))
(. button (setText "Start")))
(do (dosync (ref-set running true))
(. button (setText "Stop"))
(. (Thread.
#(loop []
(calc-state determine-new-state)
(. panel repaint)
(if life-delay (Thread/sleep life-delay))
(if @running (recur))))
start))))
(defn -main[]
(calc-state determine-initial-state)
(let [f (JFrame.)
b (JButton. "Start")
panel (proxy [JPanel] [] (paint [graphics] (paint-cells
graphics)))]
(doto f
(. setLayout (BorderLayout.))
(. setLocation 100 100)
(. setPreferredSize (Dimension. (* cell-size x-cells) (+ 60 (*
cell-size y-cells))))
(. add b BorderLayout/SOUTH)
(. add panel BorderLayout/CENTER)
(. setDefaultCloseOperation JFrame/EXIT_ON_CLOSE)
(. pack)
(. setVisible true))
(. b addActionListener
(proxy [ActionListener] []
(actionPerformed [evt] (toggle-thread panel b))))))