0

I'm trying to convert Python -> CL for a program that calculates x,y color values for the Mandelbrot set.

The python version takes about 1.2 seconds to complete. I tried using the CL cookbook for performance tips, but my CL attempt is taking an eternity, so I'm doing something very wrong.

How can I improve its performance?

Python:

import time

t0 = time.time()

WIDTH = 800
HEIGHT = 600

PIXELS = [[None] * WIDTH for _ in range(HEIGHT)]

# Plot window
RE_START = -2
RE_END = 1
IM_START = -1
IM_END = 1

MAX_ITER = 80

def mandelbrot(c):
    z = 0
    n = 0
    while abs(z) <= 2 and n < MAX_ITER:
        z = z*z + c
        n += 1
    return n

for x in range(0, WIDTH):
    for y in range(0, HEIGHT):
        c = complex(RE_START + (x / WIDTH) * (RE_END - RE_START),
                    IM_START + (y / HEIGHT) * (IM_END - IM_START))
        m = mandelbrot(c)
        color = 255 - int(m * 255 / MAX_ITER)
        PIXELS[y][x] = color

t1 = time.time()

print(t1-t0)

CL (SBCL):

(require :sb-sprof)

(declaim (optimize speed))

(defparameter *WIDTH* 800)
(defparameter *HEIGHT* 600)
(defparameter *PIXELS* (make-array `(,*WIDTH* ,*HEIGHT*) :initial-element 0))
(defparameter *MAX-ITER* 80)
(defparameter *RE-START* -2)
(defparameter *RE-END* 1)
(defparameter *IM-START* -1)
(defparameter *IM-END* 1)

(defun coord-to-complex (x y)
  (complex (+ *RE-START* (* (/ x *WIDTH*) (- *RE-END* *RE-START*)))
                     (+ *IM-START* (* (/ y *HEIGHT*) (- *IM-END* *IM-START*)))))

(defun mandelbrot (c)
  (let ((z 0)
        (n 0))
    (loop while (and (<= (abs z) 2) (< n *MAX-ITER*))
          do (setf z (+ (* z z) c)
                   n (1+ n)))
    n))

(defun calculate-mandelbrot (array)
  (loop for x from 0 below *WIDTH* do
    (loop for y from 0 below *HEIGHT* do
          (let* ((c (coord-to-complex x y))
                 (m (mandelbrot c))
                 (color (- 255 (/ 255 (* m *MAX-ITER*)))))
           (setf (aref array x y) color)))))


(sb-sprof:with-profiling (:max-samples 1000
                          :report :flat
                          :loop nil)
  (calculate-mandelbrot *PIXELS*))

3
  • @Mat thank you for the catch, i had been playing with re-arranging that function and borked it at some point Commented Jun 7, 2023 at 10:46
  • @Mat (/ MAX-ITER) is valid, it is 1/*MAX-ITER* Commented Jun 7, 2023 at 10:58
  • 1
    To make arithmetics fast in CL you need to use type annotations and ensure the numbers are within bounds... that lets the compiler turn the number operations into CPU instructions that are very fast... I wrote about it on my post here, see the "Optimizing Common Lisp" section: renato.athaydes.com/posts/revenge_of_lisp-part-2.html Commented Jun 20, 2023 at 14:23

1 Answer 1

3

In coord-to-complex, you force the system to work with fractions. Try to replace it with

(defun coord-to-complex (x y)
  (complex (+ *RE-START* (* (/ x 1.0 *WIDTH*) (- *RE-END* *RE-START*)))
           (+ *IM-START* (* (/ y 1.0 *HEIGHT*) (- *IM-END* *IM-START*)))))

so that it uses floats.

There are other possible improvements (e.g., declaring types), but this itself makes it finish in second or so.

After fixing most of the sbcl optimization warnings it can get almost order of magnitude faster, see below (I picked single floats for calculation, double floats should be about same).

(defparameter *WIDTH* 800)
(defparameter *HEIGHT* 600)
(defparameter *PIXELS* 
    (make-array `(,*WIDTH* ,*HEIGHT*)
                :initial-element 0s0
                :element-type 'single-float))
(defparameter *MAX-ITER* 80)
(defparameter *RE-START* -2s0)
(defparameter *RE-END* 1s0)
(defparameter *IM-START* -1s0)
(defparameter *IM-END* 1s0)

(defun coord-to-complex (x y)
  (declare (fixnum *width* *height* x y)
           (single-float *re-end* *re-start* *im-end* *im-start*))
  (complex (+ *RE-START* (* (/ x 1s0 *WIDTH*) (- *RE-END* *RE-START*)))
           (+ *IM-START* (* (/ y 1s0 *HEIGHT*) (- *IM-END* *IM-START*)))))

(defun mandelbrot (c)
  (let ((z #C(0s0 0s0))
        (n 0))
    (declare ((complex single-float) z c)
             (fixnum n *max-iter*))
    (loop while (and (<= (abs z) 2) (< n *MAX-ITER*))
          do (setf z (+ (* z z) c)
                   n (1+ n)))
    n))

(defun calculate-mandelbrot (array)
  (declare ((simple-array single-float) array)
           (fixnum *max-iter*))
  (loop for x fixnum from 0 below *WIDTH* do
    (loop for y fixnum from 0 below *HEIGHT* do
      (let ((color (- 255 (* (mandelbrot
                              (coord-to-complex x y))
                             255 (/ 1s0 *MAX-ITER*)))))
        (setf (aref array x y) color)))))
Sign up to request clarification or add additional context in comments.

1 Comment

You can also define constants via defconstant, not defparameter, but it would not help much.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.