Welcome to your first lesson in the "Scientific Computing in Julia for Theoretical Physics" course. Building on your existing familiarity with Julia's core features, we'll now begin our deep dive into the high-performance tools that make it so effective for research. This first module is dedicated to assembling your core toolkit for scientific computing.
Today's lesson addresses the heart of numerical work: writing efficient, vectorized computations. Physics simulations, whether for quantum systems or statistical mechanics, are dominated by operations on large arrays and matrices. We will explore how to express these operations in Julia not just correctly, but in a way that is both computationally efficient and highly readable, using broadcasting and array comprehensions. These concepts are the foundation upon which we'll build complex simulations throughout this course.
Arrays in Memory: A Foundation for Performance
Before we manipulate arrays, it's crucial to understand how they are stored in memory. The way data is laid out directly impacts how quickly it can be accessed, especially in nested loops which are common in scientific code. Unlike Python's NumPy, which uses row-major ordering, Julia, along with Fortran and MATLAB, uses column-major ordering.
In a 2D array, this means that elements in the same column are contiguous in memory.

This image illustrates the memory access pattern for row-major order, where elements of a row are contiguous, and column-major order, where elements of a column are contiguous. In Julia, the most efficient way to traverse a multi-dimensional array is to have the first index change the fastest in your loops.
To understand why this matters for performance, it's helpful to consider the computer's memory hierarchy—from fast, small caches close to the processor to slower, larger main memory (RAM). Accessing memory sequentially is much faster because the processor can predictively load chunks of data into its cache. Violating this natural memory order leads to "cache misses," which can significantly slow down your code.
The following video provides an excellent explanation of this memory model and demonstrates the performance impact of looping over an array in the correct (column-major) versus incorrect (row-major) order.
Optimizing Serial Code in Julia 1: Memory Models, Mutation, and Vectorization
Watch this segment from the video "Optimizing Serial Code in Julia 1" by Parallel Computing and Scientific Machine Learning. It will connect the abstract idea of memory layout to concrete performance implications.
Please watch from this section. The speaker explains how a multi-dimensional array is just an indexing scheme over a flat, one-dimensional block of memory and then benchmarks two loops to show the real-world speed difference between column-major and row-major access patterns.
The key takeaway is this: When iterating over multi-dimensional arrays in Julia, structure your loops so that the first index changes most rapidly.
For a 2D array A, this means the loop order should be:
for j in 1:size(A, 2) # Outer loop over columns
for i in 1:size(A, 1) # Inner loop over rows
# do something with A[i, j]
end
end
This ensures you are "walking" through memory the way the data is laid out, maximizing cache efficiency.
Vectorization and Loop Fusion with Broadcasting
While writing explicit loops is sometimes necessary, Julia provides a more powerful and expressive way to perform element-wise operations: broadcasting. This allows you to apply functions to every element of an array, or combine arrays of different (but compatible) sizes in an element-wise fashion, without writing an explicit loop.
The mechanism for this is the dot (.) syntax. For any function f and operator +, f.(A) applies f to each element of A, and A .+ B performs element-wise addition.
2 Julia programming – Numerical Methods for Data Science
This resource from a Cornell University course provides a concise introduction to broadcasting syntax.
First, read the section on broadcasting operations to see the basic syntax, including the convenient @. macro. Then, look at the section on matrix and vector operations to see how the dot syntax distinguishes element-wise operations from standard linear algebra operations.
Broadcasting isn't just syntactic sugar for a loop; it's a signal to Julia's compiler to perform an optimization called loop fusion. When you chain multiple broadcasted operations, like Y = a .* X .+ b, Julia doesn't create a temporary array for a .* X and then add b. Instead, it compiles a single, optimized loop that computes a*x[i] + b for each element and writes the result directly to Y. This avoids unnecessary memory allocations and passes over the data, resulting in significantly faster code.
The following image illustrates the rules Julia uses to "stretch" or "broadcast" smaller arrays to match the shape of larger arrays during these operations.

This diagram shows several examples of broadcasting a smaller array onto a 3x4 matrix. A scalar (shape (1,)), a row vector (shape (4,)), and a column vector (shape (3,1)) can all be validly broadcast. However, an array with an incompatible dimension, like the shape (3,) vector, cannot be broadcast against a 2D matrix.
Let's watch another segment of the "Optimizing Serial Code in Julia 1" video to see a detailed breakdown of how loop fusion works and why it's so important for performance.
Optimizing Serial Code in Julia 1: Memory Models, Mutation, and Vectorization
This segment explains loop fusion in detail.
Watch from this clip. It contrasts a "numpy-style" evaluation of A .* B .* C that creates intermediate temporary arrays with Julia's fused approach, which generates a single, highly efficient function on the fly.
In-place Operations
A crucial aspect of optimization, especially inside performance-critical loops, is avoiding memory allocation altogether. Broadcasting can be combined with the assignment operator to perform an in-place or mutating operation.
The syntax Y .= a .* X .+ b tells Julia to perform the fused broadcast operation and store the results directly into the pre-existing array Y, overwriting its contents. This completely avoids allocating a new array for the result. By convention, functions that modify their inputs have names ending in an exclamation mark (!), like LinearAlgebra.axpy!. The .= syntax is the broadcasted equivalent for this pattern.
To see the dramatic effect of these different strategies, let's look at a practical example: the AXPY operation (a*x + y).
[08x02] Julia Performance Tips and Tools | How to use @time, @profile, @profview, @profview_allocs
The video "[08x02] Julia Performance Tips and Tools" by doggo dot jl provides a step-by-step optimization of AXPY. We'll look at the final steps.
First, watch the segment on using broadcasting, which shows a significant speedup by simply adding dots to the operators. Then, as a preview of what's possible with highly optimized, mutating library functions, watch the final iteration using LinearAlgebra.axpy!. This function is essentially a pre-compiled, in-place version of the broadcasted operation, highlighting the ultimate goal of minimizing allocations.
Concise Array Construction with Comprehensions
The second major tool for vectorized computation is the array comprehension. This provides a concise and readable syntax for constructing an array, and will likely feel familiar from your experience with Python. Comprehensions are ideal for initializing arrays based on their indices, such as setting up a coordinate grid or a potential energy function.
For example, to create a 2D grid representing a potential , you can simply write:
V = [sin(i) + cos(j) for i in 1:10, j in 1:10]
Julia also has generators, which use the same syntax but with parentheses (). These lazily produce values on demand without allocating the full array, which is useful when you want to iterate over values without storing them, for instance sum(i^2 for i in 1:1000).
Array and Broadcasting · Scientific Computing For Physicists
This resource provides a great example directly relevant to physics: creating a lattice.
Please read the section titled "Example: create a meshgrid for triangular lattice". Pay close attention to the two methods shown: one using a list comprehension (mesh1) and the other using broadcasting (mesh2). This is a perfect illustration of how both tools can be used to achieve the same goal. The broadcasting version uses Ref() to "protect" the basis vectors from being broadcasted element-by-element; instead, the whole vector is treated as a scalar in the operation.
This example of building a lattice is a direct first step towards many problems you'll encounter in condensed matter physics.
Summary
In this lesson, we have laid the groundwork for writing high-performance numerical code in Julia. You are now equipped with the essential concepts and tools for efficient array manipulation.
Key Takeaways:
- Column-Major Order: Julia stores arrays with elements in the same column contiguously. Always loop with the first index changing fastest to maximize cache performance.
- Broadcasting with
.: Use the dot syntax to apply functions and operators element-wise. This is more than just convenience; it enables loop fusion. - Loop Fusion: Julia's JIT compiler combines chains of broadcasted operations into a single, efficient loop, avoiding temporary arrays and memory allocations.
- In-place Mutation with
.=: For maximum performance, use.=to write the results of a broadcasted operation into a pre-allocated array, eliminating allocations entirely. - Array Comprehensions: Use the
[...]syntax for a concise and readable way to construct arrays, especially for initializing grids and potentials based on their indices.
In our next lesson, we will continue building our toolkit by exploring the LinearAlgebra.jl package. We will move from element-wise operations to the core routines of numerical linear algebra, such as matrix factorizations (LU, QR, SVD), which are fundamental for solving the systems of equations that appear constantly in theoretical physics.