First off this will depend on your implementation since nested defines can be implemented in more than one way. On my Chez Scheme 9.5 setup I get a rather consistent 25% faster run time when I use odd-internal.
Now, for the why. This happens because nested defines (i.e. internal defines) are wildly different than actual defines.
When you use define
on the top level, you are adding a new record to the free-variables table. Whenever you try to evaluate a variable that is not bound to any lambda, it is looked up in the free-variables (hash) table. This search is very efficient, but it's slower than fetching a bound variable. So when you calculate (odd-external 40000000)
that you fetch even
and odd-external
from that table about 40mil times - even with caching and other cool stuff, this is still work to be done.
In contrast, nested defines create a bound variable. One way they are implemented is as nested lambda/let/letrec expressions. That way the odd-internal
function would be transformed into[1]:
(define (odd-internal x)
(let ((even (lambda (x)
(if (zero? x)
#t
(odd-internal (sub1 x))))))
(if (zero? x)
#f
(even (sub1 x)))))
(Which is a simplification of what Chez Scheme does).
Now every time you apply odd-internal
it's still a free-variable, so you hash it and find it in the free-variables table. However, when you apply even
, you just grab it from the environment (which can cost as little as a single memory dereference even without cool tricks).
A fun experiment would be to define both odd
and even
as bound variables, so all 40mil variable fetches would benefit from quick bound-variable fetch times. I saw a 16% improvement on top of the original 25%. Here's the code:
(define (odd-quick x)
(define (odd x) (if (zero? x) #f (even (sub1 x))))
(define (even x) (if (zero? x) #t (odd (sub1 x))))
(odd x))
[1] let
is a syntactic suger for a lambda
application, so you can read that code as:
(define (odd-internal x)
((lambda (even)
(if (zero? x)
#f
(even (sub1 x))))
(lambda (x)
(if (zero? x)
#t
(odd-internal (sub1 x))))))
let
is incorrect.let
is a function application itself. – Blau