Many standard Common Lisp functions, particularly those which do simple calculations or access data from arrays or structures, can be compiled inline by the compiler. Inline compilation results in faster, often significantly faster run times as the function call and return overheads are saved.
But there are tricks to getting the compiler to inline, and there are tools which assist is expalining what the compiler is doing. In this note, we take a simple function with a call to the math function round, and try to get the call to round to inline. Here is the function:
(defun foo (x) (declare (optimize (speed 3) (safety 0) (debug 0)) (double-float x)) (round (* x x)))
round looks like a good candidate for inlining: the type of its argument is known and speed, safety, and debug have values calling for maximum speed. But when it is compiled and the compiled function is disassembled, we see that round was not inlined:
cl-user(10): (compile 'foo) foo nil nil cl-user(11): (disassemble 'foo) ;; disassembly of #<Function foo> ;; formals: x ;; constant vector: 0: round ;; code start: #x10003508c80: 0: 48 83 ec 68 sub rsp,$104 4: 4c 89 74 24 08 movq [rsp+8],r14 9: f2 44 0f 10 6f movsd xmm13,[rdi-10] f6 15: f2 45 0f 59 ed mulsd xmm13,xmm13 20: f2 45 0f 10 fd movsd xmm15,xmm13 25: 31 c0 xorl eax,eax 27: 41 ff 97 d7 03 call *[r15+983] ; sys::new-double-float 00 00 34: 4c 89 7c 24 18 movq [rsp+24],r15 39: 48 8d 64 24 68 leaq rsp,[rsp+104] 44: 49 8b 6e 36 movq rbp,[r14+54] ; round 48: b0 08 movb al,$8 50: ff e3 jmp *rbx cl-user(11):
The line labeled 44 is the jump to the round function.
So what went wrong? Spoiler alert: there are two problems: first round returns two values and there is no indication whether both or just the first are needed. Second, the type of the first return value is not known. We know it is an integer but not whether it is a fixnum or a bignum. Without that information, the compiler cannot inline the call. (The user may not know whether the result will be a fixnum or a bignum, in which case inlining is not possible, but if the user knows the value will be a fixnum, then that information should be supplied.)
So how do we find out what to do? First, Allegro CL supports an :explain declaration (see the section Help with declarations: the :explain declaration in compiling.htm). Adding that declaraion with types :inlining and :types will cause the compiler to print out reports on what it is doing with respect the inlining and type propagation:
(compile (defun foo (x) (declare (optimize (speed 3) (safety 0) (debug 0)) (double-float x) (:explain :types :inlining)) (round (* x x)))) ;;; Note: The compilation of (ROUND (* X X)) might be done more ;;; efficiently if only one value is used from its call. ; While compiling FOO: ;Tgen1:Examined a (possibly unboxed) call to *_2OP with arguments: ;Targ2: symeval X type (DOUBLE-FLOAT * *) ;Tinf1: VARIABLE-information: LEXICAL: ((TYPE (DOUBLE-FLOAT * *))) ;Targ2: symeval X type (DOUBLE-FLOAT * *) ;Tinf1: VARIABLE-information: LEXICAL: ((TYPE (DOUBLE-FLOAT * *))) ;Tres1: which returns a value of type (DOUBLE-FLOAT * *) ;Igen1: Node: checking (DOUBLE-FLOAT * *) for implied unboxable type ;Igen1: (floats, or machine-integer): ;Igen2: Node: type is unboxable as DOUBLE-FLOAT ;Igen3:Attempt to inline an unboxed call to *_2OP while trusting declarations: ;Igen5: Node: checking for notinline declaration for EXCL::*_2OP: none ;Igen1: Arg0: checking (DOUBLE-FLOAT * *) for implied unboxable type ;Igen1: (floats, or machine-integer): ;Igen2: Arg0: type is unboxable as DOUBLE-FLOAT ;Igen1: Arg1: checking (DOUBLE-FLOAT * *) for implied unboxable type ;Igen1: (floats, or machine-integer): ;Igen2: Arg1: type is unboxable as DOUBLE-FLOAT ;Igen4:Inline attempt succeeded. ;Igen8: Unboxed result must be boxed - consider expanding scope of unboxed forms. ;Tgen1:Examined a call to ROUND with arguments: ;Targ1: call to *_2OP type (DOUBLE-FLOAT * *) ;Tres1: which returns a value of type (INTEGER * *) ;Igen1: Node: checking (INTEGER * *) for implied unboxable type ;Igen1: (floats, or machine-integer): ;Igen2: Node: type is not unboxable ;Igen3:Attempt to inline a boxed call to ROUND while trusting declarations: ;Igen5: Node: checking for notinline declaration for ROUND: none ;Igen9: No info available to inline ROUND as boxed ;Igen4:Inline attempt failed. FOO NIL NIL CL-USER(12):
The first note comments on the fact that round returns two values and the compiler could do better if it knew that only one value was required. So let us force a single value from round:
(compile (defun foo (x) (declare (optimize (speed 3) (safety 0) (debug 0)) (double-float x) (:explain :types :inlining)) (values (round (* x x))))) ; While compiling FOO: ;Tgen1:Examined a (possibly unboxed) call to *_2OP with arguments: ;Targ2: symeval X type (DOUBLE-FLOAT * *) ;Tinf1: VARIABLE-information: LEXICAL: ((TYPE (DOUBLE-FLOAT * *))) ;Targ2: symeval X type (DOUBLE-FLOAT * *) ;Tinf1: VARIABLE-information: LEXICAL: ((TYPE (DOUBLE-FLOAT * *))) ;Tres1: which returns a value of type (DOUBLE-FLOAT * *) ;Igen1: Node: checking (DOUBLE-FLOAT * *) for implied unboxable type ;Igen1: (floats, or machine-integer): ;Igen2: Node: type is unboxable as DOUBLE-FLOAT ;Igen3:Attempt to inline an unboxed call to *_2OP while trusting declarations: ;Igen5: Node: checking for notinline declaration for EXCL::*_2OP: none ;Igen1: Arg0: checking (DOUBLE-FLOAT * *) for implied unboxable type ;Igen1: (floats, or machine-integer): ;Igen2: Arg0: type is unboxable as DOUBLE-FLOAT ;Igen1: Arg1: checking (DOUBLE-FLOAT * *) for implied unboxable type ;Igen1: (floats, or machine-integer): ;Igen2: Arg1: type is unboxable as DOUBLE-FLOAT ;Igen4:Inline attempt succeeded. ;Igen8: Unboxed result must be boxed - consider expanding scope of unboxed forms. ;Tgen1:Examined a call to ROUND_1RET with arguments: ;Targ1: call to *_2OP type (DOUBLE-FLOAT * *) ;Tres1: which returns a value of type (INTEGER * *) ;Igen1: Node: checking (INTEGER * *) for implied unboxable type ;Igen1: (floats, or machine-integer): ;Igen2: Node: type is not unboxable ;Igen3:Attempt to inline a boxed call to ROUND_1RET while trusting declarations: ;Igen5: Node: checking for notinline declaration for EXCL::ROUND_1RET: ;Igen5: none ;Igen1: Arg0: checking (DOUBLE-FLOAT * *) for implied unboxable type ;Igen1: (floats, or machine-integer): ;Igen2: Arg0: type is unboxable as DOUBLE-FLOAT ;Ityp1: Node: looking for FIXNUM - failed ;Igen4:Inline attempt failed. ;Tgen1:Examined a call to VALUES with arguments: ;Targ1: call to ROUND_1RET type (INTEGER * *) ;Tres1: which returns a value of type T ;Igen1: Node: checking T for implied unboxable type (floats, or ;Igen1: machine-integer): ;Igen2: Node: type is not unboxable ;Igen3:Attempt to inline a boxed call to VALUES while trusting declarations: ;Igen5: Node: checking for notinline declaration for VALUES: none ;Igen4:Inline attempt succeeded. FOO NIL NIL CL-USER(13):
Progess but round_1ret is still not inlining
(;Igen3:Attempt to inline a boxed call to ROUND_1RET while
trusting declarations: [...] ;Ityp1: Node: looking for FIXNUM -
failed ;Igen4:Inline attempt failed.
) The compiler looked to see
if the result was a fixnum and could not be sure. Also,
because round_1ret is not inlining, its argument (the
double-float (* x x)
must be boxed. This is confiirmed by
looking at the diaassembly:
CL-USER(13): (disassemble 'foo) ;; disassembly of #<Function FOO> ;; formals: X ;; constant vector: 0: EXCL::ROUND_1RET ;; code start: #x10001671aa0: 0: 48 83 ec 68 sub rsp,$104 4: 4c 89 74 24 08 movq [rsp+8],r14 9: f2 44 0f 10 6f movsd xmm13,[rdi-10] f6 15: f2 45 0f 59 ed mulsd xmm13,xmm13 20: f2 45 0f 10 fd movsd xmm15,xmm13 25: 31 c0 xorl eax,eax 27: 41 ff 97 d7 03 call *[r15+983] ; SYS::NEW-DOUBLE-FLOAT 00 00 34: 49 8b 6e 36 movq rbp,[r14+54] ; EXCL::ROUND_1RET 38: b0 08 movb al,$8 40: ff d3 call *rbx 42: f8 clc 43: 4c 8b 74 24 78 movq r14,[rsp+120] 48: 4c 89 7c 24 18 movq [rsp+24],r15 53: 48 8d 64 24 68 leaq rsp,[rsp+104] 58: c3 ret 59: 90 nop CL-USER(14):
Now you have to decide whether the call to round will always return a fixnum. The is a program decision: it cannot be determined by examing this snippet of code. The question is, will round ever be called with a double-float value larger than 1 greater than most-positive-fixnum, meaning the absolute value of x is roughly bigger than the square root of that value. If you (the programmer) are sure that will not happen, then we can tell the compiler the result will be a fixnum:
(compile (defun foo (x) (declare (optimize (speed 3) (safety 0) (debug 0)) (double-float x) (:explain :types :inlining)) (values (the fixnum (round (* x x)))))) ; While compiling FOO: ;Tgen1:Examined a (possibly unboxed) call to *_2OP with arguments: ;Targ2: symeval X type (DOUBLE-FLOAT * *) ;Tinf1: VARIABLE-information: LEXICAL: ((TYPE (DOUBLE-FLOAT * *))) ;Targ2: symeval X type (DOUBLE-FLOAT * *) ;Tinf1: VARIABLE-information: LEXICAL: ((TYPE (DOUBLE-FLOAT * *))) ;Tres1: which returns a value of type (DOUBLE-FLOAT * *) ;Igen1: Node: checking (DOUBLE-FLOAT * *) for implied unboxable type ;Igen1: (floats, or machine-integer): ;Igen2: Node: type is unboxable as DOUBLE-FLOAT ;Igen3:Attempt to inline an unboxed call to *_2OP while trusting declarations: ;Igen5: Node: checking for notinline declaration for EXCL::*_2OP: none ;Igen1: Arg0: checking (DOUBLE-FLOAT * *) for implied unboxable type ;Igen1: (floats, or machine-integer): ;Igen2: Arg0: type is unboxable as DOUBLE-FLOAT ;Igen1: Arg1: checking (DOUBLE-FLOAT * *) for implied unboxable type ;Igen1: (floats, or machine-integer): ;Igen2: Arg1: type is unboxable as DOUBLE-FLOAT ;Igen4:Inline attempt succeeded. ;Tgen1:Examined a call to ROUND_1RET with arguments: ;Targ1: call to *_2OP type (DOUBLE-FLOAT * *) ;Tres1: which returns a value in fixnum range of type ;Tres1: (INTEGER -1152921504606846976 1152921504606846975) ;Igen1: Node: checking ;Igen1: (INTEGER -1152921504606846976 1152921504606846975) for implied ;Igen1: unboxable type (floats, or machine-integer): ;Igen2: Node: type is not unboxable ;Igen3:Attempt to inline a boxed call to ROUND_1RET while trusting declarations: ;Igen5: Node: checking for notinline declaration for EXCL::ROUND_1RET: ;Igen5: none ;Igen1: Arg0: checking (DOUBLE-FLOAT * *) for implied unboxable type ;Igen1: (floats, or machine-integer): ;Igen2: Arg0: type is unboxable as DOUBLE-FLOAT ;Ityp1: Node: looking for FIXNUM - got ;Ityp1: (INTEGER -1152921504606846976 1152921504606846975) ;Igen4:Inline attempt succeeded. ;Tgen1:Examined a call to VALUES with arguments: ;Targ1: call to ROUND_1RET type in fixnum range (INTEGER -1152921504606846976 1152921504606846975) ;Tres1: which returns a value of type T ;Igen1: Node: checking T for implied unboxable type (floats, or ;Igen1: machine-integer): ;Igen2: Node: type is not unboxable ;Igen3:Attempt to inline a boxed call to VALUES while trusting declarations: ;Igen5: Node: checking for notinline declaration for VALUES: none ;Igen4:Inline attempt succeeded. FOO NIL NIL CL-USER(15):
Now the inlining of round_1op has succeeded, and the Igen8 message has gone away. And the disassembled code:
CL-USER(15): (disassemble 'foo) ;; disassembly of #<Function FOO> ;; formals: X ;; code start: #x1000167a300: 0: 48 83 ec 78 sub rsp,$120 4: 4c 89 74 24 08 movq [rsp+8],r14 9: f2 44 0f 10 6f movsd xmm13,[rdi-10] f6 15: f2 45 0f 59 ed mulsd xmm13,xmm13 20: 0f ae 5c 24 70 stmxcsrl [rsp+112] 25: 0f ae 5c 24 74 stmxcsrl [rsp+116] 30: 81 64 24 70 ff andl [rsp+112],$40959 9f 00 00 38: 0f ae 54 24 70 ldmxcsrl [rsp+112] 43: f2 49 0f 2d c5 cvtsd2siq rax,xmm13 48: 0f ae 54 24 74 ldmxcsrl [rsp+116] 53: 48 c1 e0 03 sal rax,$3 57: 49 89 c5 movq r13,rax 60: 4c 89 ef movq rdi,r13 63: f8 clc 64: 4c 8b b4 24 88 movq r14,[rsp+136] 00 00 00 72: 4c 89 7c 24 18 movq [rsp+24],r15 77: 48 8d 64 24 78 leaq rsp,[rsp+120] 82: c3 ret 83: 90 nop CL-USER(16):
also does no consing. A slight rewrite gets rid of the call to values and a simpler set of explanations:
CL-USER(16): (compile (defun foo (x) (declare (optimize (speed 3) (safety 0) (debug 0)) (double-float x) (:explain :types :inlining)) (let ((res (the fixnum (round (* x x))))) res))) ; While compiling FOO: ;Tgen1:Examined a (possibly unboxed) call to *_2OP with arguments: ;Targ2: symeval X type (DOUBLE-FLOAT * *) ;Tinf1: VARIABLE-information: LEXICAL: ((TYPE (DOUBLE-FLOAT * *))) ;Targ2: symeval X type (DOUBLE-FLOAT * *) ;Tinf1: VARIABLE-information: LEXICAL: ((TYPE (DOUBLE-FLOAT * *))) ;Tres1: which returns a value of type (DOUBLE-FLOAT * *) ;Igen1: Node: checking (DOUBLE-FLOAT * *) for implied unboxable type ;Igen1: (floats, or machine-integer): ;Igen2: Node: type is unboxable as DOUBLE-FLOAT ;Igen3:Attempt to inline an unboxed call to *_2OP while trusting declarations: ;Igen5: Node: checking for notinline declaration for EXCL::*_2OP: none ;Igen1: Arg0: checking (DOUBLE-FLOAT * *) for implied unboxable type ;Igen1: (floats, or machine-integer): ;Igen2: Arg0: type is unboxable as DOUBLE-FLOAT ;Igen1: Arg1: checking (DOUBLE-FLOAT * *) for implied unboxable type ;Igen1: (floats, or machine-integer): ;Igen2: Arg1: type is unboxable as DOUBLE-FLOAT ;Igen4:Inline attempt succeeded. ;Tgen1:Examined a call to ROUND_1RET with arguments: ;Targ1: call to *_2OP type (DOUBLE-FLOAT * *) ;Tres1: which returns a value in fixnum range of type ;Tres1: (INTEGER -1152921504606846976 1152921504606846975) ;Igen1: Node: checking ;Igen1: (INTEGER -1152921504606846976 1152921504606846975) for implied ;Igen1: unboxable type (floats, or machine-integer): ;Igen2: Node: type is not unboxable ;Igen3:Attempt to inline a boxed call to ROUND_1RET while trusting declarations: ;Igen5: Node: checking for notinline declaration for EXCL::ROUND_1RET: ;Igen5: none ;Igen1: Arg0: checking (DOUBLE-FLOAT * *) for implied unboxable type ;Igen1: (floats, or machine-integer): ;Igen2: Arg0: type is unboxable as DOUBLE-FLOAT ;Ityp1: Node: looking for FIXNUM - got ;Ityp1: (INTEGER -1152921504606846976 1152921504606846975) ;Igen4:Inline attempt succeeded. FOO NIL NIL CL-USER(117): (disassemble *) ;; disassembly of #<Function FOO> ;; formals: X ;; code start: #x10001683ab0: 0: 48 83 ec 78 sub rsp,$120 4: 4c 89 74 24 08 movq [rsp+8],r14 9: f2 44 0f 10 6f movsd xmm13,[rdi-10] f6 15: f2 45 0f 59 ed mulsd xmm13,xmm13 20: 0f ae 5c 24 70 stmxcsrl [rsp+112] 25: 0f ae 5c 24 74 stmxcsrl [rsp+116] 30: 81 64 24 70 ff andl [rsp+112],$40959 9f 00 00 38: 0f ae 54 24 70 ldmxcsrl [rsp+112] 43: f2 49 0f 2d c5 cvtsd2siq rax,xmm13 48: 0f ae 54 24 74 ldmxcsrl [rsp+116] 53: 48 c1 e0 03 sal rax,$3 57: 49 89 c5 movq r13,rax 60: 4c 89 ef movq rdi,r13 63: f8 clc 64: 4c 8b b4 24 88 movq r14,[rsp+136] 00 00 00 72: 4c 89 7c 24 18 movq [rsp+24],r15 77: 48 8d 64 24 78 leaq rsp,[rsp+120] 82: c3 ret 83: 90 nop CL-USER(18):
But be warned: if a call to round does return a bignum, the result will be very wrong:
cl-user(19): (setq big (float (+ 5 most-positive-fixnum) 1.0d0)) 1.152921504606847d+18 cl-user(20): (foo big) 0 cl-user(21):
Copyright © 2023 Franz Inc., All Rights Reserved | Privacy Statement |