Looking for a fast way to draw pixel by pixel and why this code 1000 times slower than java?
Asked Answered
F

1

5

This is a simple code that just color the window 4 times.

Maybe there is something obvious that I don't see.

My goal is to learn computer graphic from scratch and I want to draw pixel by pixel to have full control. I'm looking for a fast way to do it.

Here is the full code.

The relevant clojure part:

(defmacro for-loop [[sym init check change :as params] & steps]
  `(loop [~sym ~init value# nil]
     (if ~check
       (let [new-value# (do ~@steps)]
         (recur ~change new-value#))
       value#)))
(time
 (for-loop
  [k 0 (< k 2) (inc k)]
  (for-loop
   [c 0 (< c 2) (inc c)]
   (for-loop
    [i 0 (< i width) (inc i)]
    (for-loop
     [j 0 (< j height) (inc j)]
     (aset ^ints @pixels (+ i (* j width)) (get cs c))))
   (.repaint canvas))))

The same code in java:

long t = System.currentTimeMillis();
for (int k = 0 ; k < 2; k++) {
  for (int c = 0; c < 2; c++) {
    for (int i = 0 ; i < width; i++) {
      for (int j = 0; j < height; j++) {
        pixels[i + j * width] = cs[c];
      }
    }
    repaint();
  }
}
System.out.println(System.currentTimeMillis() - t);
Fikes answered 21/4, 2018 at 13:24 Comment(1)
This question belongs on codereview.stackexchange.comSelfconscious
H
7

There's a couple problems:

  • If you run lein check, you'll see reflection warnings. You're forcing reflection at runtime which can slow things down. I changed the canvas creation to:

    (defonce canvas (doto (proxy [Frame] []
                            (update [g] (.paint this g))
                            (paint [^Graphics2D g]
                              (.drawImage g, ^BufferedImage image, 0, 0 nil)))
                      (.setSize width height)
                      (.setBackground Color/black)
                      (.setFocusableWindowState false)
                      (.setVisible true)))
    

    Notice the type hints I'm using. It didn't know which overload of drawImage to use, and outright couldn't find the paint method.

  • The main issue however is the use of aset. From aset's docs:

    Sets the value at the index/indices. Works on Java arrays of reference types. Returns val.

    Emphasis mine.

    The problem is that aset doesn't work with primitives. It forces each number to be wrapped as an Integer, then unwrapped again when it's used in the image. This is quite expensive when multiplied over each pixel of an image.

    Change aset to aset-int to use int primitives instead. This takes the execution time down from roughly 20 seconds to half a second.


Honestly though, I can't get it any lower. It's much faster than it was, but is still about 20x slower than the Java version. I've been working on this for almost 2 hours now, and I've hit a wall. Hopefully someone else can squeeze the last bit of time out.

Heirdom answered 21/4, 2018 at 17:58 Comment(11)
thanks. can it get faster? still 20 times slower. I fixed all Reflection warning except for getData with no effectFikes
@Fikes You'll need to do proper benchmarking to get accurate comparisons from here. Once you're measuring less than a second, you can't make reliable comparisons. I'll look it over again though to see if I can see any other areas. For Clojure at least, use Criterium for benchmarking.Heirdom
@Fikes So, I've been doing some benchmarking, and your estimate of 20x slower does seem accurate. I'm getting 200ms for Clojure using Criterium, and roughly 10ms for Java. I'm going to try changing your macro to doseq since that's really what should be used here anyways.Heirdom
@Fikes doseq was actually 7ms slower, which is within the margin of error.Heirdom
thank you for your effort. I think maybe I should give up on clojure and just use cFikes
@Fikes You don't use Clojure if you necessarily need speed. I write Clojure because it's fun to write, and leads to beautiful looking code. It depends on your stance and situation whether or not it's a viable language to use.Heirdom
I know but it would be cool if it was fast too. I'm very tempted to create my own clojure version that compiles to native code but I don't think that I have enough experience to make it fast. Jonathan Blow creating his own language, and it's been almost 4 years and no releaseFikes
@Fikes It's always a tradeoff. The pursuit of the perfect language is endless. I encourage you to reconsider Clojure, but if it's impractical for your purposes, it's impractical.Heirdom
@Fikes I would still write it in Clojure, but write these parts of the code in Java. In your project.clj file of your Leiningen project, you can use the :java-source-paths option to point where your Java source code is. To make this mixture of Clojure and Java code work with the REPL, have a look at the lein-virgil project that will automatically compile and reload your Java classes.Dishevel
@Dishevel I don't quite see how I can do that in a straight forward wayFikes
@raof Well, you create a Leiningen project. Leiningen is a tool that helps you build and manage dependencies of your Clojure project. It also lets you build both Clojure code and Java code that talk to each other. Here is such just a small Leiningen project that I created for another stack overflow question: github.com/jonasseglare/redundant-assignments . For the kind of loop that you want to do, you don't really need Clojure's powers, Java is good enough. And then you can call this code from Clojure. That is a pretty good tradeoff between flexibility and performance.Dishevel

© 2022 - 2024 — McMap. All rights reserved.