1

here is my wonderful & working LISP racket "intermediate with lambda" style recursive function to determine the symbol with the highest value of symbols in a list.

(define maximum
  (lambda [x]
    (cond
      [(empty? x) 0]
      [(cons? x)
           (cond
             [(>= (first x) (maximum (rest x))) (first x)]
             [else (maximum (rest x))]
           )
      ]
    )
  )
)

(check-expect (maximum '(1 2 3)) 3)
(check-expect (maximum '(1)) 1)
(check-expect (maximum '(0)) 0)

How can I check for and optimize runtime?

Is recursion any different in runtime than iteration?

Thank you for your answer!

Kind regards,

Alex Knauth
  • 7,496
  • 2
  • 12
  • 26
Ben Jordan
  • 136
  • 7

2 Answers2

4

There is one main thing that will improve the performance greatly, taking it from exponential to linear time.

Don't re-compute the recursion, save it as an intermediate result.

In the inner cond expression, (maximum (rest x)) is computed twice. Once in the question of the first branch, and once is the answer of the second branch.

(cond
  [(>= (first x) (maximum (rest x))) (first x)]
  [else (maximum (rest x))])

In the common case where the first question is false, (maximum (rest x)) will be re-computed, doubling the work it has to do. Even worse, this doubling can potentially happen at every level of recursion in the worst case when the max is at the end. This is what makes it exponential.

To fix this, you can use local to define and name the intermediate result.

(local [(define maxrst (maximum (rest x)))]
  (cond
    [(>= (first x) maxrst) (first x)]
    [else maxrst]))

This takes the big-O complexity from exponential to linear in the length of the input.

There are other potential optimizations such as taking advantage of tail-calls, but those aren't as important as saving the intermediate result to avoid re-computing the recursion.

This method of improving performance using local definitions is also described in How to Design Programs 2e Figure 100: Using local to improve performance.

Alex Knauth
  • 7,496
  • 2
  • 12
  • 26
1

You can use time-apply to measure runtime. Here's a procedure which will call a given function with a big list and returns the results that time-apply does:

(define (time-on-list f size #:initial-element (initial-element 0)
                      #:trials (trials 10)
                      #:verbose (verbose #f)
                      #:gc-times (gc-times '()))
  (define pre-gc (if (memv 'pre gc-times) #t #f))
  (define post-gc (if (memv 'post gc-times) #t #f))
  (when verbose
    (printf "trials  ~A
pre-gc  ~A (not counted in runtime)
post-gc ~A (counted-in-runtime)~%"
                        trials
                        pre-gc
                        post-gc))
  ;; Intentionally construct a nasty list
  (define ll (list (for/list ([i (in-range size)]) i)))
  (define start (current-milliseconds))
  (when (and post-gc (not pre-gc))
    (collect-garbage 'major))
  (let loop ([trial 0] [cpu 0] [real 0] [gc 0])
    (if (= trial trials)
        (values (/ cpu trials 1.0) (/ real trials 1.0) (/ gc trials 1.0))
        (begin
          (when pre-gc
            (collect-garbage 'major))
          (when verbose
            (printf "  trial ~A at ~Ams~%" (+ trial 1) (- (current-milliseconds)
                                                        start)))
          (let-values ([(result c r g)
                        (time-apply (if post-gc
                                        (λ (l)
                                          (begin0
                                            (f l)
                                            (collect-garbage 'major)))
                                        f)
                                    ll)])
            (loop (+ trial 1) (+ cpu c) (+ real r) (+ gc g)))))))

You can use this with varying values of size to get a feeling for performance. By default it averages over 10 trials but this can be adjusted. You can also ask for GC at various points in the process but probably you should not. This is based on a procedure I use to test performance of things: it's not particularly finished code.

You almost certainly don't want to run this on large values of size for your function: see the other answer. In particular, here are the times for list of length up to 25 with your function:

(0 0 0 0 0 0 0 0 0 0.1 0.1 0.2 0.4 0.9 1.9 3.5 
 6.7 13.6 29.7 54.3 109.8 219.7 436.6 958.1 2101.4)

This should convince you that something is terribly wrong!

tfb
  • 13,623
  • 1
  • 10
  • 24