Seyed Masoud Hosseini · Overview · Study log · Weekly summaries · Ideas · Search · 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?
Vocabulary
- dynamically typed (adjective)
- Describing a language where a variable's type is checked while the program runs, not before.
Python is a dynamically typed language, unlike C. - two-language problem (noun)
- The pattern of prototyping in an easy language and rewriting slow parts in a fast one.
The two-language problem forces scientists to rewrite Python code in C. - prototype (verb)
- To build a quick, early version of something to test an idea.
Scientists often prototype their algorithm in a high-level language first. - boxed value (noun)
- A value stored as a pointer to an object carrying extra information like its type.
Every item in a Python list is a boxed value with its own type tag. - type tag (noun)
- A marker attached to a value that records what type of data it is.
The interpreter reads the type tag before deciding how to process a value. - dispatch (verb)
- To choose and call the correct version of an operation based on the data's type.
The interpreter must dispatch the right addition method for each pair of values. - homogeneous (adjective)
- Made up of items that are all the same type.
NumPy arrays are homogeneous, storing only one type throughout. - SIMD (noun)
- A hardware feature that applies one instruction to multiple data values at the same time.
NumPy's speed partly comes from using SIMD instructions. - type inference (noun)
- The process of a compiler figuring out the type of a value without being told directly.
Julia's type inference lets the compiler generate fast, checked-free code. - specialization (noun)
- Creating a version of code tailored to a specific situation for better performance.
Julia compiles a specialization of a function for each set of argument types. - type stability (noun)
- A property where a function's output type depends only on its input types, not their values.
Type stability lets the compiler avoid runtime type checks. - type-unstable (adjective)
- Describing a function whose return type can differ depending on the actual values passed in.
A type-unstable square root function forces the compiler to fall back to slow code. - parametric type (noun)
- A type definition that can work with different underlying data types using a placeholder.
A parametric type lets one Point definition work for integers or floats. - multiple dispatch (noun)
- Choosing which function to run based on the types of all its arguments, not just one.
Multiple dispatch makes adding a real number to a complex number feel natural. - single dispatch (noun)
- Choosing which method to run based only on the type of the first argument.
Traditional object-oriented languages usually use single dispatch. - metaprogramming (noun)
- Writing code that generates or manipulates other code.
Julia's metaprogramming can generate optimized inline calculations. - immutable (adjective)
- Describing something that cannot be changed once it is created.
Making the Point struct immutable lets the compiler store it unboxed. - mutable (adjective)
- Describing something that can be changed after it is created.
A mutable struct must be stored as a pointer, which is slower. - unboxed (adjective)
- Stored directly as raw data rather than wrapped inside an object with extra information.
Unboxed values can be packed tightly in memory for speed. - generic (adjective)
- Written to work with many different types rather than one fixed type.
Julia's generic code can still run as fast as specialized C code. - arbitrary-precision (adjective)
- Describing numbers that can grow as large as needed without a fixed size limit.
Python uses arbitrary-precision integers, unlike Julia's fixed-width integers.
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
