Seyed Masoud Hosseini · Overview · Study log · Ideas · Transcript · RSS feed
Performance Engineering of Software Systems · Lecture 23 of 23 · 1:25:44
Lecture 23: High Performance in Dynamic Languages
Study guide
What this lecture covers
Guest lecturer Steven Johnson (co-creator of the FFTW library) explains how the Julia language achieves near-C performance while keeping the interactive, dynamically typed feel of Python or MATLAB. The lecture closes the course's tour of performance engineering by moving from low-level C/Cilk optimization to a higher-level question: what must a language provide so a compiler can generate fast, type-specialized code without sacrificing generality?
Johnson builds the explanation from live benchmarks in a Jupyter notebook, comparing summing an array in C, Python, NumPy, and Julia, and then digs into why Python's list representation forces slow, type-checking loops while Julia's type system and just-in-time compilation avoid that cost. After watching, you should be able to explain why dynamically typed languages are typically slow, what "type stability" and "type inference" mean for compiled performance, and how Julia's parametric types and multiple dispatch let user-defined types run as fast as built-in ones.
Key ideas
- The two-language problem: traditionally, programmers prototype in a high-level language like Python and rewrite performance-critical code in C or Fortran, losing generality and adding complexity at the boundary.
- Boxed values: a Python list stores pointers to heterogeneous "boxed" objects, each carrying a type tag; this forces the interpreter to check types and dispatch operations at runtime for every element, which is the main source of slowness.
- Homogeneous typed arrays: NumPy arrays store one type tag for the whole array and pack raw values contiguously, letting them dispatch to fast, C-like loops (and sometimes use SIMD instructions) for a large speedup over pure Python.
- Type inference and specialization: Julia compiles a specialized version of a function the first time it is called with a given combination of argument types, inferring the types of all intermediate values so the resulting machine code needs no runtime type checks.
- Type stability: a function is type-stable when its return type depends only on the types of its arguments, not their values; unstable functions (like a naive integer square root) force the compiler to fall back to boxed, dynamically checked code.
- Parametric types: Julia lets you define a family of concrete types (such as a 2D point parameterized by its coordinate type) instead of one fixed type, so user-defined types can be stored unboxed in arrays and run as fast as built-ins.
- Multiple dispatch: unlike single-dispatch object-oriented method lookup (which uses only the first argument's type), Julia chooses which method to call based on the types of all arguments, which handles mixed-type operations like adding a real number to a complex number naturally.
Walkthrough
The two-language problem and Julia's goals (1:23)
Johnson introduces the tension in high-level scientific computing languages: they're productive for interactive exploration but traditionally too slow for performance-critical loops, forcing a drop to C or Fortran. He introduces Julia, launched in 2013 with a stable 1.0 release, as a language designed to stay within a small constant factor of C while remaining fully dynamically typed and generic.
A Vandermonde matrix example (5:06)
Comparing NumPy's C implementation (hundreds of lines dispatching to type-specific kernels) against a roughly 10-line generic Julia implementation, Johnson shows the Julia version matches NumPy's performance for large matrices and can even beat hand-optimized C/Fortran libraries for special functions, because Julia's metaprogramming lets it generate optimized inline polynomial evaluations.
Live benchmark: summing an array across languages (11:14)
Using a Jupyter notebook, Johnson benchmarks summing 10 million random numbers: a plain C loop (about 10 ms), Python's built-in sum (about 40 ms, itself implemented in C but slowed by boxed-object overhead), a hand-written Python loop (about 230 ms), NumPy's sum (about 4 ms, using SIMD), and Julia's built-in and hand-written sum (both close to the C or NumPy times, and equally fast when SIMD hints are added).
Why Python lists are slow: boxing (17:16)
Johnson explains that because a Python list can hold values of any type, each element must be a pointer to a "boxed" object carrying its own type tag, and every loop iteration must dereference that pointer, read the type tag, look up the correct method (such as which plus to call), and allocate a new box for the result. This chasing of pointers and runtime dispatch is what a C loop over a raw double* avoids entirely.
Type inference and compiled specialization in Julia (34:39)
Johnson demonstrates how Julia compiles a distinct, specialized version of a function for each combination of argument types it's called with, inferring intermediate types (shown via code_llvm and code_native) so the compiled code has no runtime type checks. He shows this inference cascading through nested function calls and even recursive functions like Fibonacci and factorial, and explains that this "type inference" step is what turns dynamic, generic-looking Julia code into machine code as tight as hand-written C.
Multiple dispatch versus single dispatch (45:51)
Contrasting Julia's method(a, b) syntax with the object.method() style of Python or C++, Johnson shows that Julia dispatches on the types of all arguments, not just the first. This makes operations like adding a complex number to a real number natural to define without awkward operator-overloading workarounds, and is described as a generalization of object-oriented method dispatch called multiple dispatch.
Type stability and its pitfalls (1:26)
Johnson defines type stability: a function's return type must depend only on argument types, not values. He uses integer square root as a counterexample — returning an integer only when the input is a perfect square would make the function type-unstable — and contrasts Julia's design choices (fixed-width 64-bit integers that don't silently promote, unlike Python's arbitrary-precision integers) with the performance costs those choices avoid.
Defining fast user-defined types (57:19)
Johnson builds up a custom 2D Point type step by step: an initial generic, mutable version stores boxed pointers and is over 50 times slower than summing plain numbers; making the struct immutable with fixed field types allows the compiler to store points unboxed and inline, recovering C-like speed; finally, parameterizing the type over its coordinate type (Point{T}) lets one definition generate an unlimited family of concrete, fast types without hand-writing each variant, illustrating why Julia avoids treating built-in types as privileged.
Before you watch
- Basic familiarity with Python's dynamic typing and with C's static typing will help you follow the contrasts Johnson draws throughout.
- No specific Julia background is assumed, but recognizing loops, arrays, and simple recursive functions will make the live-coded examples easier to follow.
- The lecture builds on the course's general performance-engineering vocabulary (compilation, SIMD, caching) introduced in earlier lectures.
Check your understanding
- Why does a Python list have to store every element as a "boxed" pointer with an attached type tag, and how does this differ from a NumPy array?
- What does it mean for a function to be "type-stable," and why is a naive integer square root function that returns an integer only for perfect squares type-unstable?
- How does Julia's type inference let it compile a specialized, unboxed version of a generic function for a specific set of argument types?
- Explain the difference between single dispatch (as in typical object-oriented languages) and Julia's multiple dispatch, using the example of adding a complex number to a real number.
- Why did making the
Pointstruct immutable, rather than mutable, allow Julia to store an array of points as unboxed, contiguous memory?
Chapters
- 0:00 Intro
- 0:22 Welcome
- 1:23 Highlevel dynamic languages
- 2:54 Vectorize your code
- 3:42 Julia
- 5:19 Julia Example
- 7:11 Julia Implementation
- 11:13 Live Calculation
- 23:03 Numpy
- 30:13 Julia Some
- 46:26 Multiple Dispatch
- 51:26 Type Inference
- 57:26 Defining our own types
From the YouTube description
MIT 6.172 Performance Engineering of Software Systems, Fall 2018
Instructor: Steven Johnson
View the complete course: https://ocw.mit.edu/6-172F18
YouTube Playlist: https://www.youtube.com/playlist?list=PLUl4u3cNGP63VIBQVWguXxZZi0566y7Wf
Professor Steven Johnson talks about a new dynamic language called Julia as an alternative to the two-language approach for interactive math. Julia is then compared to Python and C.
License: Creative Commons BY-NC-SA
More information at https://ocw.mit.edu/terms
More courses at https://ocw.mit.edu
