Reputation: 463
I'm using Project Euler problem 5 brute force method to test out performance tweaking CL code. I'm new to the language and want to know how to do such things. I wrote a C implementation, which runs in about 1.3 seconds. My initial, naive CL implementation runs in around 15 seconds.
Here's my initial CL code
(defpackage :problem-5
(:use #:cl)
(:export :divides-even-p
:has-all-divisors
:smallest-multiple)
)
(in-package :problem-5)
(defun divides-even-p (num divisor)
(= 0 (rem num divisor))
)
(defun has-all-divisors (num start stop)
(loop for divisor from start to stop do
(cond
((not (divides-even-p num divisor)) (return-from has-all-divisors nil))
)
(+ divisor 1)
)
t
)
(defun smallest-multiple (n)
(do ((multiple 1 (+ multiple 1)))
((has-all-divisors multiple 1 n) (format t "~A" multiple))
))
(time (smallest-multiple 20))
The tricks I've read about up til now are 1) optimize for speed and safety, 2) inline functions and 3) explicitly set variable types. Applying those things, I get the following code
(defpackage :problem-5
(:use #:cl)
(:export :divides-even-p
:has-all-divisors
:smallest-multiple)
)
(in-package :problem-5)
(declaim (optimize (speed 3) (safety 0))
(inline divides-even-p)
(inline has-all-divisors)
)
(defun divides-even-p (num divisor)
(declare (type integer num divisor))
(zerop (rem num divisor))
)
(defun has-all-divisors (num start stop)
(declare (type integer num start stop))
(loop for divisor of-type integer from start to stop do
(cond
((not (divides-even-p num divisor)) (return-from has-all-divisors nil))
)
)
t
)
(defun smallest-multiple ()
(do ((multiple 1 (+ multiple 1)))
((has-all-divisors multiple 2 20) (format t "~A" multiple))
))
(time (smallest-multiple))
Now when I run that version, it runs in 7 seconds instead of 15. 2x speed up, so in the right direction. What else can be done to speed it up? The most glaring thing to me is the do loop in smallest-multiple. For one, I couldn't figure out how to specify a type for the multiple variable. How do you do that? Is there a better way to do open ended loops that would produce less code? How would you go about trying to increase the performance from here? The C code runs in about 1.3 seconds, so I'd be happy getting it down to 2 or 3 seconds.
Upvotes: 3
Views: 450
Reputation: 1231
I am not a complete expert in CL but would like to give some advice, which you might find helpful.
General style
You should not leave closing parenthesis on a extra line. See e.g. here for some remarks on style. Furthermore, a documentation string would help others and yourself in the future to understand your code.
Performance
I haven't reviewed my own solution of this problem but I guess specifying fixnum
instead of integer
will lead to another factor of 2 in performance and should be possible for this problem.
Loop
You could write has-all-divisors
more idiomatic using loop
s always
clause:
(defun has-all-divisors (num start stop)
(declare (type fixnum num start stop))
(loop for divisor of-type fixnum from start to stop
always (divides-even-p num divisor)))
Alternative solution
If I remember this problem correctly, you could use another algorithm which should be way faster. Collect all prime factors of the integers from 2 to 20 and build their product.
Upvotes: 4
Reputation: 51501
For one, you can use fixnum
instead of integer
. The latter encompasses all integers, the former only those that fit into a machine word minus a few tag bits (typically something around 61 or 62 bits).
The declaration in a do
loop comes at the start of the body:
(do ((m 1 (1+ m)))
((has-all-divisors m 2 20)
m)
(declare (fixnum m)))
You could also use loop
here:
(loop :for m :of-type fixnum :upfrom 1
:when (has-all-divisors m 2 20)
:do (return m))
Code improvements:
Please don't leave parentheses lying around like nail clippings.
Use if
for a two-branch conditional.
Loop
has an always
keyword:
(loop :for divisor :of-type fixnum :from start :to stop
:always (divides-even-p num divisor))
Upvotes: 8