Source-linked AI summary
Julia: A Fresh Approach to Numerical Computing
Jeff Bezanson, Alan Edelman, Stefan Karpinski, Viral B. Shah
TL;DR
Numerical computing has struggled to combine high-level convenience with high performance without relying on implementation-specific libraries or rewriting code. The paper introduces Julia’s specialization-and-abstraction design, showing that it can provide machine performance without sacrificing human convenience.
Problem
High-performance numerical computing is difficult because performance-critical libraries depend on high-level-language implementation details, limiting seamless combinations of convenience and speed.
Method
Julia combines multiple dispatch, generic programming, and macros to specialize algorithms while expressing reusable mathematical abstractions.
Results
Julia provides machine performance without sacrificing human convenience, including parallel scientific computation without traditional parallel-programming fuss.
Takeaways & Limitations
Julia offers a unified approach to convenient, high-performance numerical and scientific computing across specialized algorithms and parallel workloads.
Takeaways & Limitations
Float64 numerical results may vary across machines because they depend on BLAS, LAPACK, and Julia versions.
Abstract
from arXiv · showhide
Bridging cultures that have often been distant, Julia combines expertise from the diverse fields of computer science and computational science to create a new approach to numerical computing. Julia is designed to be easy and fast. Julia questions notions generally held as "laws of nature" by practitioners of numerical computing: 1. High-level dynamic programs have to be slow. 2. One must prototype in one language and then rewrite in another language for speed or deployment, and 3. There are parts of a system for the programmer, and other parts best left untouched as they are built by the experts. We introduce the Julia programming language and its design --- a dance between specialization and abstraction. Specialization allows for custom treatment. Multiple dispatch, a technique from computer science, picks the right algorithm for the right circumstance. Abstraction, what good computation is really about, recognizes what remains the same after differences are stripped away. Abstractions in mathematics are captured as code through another technique from computer science, generic programming. Julia shows that one can have machine performance without sacrificing human convenience.
1. High-level dynamic programs have to be slow,
Julia presents numerical computing as a balance between specialization and abstraction, using computer-science techniques to select custom algorithms and encode mathematical abstractions. Its design aims to deliver machine performance without sacrificing human convenience.
- Design principles: Julia’s design balances specialization, which enables custom treatment, with abstraction, which identifies what remains unchanged across differences.The paper frames this balance as a “dance” between specialization and abstraction.
- Design principles: Multiple dispatch selects the appropriate algorithm for the relevant circumstance.The paper identifies multiple dispatch as a computer-science technique.
- Design principles: Generic programming captures mathematical abstractions as code.This is presented as another computer-science technique supporting Julia’s abstraction-oriented design.
- Design principles: Julia demonstrates that machine performance can coexist with human convenience.This conclusion summarizes the language’s intended combination of performance and usability.
1 Scientific computing languages: The Julia innovation
Julia addresses the longstanding divide between dynamic-language productivity and C/Fortran performance by combining high-level, generic programming with efficient machine code. Its design aims to eliminate the two-language problem through features that make basic functionality and libraries implementable in Julia.
- Existing languages: Dynamic languages offer high-level productivity, while C and Fortran remain performance standards, leaving computationally intensive numerical work caught between convenience and speed.The paper identifies this productivity–performance tradeoff as a central limitation of existing scientific-computing languages.
- The Julia innovation: Julia’s central innovation is combining productivity and performance through careful language design and complementary technologies.The paper presents Julia as a high-level system intended to deliver machine performance without sacrificing programmer convenience.
- The Two Language Problem: Julia solves the two-language problem by making all basic functionality implementable in Julia rather than forcing programmers to resort to C or Fortran.The design philosophy requires basic operations such as arithmetic, loops, recursion, floating-point operations, and C calls to be fast in the high-level language.
- Design features: Julia combines optional type annotations, multiple dispatch, metaprogramming, type inference, specialization, and JIT compilation to connect expressive code with efficient execution.Multiple dispatch selects implementations using types, while inference, specialization, and LLVM-based JIT compilation support generated machine code.
- Design features: Julia’s type system remains unobtrusive because programmers need not specify types, while dataflow inference allows type information to flow naturally through programs.Type annotations are not required for performance, despite the availability of a sophisticated type system.
2 A taste of Julia
Julia’s introductory examples show concise array and matrix operations, specialized linear-algebra representations, customizable high-performance sorting, plotting interoperability, and tools for probing numerical stability. These examples illustrate how Julia combines convenient syntax with algorithmic specialization and numerical control.
- Arrays and matrices: Julia uses concise, 1-based array indexing and supports whole-expression indexing, while displaying element type and dimensionality such as Array{Float64,2}.A 3×3 example contains 64-bit floating-point values, and expressions such as (A+2I)[3,3] can be indexed directly.
- Arrays and matrices: Julia’s symmetric tridiagonal matrix type stores only diagonal and off-diagonal entries, enabling O(n) memory and O(n) algorithms for recognized tridiagonal systems.The representation is illustrated by Gil Strang’s second-order difference matrix, strang(n).
- Generic sorting: Julia separates sorting algorithms from comparison choices, allowing the same code to alphabetize strings or sort complex numbers by Cartesian or polar representations.The sort command accepts an optional less-than operator, and complex numbers are otherwise incomparable by default.
- Plotting: Julia supports plotting Brownian motion through both Python’s Matplotlib package and the Julia-native Gadfly.jl package.The examples demonstrate interoperability with a popular package ecosystem alongside a plotting system built completely in Julia.
3 Writing programs with and without types
Julia balances human convenience and computer performance by using type inference, parametric and user-defined types, and a transparent performance model. This design avoids requiring explicit type declarations or vectorization as prerequisites for high performance.
- Type inference: Julia’s type inference often provides performance without requiring users to declare types explicitly.The system automatically annotates programs with type bounds through dataflow type inference, although some inherently dynamic programs may remain only trivially typed.
- Parametric types: Parametric array types represent element type and dimensionality, enabling generic arrays and arrays of arrays.For example, Array{T,1} denotes a vector and Array{T,2} denotes a matrix, while T and ndims serve as type parameters.
- Type system: Julia treats user-defined and built-in types without the traditional dynamic-language performance asymmetry.Its type system supports abstract, concrete bits, composite, and immutable composite types, with parameters and unions.
- Parametric types: Julia’s sparse matrix type can store nonnumeric Julia types as nonzeros, alongside specialized structures such as Hermitian, Triangular, Bidiagonal, and Diagonal matrices.This extends numerical matrix representations beyond fixed numeric storage choices.
- Performance: Vectorization supplies type information and can improve performance, but Julia does not require vectorization and runs similar code at speeds comparable to C.Traditional vectorization favors built-in types and can make restructuring unnatural or impossible; Julia’s approach supports high performance without that restructuring.
- Performance: Julia’s transparent performance model gives Vector{Float64} the same in-memory representation as in C or Fortran and supports direct interaction with C through ccall.Together with sophisticated type inference, this transparency is a stated design goal and supports reasoning about data representation and performance.
4 Code selection: Run the right code at the right time
Julia uses code selection and specialization to choose implementations based on argument types while preserving abstraction through reusable function names. This approach supports the same operations across diverse objects, including numeric values and matrix structures.
- Code specialization and abstraction: Julia overloads function names and selects specialized code based on argument types across the software stack.Specialization optimizes for the details of the case at hand while abstraction supports calling code that may not yet be written.
- Examples of overloaded operations: The same operation name can apply to floating-point numbers, integers, and both sparse and dense matrices.These examples illustrate how abstraction gives different objects a shared interface while allowing specialized implementations.
- Examples of overloaded operations: Overloading multiple-argument functions provides a powerful abstraction for choosing different algorithms under a common name.The name “det” can denote determinant computation using different algorithms for different matrix structures.
4.1 Multiple Dispatch
Julia’s multiple dispatch selects function implementations from the types of all arguments, making mathematical operations concise to express and helping achieve high performance. This dynamic, adaptable approach supports numerical computing by matching specialized definitions to interacting values and entities.
- Multiple Dispatch: Multiple dispatch selects a function implementation based on the types of each argument, expressed with argument::Type annotations.The mechanism removes long lists of case statements and contributes to Julia’s speed.
- Multiple Dispatch: Julia uses multiple dispatch to give * distinct meanings for number–function scaling, function–number scaling, and function composition.These operations are easy to express through separate definitions selected by argument types.
- Multiple Dispatch: Generic definitions let function composition make (f^2)(x) compute f(f(x)), matching Gauss’s preferred interpretation of sin2 φ.The behavior follows from defining x^2 generically as x*x.
- Multiple Dispatch: The compiler can choose the sharpest matching definition from input types, keeping execution paths tight and minimal for higher performance.This paradigm suits numerical computing because many important operations involve interactions among multiple values or entities.
- Multiple Dispatch: Julia’s dynamic multiple dispatch is more flexible and adaptable than traditional class-based single dispatch while retaining powerful performance capabilities.The comparison characterizes class-based object orientation as dynamic single dispatch and overloading as static multiple dispatch.
4.2 Code selection from bits to matrices
Julia applies the same code-selection mechanism from low-level numerical representations to high-level matrix operations. Multiple dispatch selects specialized integer, floating-point, dense, and sparse implementations while preserving a common “+” interface.
- Code selection across levels: Julia uses one code-selection mechanism at every level, from low-level numerical representations to high-level matrix operations.The section explicitly describes this mechanism as spanning “from the top to the bottom.”
- Low-level addition: For scalar addition, Julia selects integer addition, floating-point addition, or integer-to-float promotion according to the operand types.An Int–Float pair uses VCVTSI2SD16 for conversion before the floating-point add.
- Low-level addition: Four methods implement the generic ⊕ function for the four Int64 and Float64 operand combinations.The listed methods cover Int64–Int64, Float64–Float64, Int64–Float64, and Float64–Int64.
- Matrix addition: For matrix addition, Julia uses a dense algorithm when either matrix is dense and a sparse algorithm when both matrices are sparse.The same dispatch pattern applies at a higher abstraction level despite differing dense and sparse storage representations.
- Matrix addition: Eight methods implement ⊕ across scalar and matrix additions, combining four low-level methods with four high-level methods.The matrix cases cover dense–dense, dense–sparse, sparse–dense, and sparse–sparse operands.
4.3 The many levels of code selection
Julia translates abstract ideas into efficient execution through code selection at multiple levels. Generic functions use multiple dispatch to select the most specific method based on argument types, from high-level structures to low-level operations.
- Multiple dispatch: Julia’s generic functions select the method with the most specific signature matching all arguments, enabling one name to represent different functions.A generic function can contain multiple methods, and Julia decides which method to use dynamically rather than first dispatching on a single type.
- Namespaces: Namespaces allow the same name to refer to different functions in different circumstances, keeping vocabulary simple and programs easy to read.The name select can denote list selection, database record selection, or a user-defined function.
- Multiple dispatch: Multiple dispatch selects among methods based entirely on the types of all arguments, including alternative implementations for dense, sparse, or specially structured matrices.Julia can define det at an abstract matrix level while choosing efficient methods for specific matrix types.
- Generic programming: Within one structure, Julia preserves a common abstract computation while allowing the compiler to generate different executable code for different contained data types.The norm can be computed in the same exact way for vectors of Float64 or Int32 values, while executable code differs.
- Optimization: Julia applies the same code-selection mechanism from matrix operations to bit operations, optimizing the whole program at compile-time or run-time.This lets Julia pick the right method at the right time across the lowest and highest levels of computation.
4.4 Is “code selection” just traditional object oriented programming?
Julia’s code selection differs from traditional class-based object-oriented programming by using multiple dispatch and generic functions to select methods from combinations of argument types. This design supports numerical computing by matching algorithms to input structures without separating built-in and user-defined code.
- Multiple dispatch: Julia uses multiple dispatch to choose methods from the types of all arguments, rather than single dispatch based on one argument.Generic functions are not constrained by class-based method encapsulation.
- Algorithm selection: Julia matches algorithms to input structures, such as sparse routines for sparse matrices and dense routines for dense matrices.High-level library writers are treated like other users and must match the best algorithm to the input structure.
- Limitations of class-based methods: Class-based designs can require modifying and testing methods across multiple matrix classes when a new structure, such as a tridiagonal matrix, is introduced.The example contrasts encapsulated methods such as sparse-matrix plus and subtraction with Julia’s generic-function approach.
- Generic functions: Generic functions select a general operation when applicable but use a more specific method for structures such as sparse or bidiagonal matrices.The passage illustrates this with specialized implementations of matrix addition and indexing.
4.5 Quantifying the use of multiple dispatch
The section quantifies Julia’s use of multiple dispatch with dispatch ratio, choice ratio, and degree of specialization. Julia’s Base library shows substantially heavier multiple-dispatch usage than previous systems, with many definitions per function and especially high values for common operators.
- Results: Julia’s multiple-dispatch usage is characterized by significantly favorable metrics compared with previous applications.Table 4.5 reports dispatch ratio (DR), choice ratio (CR), and degree of specialization (DoS).
- Metrics: Dispatch ratio (DR) measures the average number of methods in a generic function.It is one of three metrics used to evaluate the extent of multiple dispatch.
- Metrics: Choice ratio (CR) averages each method’s total methods across its generic functions, emphasizing functions with many methods.It equals the sum of squared method counts per generic function divided by the total number of methods.
- Metrics: Degree of specialization (DoS) measures the average number of type-specialized arguments per method.Together with DR and CR, DoS evaluates how extensively multiple dispatch is used.
- Results: Julia’s Base library exhibits a high degree of multiple dispatch, with functions generally having many definitions compared with most multiple-dispatch systems.Statistics rise dramatically for a biased sample of common operators, which are obvious candidates for multiple dispatch.
4.6 Case Study for Numerical Computing
Julia’s numerical-computing case study shows how reusable abstractions and dispatch let programmers define type-specific algorithms while preserving performance. Multiple dispatch extends this approach to specialized operations on combinations of matrix types.
- Reusable abstractions: Julia uses reusable abstractions and polymorphism to make programs efficient, powerful, and maintainable across numerical data types.Linear algebra increasingly spans high-precision numbers, integers, finite-field elements, and rational numbers, beyond floating-point computation.
- Type-based determinant dispatch: A determinant function can dispatch solely on its argument type, selecting formulas for diagonal, triangular, general, and symmetric-tridiagonal matrices.General matrices use QR decomposition, while symmetric tridiagonals use a three-term recurrence.
- Type-based determinant dispatch: Type-based code selection preserves performance because Julia can specialize early when types are known and resume efficient execution after runtime dispatch.When the argument type is unavailable initially, selection occurs at runtime before efficient execution continues inside the chosen method.
- Extensible matrix operations: Julia represents matrix structures as types and operations as functions, allowing users to extend existing functions and add specialized methods for new matrix types.The case study adds a symmetric-arrow matrix type and an external determinant method using its specialized formula.
- Multiple dispatch: Multiple dispatch selects specialized algorithms for combinations of argument types, such as adding a symmetric-arrow matrix to a diagonal without forming a full dense matrix.The general matrix case falls back to dense addition, while the symmetric-arrow-plus-diagonal case updates diagonal entries and preserves off-diagonal data.
5 Leveraging language design for high performance libraries
Julia’s language design choices support high-performance libraries by combining machine arithmetic, compiler optimization, multiple dispatch, macros, and parallelism with convenient, extensible numerical abstractions. These mechanisms enable efficient linear algebra, code generation, distributed computation, and natural—not forced—vectorization.
- Language design and optimization: Julia uses machine arithmetic for integer computations, enabling the compiler to optimize a fixed ten-iteration integer loop into one multiply and one add.For f(k)=5k−1, the tenfold iterate is f^(10)(k)=−2441406+9765625k.
- Linear algebra: Multiple dispatch makes linear-algebra factorizations first-class objects, allowing Julia to solve systems, extract components, and perform least squares directly on compact structures.QRCompactWY stores compact Q and R, while LU stores L and U in packed form.
- Linear algebra: Julia’s LAPACK integration exposes all LAPACK functionality through fully Julia-implemented wrappers callable interactively, enabling users to add missing functionality without a C compiler.Wrappers use ccall and can be extended directly in users’ own code.
- Code generation: Julia macros provide parse-time symbolic code generation, supporting efficient specialized library implementations such as polynomial evaluation with Horner’s rule.The @evalpoly example evaluates the polynomial to 6543 at x=10, and generated code can be inspected inline.
- Code generation: 3 to 4 times faster than compiled Matlab code and 2 to 3 times faster than compiled SciPy code, Julia’s erfinv implementation demonstrates the performance of generated specialized code.A macro rewrite also produced a factor of four performance improvement for real polynomials evaluated at complex arguments.
- Parallelism: Julia implements distributed-memory primitives entirely within the language and builds distributed arrays, pmap, and independent-iteration parallelization on top of them.A Tracy–Widom Monte Carlo simulation on 1024 processors ran in exactly the same wall-clock time as the sequential run.
6 Conclusion and Acknowledgments
Julia was created for numerical computing and has been adopted by people in universities and companies worldwide across diverse fields. Its development depended on contributions from the Julia community and support from colleagues at MIT.
- Conclusion: Julia has been adopted at universities and companies around the world across engineering, mathematics, physical and social sciences, finance, and biotech.The authors report learning regularly that new users have picked up Julia in these fields.
- Conclusion: Julia has become more than a language: it is a place for programmers, physical scientists, and social scientists.
- Acknowledgments: The authors credit the Julia community’s enthusiasm and contributions as essential to making Julia possible.They specifically thank Michael La Croix for his Julia display macros.
- Acknowledgments: Colleagues at MIT provided collegial support that enabled an academic research project to update technical computing and made it more enjoyable.The authors name Jeremy Kepner, Chris Hill, Saman Amarasinghe, Charles Leiserson, Steven Johnson, and Gil Strang.