Optimizing function with vector reassignments

The basic thing to keep in mind is that Julia’s compiler (like many compilers) specializes code mainly based on the types of objects, not based on their values (which can change at runtime). [i,j,k] is a Vector{Int}, which does not contain the length of the array in the type. In principle, the compiler could be made clever enough to infer it in this case — essentially by constant propagation — since [i,j,k] appears literally in the source code, though it doesn’t currently do this AFAIK. (Even that analysis can be tricky, e.g. because various functions can resize a Vector at runtime.) But what about all of your other vectors, like neighboratom? How is the compiler supposed to know their lengths?

In contrast, @SVector[i,j,k] creates an object of type SVector{3,Int}, where the length 3 is part of the type (and internally to the SVector the data is stored in a Tuple{Int,Int,Int}, for which the length is also part of the type, and the compiler has special optimizations for tuples). So, the compiler can specialize on this in several ways:

  1. local-variable SVector objects need not be stored on the heap, and can be stored on the stack or in registers (exactly as if the individual components were separate scalar variables)
  2. arrays of SVector objects can store them “inline” in the array data. e.g. a Vector{SVector{3,Float64}} stores an arbitrary number of 3-component SVectors, each as 3 numbers one after another in the array data (exactly like an array of Float64 of 3x the length). And when you access an element of the array, the compiler knows it is a 3-component SVector and can specialize the resulting code using (1) and (3).
  3. operations on SVector can be specialized on the length and completely unrolled. For example, adding two SVectors can be accomplished with 3 scalar additions and no loop.
5 Likes