Highly Optimizing Ahead-of-Time Compilers for Dynamic Languages
Soutenance de thèse de doctorat
Highly Optimizing Ahead-of-Time Compilers for Dynamic Languages
Olivier Melançon
Jury
Eugène Syriani
PrésidentDIRO
Sébastien Roy
MembreDIRO
Marc Feeley
Directeur de rechercheDIRO
Manuel Serrano
Co-directeur de rechercheINRIA UCA
Matthew Flatt
Examinateur externeUniversity of Utah
Preface
First, some background
Example: My Favorite Polynomial
Suppose you want to compute values of your favorite polynomial: p(x) = x^2 + x.
Naturally, you write a program. You choose C because you heard it's fast!
intp(intx){returnx*x+x;}p(5)//30p(10)//110p(3.14)//12staticf_intstaticdoublef_dbl(doublex){returnx*x+x;}#definef(x)_Generic((x),int:f_int,double:f_dbl)(x)~.9996p(46341)//-2147432674int p(int x) {
return x * x + x;
}- Statically typed for
int - Workaround gymnastics for
float - It does not cover overflows!
Example: My Favorite Polynomial (Attempt 2)
You turn to OCaml!
letfx=x*x+xlet_=(f5)(*30*)let_=(f3.14)(*typeerror*)(typea)(moduleN:NUMwithtypet=a)(:a)N.add(N.mul)fmoduleInt)fmoduleFloat)~12.9996let f x = x * x + x- Compile-time type error for
floatinput - There is a fix, but...
- Still a silent wrap-around for
intoverflow
Example: My Favorite Polynomial (Attempt 3)
You decide to try dynamic languages.
(define (p x) (+ (* x x) x)) (p 5) ; 30(p 3.14) ; ~12.9996(p 46341) ; 2147534622def p(x): return x * x + x p(5) # 30p(3.14) # ~12.9996p(46341) # 2147534622It just works! But what is the catch?
Performance of Dynamic Languages
What makes dynamic languages slower?
(define(px)(+(*xx)x))let(y(cond((fixnum?x)(or(fx*?xx)(##))((flonum?x)(fl*x(else(##*xx)))))(cond((and(fixnum?y)(fixnum?x))(or(fx+?yx)(##+yx)))((and(flonum?y)(flonum?x))(fl+yx))(else(##+yx)))))(define (p x)
(+ (* x x) x))- static languages (C, OCaml) test types at compile time
- dynamic languages (Scheme, JavaScript, Python) check them at run time
- type checks
- overflow checks
- More flexible, but degrades performance
Object of this Thesis
How can an ahead-of-time compiler eliminate run-time checks, so that dynamic languages run as fast as static ones without losing their flexibility?
Table of Contents
-
Background on Static Analysis
-
Static Basic Block Versioning (SBBV)
-
Lambda Versioning (ΛV)
-
Technique Improvements
-
Experimental Evaluation
-
Future Work
Static Analysis
What is it and why does it fall short?
Intuition on Static Analysis
Even to the human eye, some tests beg to be removed.
(define(findvvpred)(letloop((i0))(cond((>=i(vector-lengthv))#f)((predvi)i)(else(loop(+i1))))))(vector-refv(findvvpred))if(vector?v)(##(fail))cond((fixnum?i)(or(fx+?i1)(##+i1)))((flonum?i)(fl+i1.0))(else(##))let((arg)(if(and(vector?v)(fixnum?arg)(fx>=arg0)(fx<arg(##vector-lengthv)))(##vector-refvarg)(fail)))(if(vector?v)fx>=)#f)(else(loop(fx+i1)))))(define (findv v pred)
(let loop ((i 0))
(cond
((>= i (vector-length v)) #f)
((pred v i) i)
(else (loop (+ i 1))))))
(vector-ref v (findv v pred))- operators have implicit run-time checks
- many of which are redundant
- eliminating them can make the program more efficient
- lift
vector?out of the loop - fixnum arithmetic
- eliminated bound checks
- can this test be removed?
- lift
Static Analysis Overview
- Based on abstract interpretation
- Technique for sound approximation of program semantics
- Executes on abstract values (sets of concrete values)
Algorithm sketch:
- Propagate abstract values through the control-flow graph
- Start from an entry point with a generic context
- A block's context is at least the union of its predecessors'
- Branches narrow contexts flowing to their successors
- Repeat until a fixed point is reached
Dynamic languages
- Wide range of run-time behaviors: approximation is coarse.
Union of Incoming Contexts
Narrowing of Outgoing Contexts
- gain information at conditional branches
- arithmetic comparisons
- type checks
Static Analysis of findv
(define(findvvpred)(letloop((i0))(cond((>=i(if(vector?v)(##vector-lengthv)(fail)))#f)((predvi)i)(else(loop(cond((fixnum?i)(or(fx+?i1)(##+i1)))((flonum?i)(fl+i1.0))(else(##+i1))))))))(let((arg(findvvpred)))(if(and(vector?v)(fixnum?arg)(fx>=arg0)(fx<arg(##vector-lengthv)))(##vector-refvarg)(fail)))fx>=(define (findv v pred)
(let loop ((i 0))
(cond
((>= i (if (vector? v) (##vector-length v)
(fail)))
#f)
((pred v i) i)
(else (loop (cond ((fixnum? i)
(or (fx+? i 1) (##+ i 1)))
((flonum? i) (fl+ i 1.0))
(else (##+ i 1))))))))
(let ((arg (findv v pred)))
(if (and (vector? v) (fixnum? arg)
(fx>= arg 0) (fx< arg (##vector-length v)))
(##vector-ref v arg) (fail)))Static Basic Block Versioning
Unifying abstract interpretation and code duplication
Overview of Static Basic Block Versioning
Static analysis and dynamic languages
- Tug of war between union and narrowing operations
- Inputs exist that produce wide ranges of behaviors
- Union wins over narrowing
Core idea
- Delay the union operation
- Duplicate basic blocks for different contexts
- When a version limit is exceeded, perform a merge
SBBV: A first example
(define(polynomialx)(+(*xx)1))let(y(cond((fixnum?x)(or(fx*?xx)(##))((flonum?x)(fl*xx))(else(##*xx)))))(cond((fixnum?y)(or(fx+?y1)(##+y)((flonum?y)(fl+y1.0))(else(##+y1)))))(define (polynomial x)
(+ (* x x) 1)) SBBV: Behavior with loops
(define(vprintv)(letloop((i0))(when(<i(vector-lengthv))(display(vector-refvi))(loop(+i1)))))if(vector?v)(##vector-lengthv)(fail)))(display(if(and(vector?v)(fixnum?i)(fx>=i0)(fx<i(##)##(fail))cond((fixnum?i)(or(fx+?i1)(##+i1)))((flonum?i)(fl+i1.0))(else(##))(define (vprint v)
(let loop ((i 0))
(when (< i (vector-length v))
(display (vector-ref v i))
(loop (+ i 1)))))SBBV: Back to findv
(define(findvvpred)(letloop((i0))(cond((>=i(if(vector?v)(##vector-lengthv)(fail)))#f)((predvi)i)(else(loop(cond((fixnum?i)(or(fx+?i1)(##+i1)))((flonum?i)(fl+i1.0))(else(##+i1))))))))(let((arg(findvvpred)))(if(and(vector?v)(fixnum?arg)(fx>=arg0)(fx<arg(##vector-lengthv)))(##vector-refvarg)(fail)))(define (findv v pred)
(let loop ((i 0))
(cond
((>= i (if (vector? v) (##vector-length v)
(fail)))
#f)
((pred v i) i)
(else (loop (cond ((fixnum? i)
(or (fx+? i 1) (##+ i 1)))
((flonum? i) (fl+ i 1.0))
(else (##+ i 1))))))))
(let ((arg (findv v pred)))
(if (and (vector? v) (fixnum? arg)
(fx>= arg 0) (fx< arg (##vector-length v)))
(##vector-ref v arg) (fail)))SBBV Experimental Results
SBBV: Experimental Setup
Implemented in two production-ready Scheme compilers: Bigloo and Gambit
19 benchmarks
12 macro, up to 11,740 lines
7 micro: fib, tak, ack...
8 configurations
No SBBV: own optimizations
Version limit 1 to 5, 10, 20
4 measures
Dynamic checks, program size
Execution and compile time
Bigloo and Gambit Are Already Fast
Execution time in seconds, SBBV with a limit of 2 versions (highlighted: fastest)
| Program | Node.js | Bigloo | Bigloo + SBBV | Gambit | Gambit + SBBV | Chez | Racket |
|---|---|---|---|---|---|---|---|
almabench | 6.31 | 15.68 | 14.46 | 14.64 | 13.86 | 17.69 | 19.30 |
boyer | 40.31 | 7.40 | 7.22 | 8.21 | 6.49 | 8.09 | 12.38 |
earley | 56.75 | 14.90 | 13.83 | 10.87 | 10.34 | 9.53 | 24.73 |
leval | 18.42 | 7.53 | 6.47 | 12.03 | 9.74 | 7.16 | 16.96 |
maze | 10.07 | 7.47 | 6.70 | 6.75 | 5.73 | 12.01 | 12.12 |
bague | 305.04 | 12.90 | 11.19 | 15.54 | 12.93 | 21.01 | 19.33 |
SBBV Results: Dynamic Checks
SBBV Results: Program Size
SBBV Results: Execution Time
SBBV Results: Compile Time
Lambda Versioning
Whole-program ahead-of-time basic block versioning
Overview of Lambda Versioning
The missing puzzle piece of SBBV
- Intraprocedural technique: limited propagation across abstraction barriers (functions)
Why?
- SBBV propagates information forward only.
- Propagating from a function's exit site to the call site requires backward propagation.
- Call sites have a single return point.
- This acts as a join point for all exit sites, losing context precision.
Solution
- Backward propagation from functions' exit sites to call sites.
- Duplicate entry points and return points to preserve context precision.
Framing the problem with an example
(define(polynomialx)(+(squarex)x))(define(squarex)(*xx))let((y))(cond((and(fixnum?y)(fixnum?(or(fx+?yx)(##+yx)))((and(flonum?y)(flonum?x))(fl+yx))(else(##+yx)))))cond((fixnum?x)(or(fx*?xx)(##)((flonum?x)(fl*xx))(else(##*xx))))(define (polynomial x)
(+ (square x) x))
(define (square x)
(* x x))- This conditional type information is lost at the return point
- We want the analysis to define specialized call interfaces:
- If the argument is a fixnum, provide one return point for fixnum and one for bignum
- If the argument is a flonum, the result is always a flonum
- If the argument is unknown, it could be a fixnum, flonum or another number, provide a return point for each
Core idea of Lambda Versioning
Core idea
- Transform the source program into one with multi-entry/multi-return functions
- Context at call sites propagates information to generate specialized entry points
- Entry points track the exit sites they can reach
- When a specialized exit site is discovered/removed, its call sites are notified and updated
Algorithm sketch
- Associate entry points and exit sites (multi-source reachability algorithm)
- New exit site \implies generate a specialized return point for its call sites
- Merge removes an exit site \implies remove the corresponding return points
- Return points duplication is governed version limit
Example: polynomial and square
(define (polynomial x) (let ((y (square x))) (cond ((and (fixnum? y) (fixnum? x)) (or (fx+? y x) (##+ y x))) ((and (flonum? y) (flonum? x)) (fl+ y x)) (else (##+ y x))))) (define (square x) (cond ((fixnum? x) (or (fx*? x x) (##* x x))) ((flonum? x) (fl* x x)) (else (##* x x)))) (polynomial (read))Recursive example: Fibonacci
(define(fibn)(if(<=n1)n(+(fib(-n1))(fib(-n2)))))(fib(read))if(fixnum?n)(fx<=n1)(##)let((acond((fixnum?n)(or(fx-?n1)(##)((flonum?n)(fl-n1.0))(else(##-n1)))))(bcond((fixnum?n)(or(fx-?n2)(##-n2)))((flonum?n)(fl-n2.0))(else(##)(cond((and(fixnum?a)(fixnum?b))(or(fx+?ab)(##+ab)))((and(flonum?a)(flonum?b))(fl+ab))(else(##+ab))))))(define (fib n)
(if (<= n 1)
n
(+ (fib (- n 1)) (fib (- n 2)))))
(fib (read))Example: findv
(define(findvvpred)(letloop((i0))(cond((>=i(vector-lengthv))#f)((predvi)i)(else(loop(+i1))))))(vector-refv(findvvpred))if(vector?v)(##(fail))cond((fixnum?i)(or(fx+?i1)(##+i1)))((flonum?i)(fl+i1.0))(else(##))let((arg)(if(and(vector?v)(fixnum?arg)(fx>=arg0)(fx<arg(##vector-lengthv)))(##vector-refvarg)(fail)))(define (findv v pred)
(let loop ((i 0))
(cond
((>= i (vector-length v)) #f)
((pred v i) i)
(else (loop (+ i 1))))))
(vector-ref v (findv v pred))ΛV Experimental Results
ΛV: Experimental Setup
Implemented source to source: Scheme in, specialized Scheme out, run on Gambit
27 benchmarks
12 macro, over 400 lines
15 micro: fib, tak, sum...
60 configurations
3 heuristics, 2 operator forms
Version limit 1 to 10
3 measures
Type checks executed
Compile time, code duplication
Why No Execution Time?
- Multi-return functions through CPS: one continuation per return point
- No native implementation of hyperfunctions: a Scheme macro emulates them, slower than the source program
(define square (hyperfunction entries: (any: A1 K: (fx→fx fl→fl fx|fl|bg→fx|fl|bg)) (fx: A2 K: (fx→fx #f fx→bg)) (fl: A3 K: (#f fl→fl)) blocks: A1: (lambda (K x) (call *.any×arg[0] fx×arg[0]→fx: K[0] fl×arg[0]→fl: K[1] fx|fl|bg×arg[0]→fx|fl|bg: K[2] x x)) A2: ... A3: ...))ΛV Results: Type Checks
ΛV Results: Compile Time
ΛV Results: Code Duplication
Various Improvements
Three refinements.
Vector bound checks
intervals with symbolic bounds
- A bound is
⟦v⟧-i: the length of vectorvminusi (fx< x len)narrows the indexxto[0, ⟦v⟧-1]- The bound check of
vector-refis removed
Operators as functions
specializations are reused
- SBBV requires inlined operators
- ΛV supports
+and*as functions - Each specialization is computed once
Merge heuristics
similarity vs arithmetic
similaritymerges the closest contextsarithmeticpreserves pure arithmeticarithmeticis better on all benchmarks
Vector Bound Check Elimination
Vector Support: New Rules
Intervals with symbolic bounds
- A bound is written
⟦v⟧-i, withi ≥ 0: the length of vectorvminusi
New rules
(##vector-length v)returns the singleton⟦v⟧⟦v⟧: exactly the length ofv, some fixnum in[0, maxfix]
(fx< x len)withlen = ⟦v⟧narrows the upper bound ofxto⟦v⟧-1when true(fx< x len)withxin[0, ⟦v⟧-1]andlen == ⟦v⟧is always true
Example: vector-ref
(define(vector-safe-refvi)(if(and(fixnum?i)(>=i0)(<i(vector-lengthv)))(vector-refvi)#f))(vector-safe-ref#(123)(read))if(vector?v)(##vector-lengthv)(fail))))(if(and(vector?v)(fixnum?i)(fx>=i0)(fx<i(####(fail))(define (vector-safe-ref v i)
(if (and (fixnum? i) (>= i 0) (< i (vector-length v)))
(vector-ref v i)
#f))
(vector-safe-ref #(1 2 3) (read))Example: findv with vector bounds
(define(findvvpred)(letloop((i0))(cond((>=i(vector-lengthv))#f)((predvi)i)(else(loop(+i1))))))(vector-refv(findvvpred))if(vector?v)(##(fail))cond((fixnum?i)(or(fx+?i1)(##+i1)))((flonum?i)(fl+i1.0))(else(##))let((arg)(if(and(vector?v)(fixnum?arg)(fx>=arg0)(fx<arg(##vector-lengthv)))(##vector-refvarg)(fail)))(define (findv v pred)
(let loop ((i 0))
(cond
((>= i (vector-length v)) #f)
((pred v i) i)
(else (loop (+ i 1))))))
(vector-ref v (findv v pred))Operators as Functions
Operators as functions: the polynomial
(define(px)(+(*xx)x))(p(read))(define(*ab)(cond((and(fixnum?a)(fixnum?b))(or(fx*?ab)(##*ab)))((and(flonum?a)(flonum?b))(fl*ab))(else(##*ab))))(define(+ab)(cond((and(fixnum?a)(fixnum?b))(or(fx+?ab)(##+ab)))((and(flonum?a)(flonum?b))(fl+ab))(else(##+ab))))(define (p x)
(+ (* x x) x))
(p (read))- SBBV requires inlined operators
- ΛV can support operators as functions
Operators as functions: compile time
Operators as functions re-use specializations. Inlined operators had to re-specialize at every site.
Merge Heuristics
Arithmetic vs Similarity
similarity
merge closest
arithmetic
prefer pure arithmetic
Empirical Comparison
arithmeticfares better on all benchmarks- generates optimal program for recursive functions such as
fib - heuristic has a larger impact on ΛV than SBBV
Future Work
Native Multi-Return Functions
- Efficient native implementation
- Here is a work-in-progress
.hyper + nregs=3 nk=2 nfree=0 .entry 1 params=2 label=A ; any x any .entry 2 params=2 label=B ; fx x fxA: brntag r0, fixnum, GEN brntag r1, fixnum, GENB: fxadd? r2, r0, r1, GEN ret 0, r2 ; fixnum resultGEN: add r2, r0, r1 ret 1, r2 ; generic result .hyper fib nregs=4 nk=2 nfree=0 ...C2: ldclo r3 ; fib(n-1) calld r3, @fib, 2, K(D2, D1), (rv)Heterogeneous Data Structures
- Most remaining checks in benchmarks are on values read from heterogeneous structures (vector, pair, box, etc.)
- Include mutability/escapability information in contexts to track the type of values within structures
(let ((v (make-vector 1 0))) (##vector-set! v 0 42) (##vector-ref v 0))Built-in Constant Folding
(define (incr x) (+ x 1))(incr 0)Conclusion
SBBV eliminates run-time checks
54–62%
fewer dynamic checks in Bigloo and Gambit with only 2 versions per block, about 10% faster.
ΛV removes nearly all type checks
1
type check per input, then none; ΛV specializes fib, ack and tak optimally.
whole-program specialization
AOT
code duplication to anticipate multiple run-time behaviors.
Remerciements
Merci à Marc Feeley et Manuel Serrano pour vos conseils et soutiens indéfectibles.
Merci au jury de thèse. Un merci particulier à Matthew Flatt.
Merci à toute la communauté du DIRO.
Merci à tous mes collègues et amis du LTP. 🤽
Merci à mes amis.
Merci à ma famille, pour votre soutien, votre amour et votre fierté.