java faster than clojure?(probably not)

59 views
Skip to first unread message

Julien

unread,
May 9, 2009, 6:53:45 PM5/9/09
to Clojure
I'm interested to do audio synthesis and eventually audio DSP on the
JVM using the Java sound API and I think that it could be fun to do
that following the functional programming paradigm.
I don't intend to build a huge library but just to try some experiment
on my own to better understand how digital audio synthesis work.

Here is a piece of code that is filling up a byte array putting into
it audio samples calculated from the math sin function.
I'm trying to convert this java code into clojure. While the java
version is almost instantaneous my clojure version is way too slow. So
I figured I must be doing something wrong.

It is assuming that the audio channel is mono and sample size of 16
bits. The sample rate is set to 44100Hz.

-java code
int EXTERNAL_BUFFER_SIZE = 128000;
byte[] sampleArray = new byte[EXTERNAL_BUFFER_SIZE*20];

float frequency = 2000; // Hz
double sampleInterval = frequency/sampleRate;
short sample;
float impulseTrain = 0;
for(int sampleIndex=0;sampleIndex+1<sampleArray.length;sampleIndex+=2)
{
sample = (short) (Short.MAX_VALUE * Math.sin(impulseTrain * 2 *
Math.PI));
sampleArray[sampleIndex] = (byte) (sample >> 8);
sampleArray[sampleIndex+1] = (byte) (sample & 0xFF);

impulseTrain += sampleInterval;
if(impulseTrain>1) impulseTrain -= 1;
}

-my clojure function
(defn byte-array-sound [frequency sample-rate nb-frame]
(let [sample-array (make-array (. Byte TYPE) (* nb-frame 2))
sample-interval (/ frequency sample-rate)
limit (alength sample-array)]
(loop [sample-index 0
impulse-train 0]
(let [sample (short (* (Short/MAX_VALUE) (Math/sin (* impulse-
train 2 (Math/PI)))))]
(aset sample-array sample-index (byte (bit-shift-right sample
8)))
(aset sample-array (+ sample-index 1) (byte (bit-and sample
255))))
(if (< (+ sample-index 2) limit)
(recur (inc (inc sample-index))
(let [next-impl-tr (+ impulse-train sample-interval)]
(if (< next-impl-tr 1)
next-impl-tr (- next-impl-tr 1))))
sample-array))))

With a byte array of 2 560 000 bytes the java version is very fast,
may be because of some optimizations somehow not available from
clojure.
I tried the clojure function with a much smaller byte array of 100 000
bytes and it is very slow.(about 40 sec to return)
May be clojure isn't optimize to handle primitive types as java is or
I need to learn my clojure.

I figured those functions out by myself so there might be another way
to achieve the same thing more efficiently.
My knowledge about audio synthesis, DSP is just growing.

I'd appreciate any suggestions.

David Nolen

unread,
May 9, 2009, 7:03:40 PM5/9/09
to clo...@googlegroups.com
I would set your *warn-on-reflection* flag of course to see if you're missing out on any type hinting.  Also watch out for math ops that involve more than 2 arguments:

;; takes ~400ms on my machine
(time (dotimes [x 1000000]
(* 1 2 3)))

;; takes ~50ms on my machine
(time (dotimes [x 1000000]
(* 1 (* 2 3))))

That's almost an order of magnitude for multiplication. Out of curiosity, how long does that function take to execute in Java?

David Nolen

unread,
May 9, 2009, 7:14:39 PM5/9/09
to Clojure
Also type hinting your math ops like mad on tight loops helps:

;; ~20ms
(time (dotimes [x 1000000]
(* (float 1.0) (* (float 2.0) (float 3.0)))))

;; ~65ms
(time (dotimes [x 1000000]
(* 1.0 (* 2.0 3.0))))

Another 3x increase.

Julien

unread,
May 9, 2009, 7:35:16 PM5/9/09
to Clojure

> Out of curiosity, how long does that function take to execute in Java?

The java version is much faster even with much more work to do, which
put me perplex.
I didn't check how much it takes, must be about 100ms.

Thx for the tips, though that doesn't really improve anything.
My guess is that it has something to do recursion.
I set *warn-on-reflection* but doesn't give me any warning.

Is there another way to do iterations in clojure?
Would trying to compile the code do any good?

David Nolen

unread,
May 9, 2009, 8:08:10 PM5/9/09
to clo...@googlegroups.com
What's a sample set of values you would pass to this function?

Stuart Sierra

unread,
May 9, 2009, 8:30:00 PM5/9/09
to Clojure
One comment, although this has no effect on performance: you don't
need to use the static class fields as functions. That is, you can
write Math/PI and Short/MAX_VALUE instead of (Math/PI) and (Short/
MAX_VALUE).

-Stuart Sierra


On May 9, 6:53 pm, Julien <julien.ma...@gmail.com> wrote:

David Nolen

unread,
May 9, 2009, 8:36:08 PM5/9/09
to clo...@googlegroups.com
(set! *warn-on-reflection* true)

(defn byte-array-sound [frequency sample-rate nb-frame]
  (let [sample-array (make-array (. Byte TYPE) (* nb-frame 2))
sample-interval (/ (float frequency) (float sample-rate))
limit (alength sample-array)]
    (loop [sample-index 0
  impulse-train (float 0)]
      (let [sample (short (* (int (Short/MAX_VALUE)) 
    (Math/sin (* impulse-train 2.0 (Math/PI)))))]
(aset-byte sample-array sample-index (byte (bit-shift-right sample
      8)))
(aset-byte sample-array (+ sample-index 1) (byte (bit-and (int sample)
    (int 255)))))
      (if (< (+ sample-index 2) limit)
(recur (inc (inc sample-index))
      (float
(let [next-impl-tr  (+ impulse-train sample-interval)]
 (if (< next-impl-tr 1)
   next-impl-tr (- next-impl-tr 1)))))
sample-array))))

(time (byte-array-sound 44100 96000 50000))

Takes 90ms on my machine. For some reason byte doesn't inform the compiler about the bit-and op and I had to convert the args int, also I had to type-hint Short/MAX_VALUE. Perhaps Rich Hickey or someone more knowledgeable then me can shed some light on why?

Julien

unread,
May 10, 2009, 7:52:45 AM5/10/09
to Clojure


On May 10, 2:08 am, David Nolen <dnolen.li...@gmail.com> wrote:
> What's a sample set of values you would pass to this function?

(byte-array-sound 2000 44100 100000)
here frequency would be the pitch of the sound generated 2000Hz,
sample-rate set to 44100Hz.

Thank you David, it does work faster now.

I uploaded the whole code to actually play the sound.
http://groups.google.com/group/clojure/web/sample.clj

Julien

unread,
May 10, 2009, 9:35:38 AM5/10/09
to Clojure
here's a improved version of the byte-array-sound function which is
probably easier to understand. based upon a real mathematical formula.

(defn byte-array-sound-2 [frequency sample-rate nb-frame]
(let [sample-array (make-array (. Byte TYPE) (* nb-frame 2))
sample-interval (/ 1 (double sample-rate))
limit (alength sample-array)]
(loop [sample-index 0
t (double 0)]
(let [sample (short (* (int Short/MAX_VALUE) (Math/sin (* (* t
2.0) (* Math/PI frequency)))))]
(aset-byte sample-array sample-index (byte (bit-shift-right sample
8)))
(aset-byte sample-array (+ sample-index 1) (byte (bit-and (int
sample) (int 255)))))
(if (< (+ sample-index 2) limit)
(recur (inc (inc sample-index))
(double (+ t sample-interval)))
sample-array))))

Vincent Foley

unread,
May 10, 2009, 11:58:26 AM5/10/09
to Clojure
Hello Julien,

I am in a similar situation to yours: I am writing a Clojure library
to parse Starcraft replay files, but my Clojure code is very far from
nearly equivalent Java code. Currently, on my home PC, parsing 1,050
files takes ~70 seconds with Clojure and about 12 with Java. The code
is available at this URL: http://bitbucket.org/gnuvince/clj-starcraft/.

The Clojure used to take nearly three minutes (even >10 minutes at the
very beginning) to complete. Here are some performance tips that
helped me shave off a lot of processing time.

- (set! *warn-on-reflection* true). This is one of the most important
thing you can do; the Java reflection library is a lot slower than
direct calls (obviously), so make sure that all the type hints are in
place to have direct calls. Put it at the top of your main script and
fix every warning it gives you. It was, for me, the most important
change to make. It brought down performance from over 10 minutes down
to about 3.

- In tight loops, coerce your numbers to their equivalent Java
numerical primitives.

- let bindings are a lot faster to look up than vars. If you have a
var that is looked up very frequently, consider the following idiom to
close over the var:

(def *my-var* <some value>)
(let [local-my-var *my-var*]
(defn my-fn [x y]
...))

- In tight loops, avoid using destructuring binding if you're binding
from a vector. Consider the following snippets:

user> (let [v [1 2 3]] (time (dotimes [_ 1e6] (let [[a b c] v] (+ a b
c)))))
"Elapsed time: 226.992598 msecs"

user> (let [v [1 2 3]] (time (dotimes [_ 1e6] (let [a (v 0), b (v 1),
c (v 2)] (+ a b c)))))
"Elapsed time: 151.938344 msecs"

The second one is definitely uglier, but I was able to go down from 80
seconds to 70 in my program.

- Like David mentioned earlier, use two parameters for the math
primitives.

- -Xprof and -Xrunhprof:cpu=samples are your friends

- A tip given to me on #clojure by Cark: try to "compile" some of your
code down to fns. I was able to get a very appreciable speed boost by
"compiling" my reader functions to fns (see compile-field and compile-
record in unpack.clj)

I hope some of these help. I realize that not all are directly
applicable to your case, but I figure other people may find them
useful.

Cheers,

Vincent.

On May 9, 6:53 pm, Julien <julien.ma...@gmail.com> wrote:

David Nolen

unread,
May 10, 2009, 1:33:14 PM5/10/09
to clo...@googlegroups.com
This particular case also looks easy parallelize. Using agents you can divide the execution time by number of available CPUs.
Reply all
Reply to author
Forward
0 new messages