Understanding Performance Optimization in Functional Programming with Lisp
This guide delves into optimizing performance in Lisp programming, highlighting the distinction between writing fast programs and writing programs quickly. It emphasizes the importance of identifying and optimizing bottlenecks—usually a small percentage of the code that consumes most execution time. Optimization strategies include using efficient algorithms, careful compilation settings, and type declarations for improved efficiency. The document also addresses garbage avoidance techniques, offering practical insights for enhancing speed in functional programming projects.
Understanding Performance Optimization in Functional Programming with Lisp
E N D
Presentation Transcript
11 Speed & Debug Functional Programming
Speed - Introduction • Lisp is really two languages: • A language for writing fast programs • A language for writing programs fast • In the early stage, you can trade speed for convenience. Then, you can refine critical portions to make them faster once your program begins to crystallize
Speed - The Bottleneck Rule • Optimization • Should be focused on bottlenecks • Should not begin too early • Should begin with algorithms • Programs tend to have a few bottlenecks that account for a great part of the execution time • “Most of the running time in none-IO-bound programs is concentrated in about 3% of the source text. Optimizing these parts of the program will make it run noticeably faster; optimizing the rest of the program will be a waste of time on comparison.”, by Knuth
Speed - The Bottleneck Rule • Optimize the program from the top • Make sure that you’re using the most efficient algorithm before you resort to low-level coding tricks • Decisions about algorithms have to be made early
Speed - Compilation • Five parameters control the way your code is compiled • Speed: the speed of the code produced by the compiler • Compilation-speed: the speed at which your program will be compiled • Safety: the amount of error-checking done in the object code • Space: the size and memory needs of the object code • Debug: the amount of information retained for debugging
Speed - Compilation • The compilation parameters are not real variables. They are assigned weights from 0 (unimportant) to 3 (most important) in declarations • (defunbottlenect (…) (do (…) (…) (do (…) (…) (declare (optimize (speed 3) (safety 0))) …))) • Add such declarations until the code was finished and tested
Speed - Compilation • Inline function • Without the cost of calling functions • Similar to macros • Macro • Since macros use mere textual substitution, this may result in unintended side-effects and inefficiency due to re-evaluation of arguments and order of operations • Compiler errors within macros are often difficult to understand, because they refer to the expanded code • Recursive functions cannot be inlined • If an inlined function is redefined, we have to recompile any function that calls it
Speed - Compilation • (declaim(inline single?))(defun single? (lst) (and (consplst) (null (cdrlst)))) • (defunfoo (x) (single? (bar x))) • When foo is compiled(defunfoo (x) (let ((lst (bar x))) (and (consplst) (null (cdrlst)))))
Speed - Type Declarations • In most languages, you have to declare the type of each variable, and the variable can only hold values of that type → strongly typed language • Common List uses manifest typing (run-time typing) • Type information is attached to the data objects • Type information is used at run-time • Variables can hold objects of any type • We have to pay for this flexibility in speed • The function have to look at the types of each of its arguments at run-time • If we just want one type, say, fixnum, this is an inefficient way
Type Declarations • In Common Lisp, type declarations are completely optional • Global declarations are made with declaim • (declaim (typefixnum *count*)) ;type can be omitted • Local declarations are made with declare • (defun poly (a b x) (declare (fixnum a b x)) (+ (* a (expt x 2)) (* b x)))
Type Declarations • Type declarations are particularly important for the contents of complex objects, including arrays and structures • Declarations can improve efficiencyThe compiler can determine the types of arguments to functions and represent these objects more efficients • If nothing is known about the type of elements an array will contain, it has to be represented in memory as a block of pointers • If it is known that the array will only contain, say, double-floats, then the array can be represented as a block of actual double-floats
Type Declarations • (setf x (vector 1.234d0 2.345d0 3.456d0) y (make-array 3 :element-type ‘double-float) (aref y 0) 1.234d0 (aref y 1) 2.345d0 (aref y 2) 3.456d0)
Speed - Type Declarations • (setf a (make-array ‘(1000 1000) :element-type ‘single-float :initial-element 1.0))(defunsim-elts (a) (declare (type (simple-array single-float (1000 1000)) a)) (let ((sum 0.0)) (declare (type single-float sum)) (dotimes (r 1000) (dotimes (c 1000) (incf sum (aref a r c)))) sum))
Speed - Type Declarations • > (compile-file “..\\test\\declare.lsp”) • > (load “..\\test\\declare.fas”) • > (time (sum-elts a)) Real time: 0.823 sec. Run time: 0.8112052 sec. Space: 11999988 Bytes GC: 1, GC time: 0.1092007 sec. 1000000.0 • > (compile-file “..\\test\\nodeclare.lsp”) • > (load “..\\test\\nodeclare.fas”) • > (time (sum-elts-nodeclare a))Real time: 1.026 sec. Run time: 0.9984064 sec. Space: 11999988 Bytes GC: 2, GC time: 0.2340015 sec. 1000000.0
Speed - Garbage Avoidance • Dynamic allocation is slow • Programs that cons a lot tend to run slowly in Lisp implementations with bad garbage collectors • Until recently, most Lisp implementations have had bad garbage collectors • Efficient programs should cons as little as possible
Speed - Garbage Avoidance • One of the easiest way is to use destructive functions • When you know it’s safe to modify a list, you can use delete instead of remove, nreverse instead of reverse, and so on
Speed - Garbage Avoidance • Use a pre-allocated vector instead of building it using conses • > (setf *print-array* t)T> (setfvec (make-array 10 :fill-pointer 2 :initial-element nil))#(NIL NIL)> (length vec)2> (vector-push ‘a vec)2> vec#(NIL NIL A)> (vector-pop vec)A> vec#(NIL NIL)
Speed – Fast Operators • svref is more efficient than aref • eq is more efficient than eql • (reduce #’+ ‘(1 2 3)) is more efficient than (apply #’+ ‘(1 2 3))
Debug • trace • (defun count-atoms (expression) (if (atom expression) 1 (+ (count-atoms (first expression)) (count-atoms (rest expresstion))))) • > (count-atoms ‘((this is) (a test)))7 ;we expect 4
Debug • > (trace count-atoms);; Tracing function COUNT-ATOMS. (COUNT-ATOMS) • (count-atoms '((this is) (a test)))
Debug • 4. Trace: (COUNT-ATOMS '(A TEST)) • 5. Trace: (COUNT-ATOMS 'A) • 5. Trace: COUNT-ATOMS ==> 1 • 5. Trace: (COUNT-ATOMS '(TEST)) • 6. Trace: (COUNT-ATOMS 'TEST) • 6. Trace: COUNT-ATOMS ==> 1 • 6. Trace: (COUNT-ATOMS 'NIL) • 6. Trace: COUNT-ATOMS ==> 1 • 5. Trace: COUNT-ATOMS ==> 2 • 4. Trace: COUNT-ATOMS ==> 3 • 4. Trace: (COUNT-ATOMS 'NIL) • 4. Trace: COUNT-ATOMS ==> 1 • 3. Trace: COUNT-ATOMS ==> 4 • 2. Trace: COUNT-ATOMS ==> 7 • 7 2. Trace: (COUNT-ATOMS '((THIS IS) (A TEST))) 3. Trace: (COUNT-ATOMS '(THIS IS)) 4. Trace: (COUNT-ATOMS 'THIS) 4. Trace: COUNT-ATOMS ==> 1 4. Trace: (COUNT-ATOMS '(IS)) 5. Trace: (COUNT-ATOMS 'IS) 5. Trace: COUNT-ATOMS ==> 1 5. Trace: (COUNT-ATOMS 'NIL) 5. Trace: COUNT-ATOMS ==> 1 4. Trace: COUNT-ATOMS ==> 2 3. Trace: COUNT-ATOMS ==> 3 3. Trace: (COUNT-ATOMS '((A TEST)))
Debug • (defun count-atoms (expression) (cond ((atom expression) 1) ((null expression) 0) (t (+ (count-atoms (first expression)) (count-atoms (rest expression)))))) • > (count-atoms ‘((this is) (a test)))7
Debug • (defun count-atoms (expression) (cond ((null expression) 0) ((atom expression) 1) (t (+ (count-atoms (first expression)) (count-atoms (rest expression)))))) • > (count-atoms ‘((this is) (a test)))4 • > (untrace count-atoms)
Debug • > (dribble “record.txt”) ;記錄在toplevel的過程……> (dribble) ;stop dribbling • > (ed “record.txt”)
Final Projects • Write a Lisp program with at least 15 functions • Due June 30 • Report • Source code • Demo