Mandelbrot Set implementation in Common Lisp

Some remarks about your code:

  • mandelbrot: lacks declarations, squares are computed twice in the loop

  • mandelbrot: in the computation for TMPREAL you are using the new value of TMPCLX, not the old one

  • You don't want to use METHODS to set pixels. SLOW.

  • FLOORDIV is one of FLOOR or TRUNCATE (depending on what you want) in Common Lisp, see (FLOOR 10 3)

  • use type declarations

  • in writexbm don't repeatedly call DATA and ARRSIZE

  • setpixel, unsetpixel looks very expensive, again repeatedly dereferencing the structure

  • draw-mandelbrot has a lot of repeated computations that can be done once

  • Common Lisp has 2d arrays which simplify the code

  • Common Lisp has complex numbers, which also simplify the code

  • a variable name 'self' makes no sense in Common Lisp. Name it to what it is.

Generally the code is full of waste. It makes little sense to benchmark your code, since it is written in a style that hopefully nobody uses in Common Lisp. Common Lisp has been designed with experience of large mathematical software like Macsyma and allows to write mathematical code in a straight forward way (no objects, just functions over numbers, arrays, ...). The better compilers can take advantage of primitive types, primitive operations and type declarations. Thus the style is different from what one might write in Python (which usually either is object-oriented Python or calls to some C code) or Ruby. In heavy numerical code it is usually not a good idea to have dynamic dispatch like with CLOS. Setting pixels in bitmaps via CLOS calls in a tight LOOP is really something one wants to avoid (unless you know how to optimize it).

The better Lisp compilers will compile the numeric functions to direct machine code. During the compilation they give hints which operations are generic and can't be optimized (until the developer adds more type information). The developer can also 'DISASSEMBLE' the functions and check for code that is generic or does unnecessary function calls. "TIME' gives runtime information and also informs the developer about the amount of memory 'consed'. In numeric code consing of 'floats' is a usual performance problem.

So, to sum up:

  • if you write code and think that it does the same in different languages, when the code looks similar or has a similar structure, this might not be the case - unless you really know both languages and both language implementations.

  • if you write code in one language and port it in a similar style to a different language, you may miss a whole existing culture to write solutions to these kinds of problems in a different way. For example one can write code in C++ in an object-oriented style and port it in a similar way to FORTRAN. But no one writes such code in FORTRAN. Written in FORTRAN style, will typically result in faster code - especially since the compilers are heavily optimized for idiomatic FORTRAN code.

  • "when in rome, speak like the romans"

Example:

in SETPIXEL there is a call to (first (dim self)). Why not make that value a slot in the structure in the first place, instead of doing a list access all the time? But then the value is constant during the computation. Still the structure is passed, and the value is retrieved all the time. Why not just get the value outside the main loop and pass it directly? Instead of doing multiple computations of it?

To give you an idea how code might be written (with type declarations, loops, complex numbers, ...), here is a slightly different version of the mandelbrot computation.

The core algorithm:

(defvar *num-x-cells* 1024)
(defvar *num-y-cells* 1024)
(defvar *depth* 60)


(defun m (&key (left -1.5) (top -1.0) (right 0.5) (bottom 1.0) (depth *depth*))
  (declare (optimize (speed 3) (safety 0) (debug 0) (space 0)))
  (loop with delta-x-cell float = (/ (- right left) *num-x-cells*)
        and  delta-y-cell float = (/ (- bottom top) *num-y-cells*)
        and field = (make-array (list *num-x-cells* *num-y-cells*))
        for ix fixnum below *num-x-cells*
        for x float = (+ (* (float ix) delta-x-cell) left)
        do (loop for iy fixnum below *num-y-cells*
                 for y = (+ (* (float iy) delta-y-cell) top)
                 do (loop for i fixnum below depth
                          for z of-type complex = (complex x y)
                          then (+ (complex x y) (* z z))
                          for exit = (> (+ (* (realpart z) (realpart z))
                                           (* (imagpart z) (imagpart z)))
                                        4)
                          finally (setf (aref field ix iy) i)
                          until exit))
        finally (return field)))

Above function returns a 2d array of numbers.

Writing an xbm file:

(defun writexbm (array pathname &key (black *depth*))
  (declare (fixnum black)
           (optimize (speed 3) (safety 2) (debug 0) (space 0)))
  (with-open-file (stream pathname :direction :output :if-exists :supersede)
    (format stream "#define mandelbrot_width ~d~&"  (array-dimension array 0))
    (format stream "#define mandelbrot_height ~d~&" (array-dimension array 1))
    (format stream "#define mandelbrot_x_hot 1~&")
    (format stream "#define mandelbrot_y_hot 1~&")
    (format stream "static char mandelbrot_bits[] = {")
    (loop for j fixnum below (array-dimension array 1) do
          (loop for i fixnum below (truncate (array-dimension array 0) 8)
                for m fixnum = 0 then (mod (1+ m) 8) do
                (when (zerop m) (terpri stream))
                (format stream "0x~2,'0x, "
                        (let ((v 0))
                          (declare (fixnum v))
                          (dotimes (k 8 v)
                            (declare (fixnum k))
                            (setf v (logxor (ash (if (= (aref array
                                                              (+ (* i 8) k) j)
                                                        black)
                                                     1 0)
                                                 k)
                                            v)))))))
    (format stream "~&}~&")))

Above function takes an array and a pathname and writes the array as XBM file. One number 'black' will be 'black' and the other numbers are 'white'

Call

(writexbm (m) "/tmp/m.xbm")

I'm not sure this part is correct:

        (setq tmpcplx (+ (* (* tmpreal tmpcplx) 2) cplx))
        (setq tmpreal (+ (- (* tmpreal tmpreal) (* tmpcplx tmpcplx))
           real))

Isn't tempcplx being overwritten with its new value on the first line, meaning that the second line is using the new value, not the original one?

In the Python version you're avoiding this problem by using tmpb:

  tmpb = tmpcplx
  tmpcplx = tmpreal*tmpcplx*2
  tmpreal = tmpreal*tmpreal - tmpb*tmpb
  tmpcplx += cplx
  tmpreal += real

It seems to me the Lisp version should do something similar, i.e. store the original value of tmpcplx first, and use that store for the calculation of tmpreal:

        (setq tmpb cplx)
        (setq tmpcplx (+ (* (* tmpreal tmpcplx) 2) cplx))
        (setq tmpreal (+ (- (* tmpreal tmpreal) (* tmpb tmpb))
           real))