2
\$\begingroup\$

I'm trying to learn Clojure coming from a Java background. I have written a simple implementation of Conway's game of life in both Clojure and Java, trying to keep the overall structure of the code as similar as possible to make them easy to compare.

I'm measuring the number of simulation steps per second and my Java version can easily go as high as 1000 steps per second, but my clojure version barely hits 4 steps per second.

This is my first non trivial clojure program and I think the problem must be me not using clojure correctly rather than clojure being this much slower. Can any Clojure pros here tell me where I'm going wrong?

I'm using seesaw for the Clojure version and regular Swing for the java version.

I know there are faster algorithms for Conway's game of life but right now I'm mostly interested in trying to get to grips with how Clojure works and thought the naive algo would be the best starting point.

Clojure version:

(ns gol.core
  (:gen-class)
  (:use seesaw.core
        seesaw.color
        seesaw.graphics))

(def wCells  100)
(def hCells 100)
(def cellSize 5)
(def cellColor (color 0 255 0))
(def statusBarHeight 20)
(def windowWidth  (* wCells cellSize))
(def windowHeight (+ statusBarHeight (* hCells cellSize)))
(def frameCap 30)
(def maxFrameDuration (quot 1000 frameCap))

(defn vec2d 
  [sx sy fun]  
  (mapv (fn[x](mapv (fn[y] (fun x y)) (range sx))) (range sy)))

(def globalBoard (atom (vec2d wCells hCells (fn [x y] (rand-int 2)))))

(def lastTime (atom (System/currentTimeMillis)))

(defn neighbors
  [board p]
  (let [x (first p)
        y (second p)
        xrange (range (dec x) (+ 2 x))
        yrange (range (dec y) (+ 2 y))]
  (for [i xrange j yrange 
        :let [q [(mod i wCells) (mod j hCells)] ]
        :when (not= p q)] 
    (get-in board q))))


(defn gol
    [board p]
  (let [v (get-in board p)
        n (count (filter pos? (neighbors board p)))]
    (cond
      (= n 3) 1
      (and (= v 1) (= n 2)) 1
      :else 0)))

(defn fps
  []
  (let [previous @lastTime
       current (System/currentTimeMillis)
       diff (- current previous)
       toSleep (- maxFrameDuration diff)] 
    (when (pos? toSleep) (Thread/sleep toSleep))
    (reset! lastTime (System/currentTimeMillis))
    (double (/ 1000 (- (System/currentTimeMillis) previous)))))

(defn painter [c g]
  (let [board @globalBoard
        xrange (range 0 wCells)
        yrange (range 0 hCells)
        red  (color 255 0 0)
        blue (color 0 0 255)]
    (.setColor g red)
    (.drawRect g 0 0 windowWidth (- windowHeight statusBarHeight))
    (.setColor g cellColor)
    (doseq [i xrange j yrange :when (= 1 (get-in board [i j]))]
      (.fillRect g (* cellSize i) (* cellSize j) cellSize cellSize))
        (.setColor g blue)
        (.drawString g (str "Simulations per second: " (fps)) 50 (- windowHeight 5))
      (reset! globalBoard (vec2d wCells hCells (fn [x y] (gol board [x y]))))
))

(def m-canvas
    (canvas :id :mcanvas
          :background :black
          :paint painter))

(defn -main
  [& args]
  (invoke-later
        (-> (frame :title "Game Of Life",
               :width (+ 3 windowWidth), :height (inc windowHeight),
               :content m-canvas,
;                            :on-close :exit
        )
            show!)
    )
)

(def t (timer (fn [e] (repaint! m-canvas)) :delay 1))

Java version:

package gol;

import java.awt.Color;
import java.awt.Graphics;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class GOL {
    private static final int wCells = 100;
    private static final int hCells = 100;
    private static final int cellSize = 5;
    private static final Color cellColor = new Color(0,255,0);
    private static final int statusBarHeight = 20;
    private static final int windowWidth = wCells * cellSize;
    private static final int windowHeight = statusBarHeight + (hCells * cellSize);
    private static final int frameCap = 10;
    private static final int maxFrameDuration = 1000/frameCap;

    private static int[][] globalBoard  = new int[wCells][hCells];
    static {
        Random rng = new Random();
        for (int i = 0;i<wCells;i++) {
            for (int j = 0;j<hCells;j++) {
                globalBoard[i][j] = rng.nextInt(2);
            }
        }
    }

    private static long lastTime = System.currentTimeMillis();

    private static int gol(int[][] board, int x, int y) {
        int v = board[x][y];
        int n = 0;
        for (int i = x-1;i<x+2;i++) {
            for (int j = y-1;j<y+2;j++) {
                int ii = (wCells+i)%wCells;
                int jj = (hCells+j)%hCells;
                int b = board[ii][jj];
                if (!(ii == x && jj == y) && b == 1) {
                    n++;
                }
            }
        }
        if (n==3) return 1;
        if (v==1 && n == 2) return 1;
        return 0;
    }

    private static double fps() {
        long previous = lastTime;
        long diff = System.currentTimeMillis() - previous;
        long toSleep = maxFrameDuration - diff;
        if (toSleep > 0) {
            try { Thread.sleep(toSleep); } catch (InterruptedException e) { e.printStackTrace(); }
        }
        lastTime = System.currentTimeMillis();
        return 1000.0 / (System.currentTimeMillis()-previous);
    }

    static class GUI extends JPanel {
        public void paint(Graphics g) {
            g.setColor(Color.BLACK);
            g.fillRect(0, 0, windowWidth, windowHeight);
            g.setColor(Color.RED);
            g.drawRect(0, 0, windowWidth, windowHeight-statusBarHeight);
            g.setColor(cellColor);
            for (int i = 0;i<wCells;i++) {
                for (int j = 0;j<hCells;j++) {
                    if (globalBoard[i][j] == 1) {
                        g.fillRect(i*cellSize, j*cellSize, cellSize, cellSize);
                    }
                }
            }
            g.setColor(Color.BLUE);
            g.drawString("Simulations per second: "+fps(), 50, windowHeight - 5);
            int[][] newBoard = new int[wCells][hCells];
            for (int i = 0;i<wCells;i++) {
                for (int j = 0;j<hCells;j++) {
                    newBoard[i][j] = gol(globalBoard, i, j);
                }
            }
            globalBoard = newBoard;
        }
    }

    public static void main(String[] args) {
        JFrame frame = new JFrame();
        frame.setTitle("Game Of Life");
        frame.setBounds(0, 0, windowWidth+3, windowHeight);
        frame.setContentPane(new GUI());
        frame.setVisible(true);
        new Timer().schedule(new TimerTask() { public void run() { frame.repaint(); } }, 0, 1);
    }
}
\$\endgroup\$
3
  • \$\begingroup\$ Have you attempted to profile the clojure version? \$\endgroup\$ Commented May 3, 2020 at 18:31
  • 1
    \$\begingroup\$ I did, after finding some reflection warnings I added a type hint of ^java.awt.Graphics to the g argument in painter and now I'm finding most of the time in the clojure version is being spend in the .fillRect method. The java version also calls this method but the clojure version seems to be spenidng an order of magnitude more time there. \$\endgroup\$ Commented May 3, 2020 at 18:50
  • \$\begingroup\$ You might want to improve the question by adding the profile information. \$\endgroup\$ Commented May 3, 2020 at 18:54

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.