LoopVectorization.jl: vectorizing Julia loops with @turbo, and what it may reorder
Macro(s) for vectorizing loops.
At a glance
- What is it?
- A Julia macro package that rewrites simple for loops and broadcasts into vectorised code, promising the same arithmetic results as the scalar loop, but only if the loop is free of bounds violations, empty iteration, order dependence and sibling loops.
- Who is it for?
- Adopt LoopVectorization.jl when you have a numerically simple loop that dominates your runtime and that you can prove is free of order dependence, because the macro removes the manual work of writing SIMD-friendly code by hand. Do not adopt it for loops with side effects, reductions that depend on order, or nested loops with siblings, and do not treat it as a drop-in optimisation for arbitrary code.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 9 days ago.
- What is it written in?
- Mainly Julia, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
@turbo is one macro that rewrites the loop body
The package is described simply as macros for vectorising loops, and the whole public surface is one macro. The `@turbo` macro may be used to prefix a `for` loop or a broadcast statement, and it then tries to vectorise the loop to improve runtime performance.
The idea is to write the loop the way you would write it in plain Julia and let the macro find the SIMD structure. The example the documentation gives is a dot product, where the plain version uses `@inbounds @simd` and the vectorised version is the same loop with `@turbo` in place of the SIMD hint:
julia> function mydotavx(a, b)
s = 0.0
@turbo for i ∈ eachindex(a,b)
s += a[i]*b[i]
end
s
endThe `simd` annotation tells LLVM it may vectorise the loop, but leaves the rest of the work to the compiler. The macro takes a different approach: it analyses the loop body and generates the vectorised form itself, which is why it can also handle the parts LLVM handles badly.
There is also a `turbo` variant for broadcasts, and the documentation mentions that the macro currently supports simple nested loops where the inner loop bounds are constant across iterations of the outer loop, with only a single loop at each level of loop nesting. The documentation says those limitations should be removed in a future version, so the current restriction is a known boundary rather than a design goal.
The primary language is Julia and the licence is MIT. There is no homepage listed for the repository, and the documentation is published on the standard Julia documentation hosting under the stable and latest labels.
Four conditions you have to guarantee yourself
The warning section is the most important part of the documentation, and the framing is blunt: misusing LoopVectorization can have serious consequences, and like `@inbounds` it can lead to segfaults and memory corruption.
The documentation then states the expectation, that any time you use the `@turbo` macro with a given block of code you meet four conditions.
First, you are not indexing an array out of bounds, because `@turbo` does not perform any bounds checking. Second, you are not iterating over an empty collection, because iterating over an empty loop such as `for i ∈ eachindex(Float64[])` is undefined behaviour and will likely result in out of bounds memory accesses, so you have to ensure that loops behave correctly. Third, you are not relying on a specific execution order, because `@turbo` can and will re-order operations and loops inside its scope, so correctness cannot depend on a particular order. The documentation makes the consequence concrete: you cannot implement `cumsum` with `@turbo`.
Fourth, you are not using multiple loops at the same level in nested loops.
This is the contract. There is no runtime check that any of the four hold, which is exactly why the same warning that applies to `@inbounds` applies here. The macro assumes that loop iterations can be reordered, and it is the programmer's job to make that assumption true.
The tail is where the win actually is
The dot product example is chosen deliberately, because it shows where the value lies.
By default, LLVM and Julia generate essentially optimal code for the primary vectorised part of a dot product loop. In many cases, such as the dot product, this vectorised part computes four times the SIMD vector width iterations at a time. On the CPU the benchmarks were run on with `Float64` data, the SIMD vector width is 8, meaning it computes 32 iterations at a time.
The problem is the tail. LLVM is very slow at handling the remainder, `length(iterations) % 32`, and the benchmark plots show performance dropping as the size of the remainder increases. For simple loops like a dot product, the documentation says the most important optimisation of LoopVectorization.jl is to handle these tails more efficiently.
The example compares the two implementations on arrays of length 256 and then 255. With 256 elements the remainder is zero and the two versions perform similarly. With 255 elements the remainder becomes 32, the plain version slows down to 36.530 ns against 12.220 ns for the aligned case, while the `@turbo` version stays at 12.226 ns, essentially unchanged.
So the honest pitch is not that `@turbo` makes loops faster than a good compiler. It is that it removes the cliff at the end of the array, and it removes the need for you to hand write the SIMD structure. For a loop over exactly a multiple of the vector width, base Julia may already be doing the work.
Matrix multiply, and the case for a macro over hand written SIMD
The second worked example is a matrix multiplication, and it is the case where the macro earns its keep, because writing this by hand in a way LLVM will vectorise is a chore.
The plain version uses `@inbounds @fastmath` over the outer axes with an inner reduction, and the macro version replaces that with `@turbo`:
julia> function mygemmavx!(C, A, B)
@turbo for m ∈ axes(A,1), n ∈ axes(B,2)
Cmn = zero(eltype(C))
for k ∈ axes(A,2)
Cmn += A[m,k] * B[k,n]
end
C[m,n] = Cmn
end
endThe structure of the two functions is otherwise identical, and the benchmark in the documentation runs both on matrices of size 191 by 189 by 171, reporting a median time of 112.528 microseconds for the macro version with zero bytes allocated.
The point of the example is that a readable, obviously correct nested loop becomes vectorised without the author writing a single intrinsic or worrying about alignment. The outer loop over the two matrix axes is exactly the pattern the macro supports, since the inner loop bounds are constant across iterations of the outer loop and there is one loop at each level.
There is a caveat embedded in the choice of example. `@fastmath` in the baseline and reordering in the macro version are both relaxations of strict floating point semantics, so the two implementations are not required to produce bit-identical results. For a matrix multiply the differences are usually negligible, but for a code path where exact reproduction matters, that is the thing to check first.
Maintenance now runs on grants, not on spare time
The maintenance section is unusual and worth reading in full, because it tells you what will and will not get fixed.
The plan is for `LoopVectorization.jl` to be maintained through the SciML Small Grants program, with a linked grant item about updating LoopVectorization to support changes in Julia 1.12. The instructions for anyone who wants a bug fixed or support extended to another Julia patch release are explicit: either donate to the Small Grants program with a note on the purpose of the donation, or create the pull request that fixes the issue yourself, applying for the small grant first to claim it.
The second condition has a clause that tells you a lot about the project's shape. The small grants program is not a mentorship program, and you would be expected to complete the tasks without guidance from maintainers, past or present.
In practice that means the library's responsiveness is tied to funding and to contributors willing to do the work. That is a coherent model for a library whose maintenance burden is dominated by tracking Julia's own breaking changes, but it does mean an issue you report may sit indefinitely unless you fund or fix it yourself.
The release history is consistent with that. v0.12.175 came on 2026-10-14, v0.12.174 on 2026-05-30 and v0.12.173 on 2025-10-08, so the patch releases track Julia releases over time rather than shipping on a schedule of their own. The repository is not archived and the last push was on 2026-10-14.
Installation is through the standard package manager, and the documentation states that LoopVectorization is supported on Julia 1.1 and later, and is tested on Julia LTS, latest, pre-release and nightly.
Where @turbo is the wrong tool
Three cases are clear from the documentation's own conditions.
The first is a loop with a genuine order dependency. If your loop accumulates into a shared value in a way that the result depends on the sequence, `@turbo` can and will re-order it, and the result changes rather than the speed. The `cumsum` example is the canonical one. A loop that writes to a shared counter, or that has a data race, is in the same category.
The second is a loop whose bounds are not known to be safe. With no bounds checking, an off-by-one in a loop that was previously caught by Julia's array bounds checks becomes a segfault or silent memory corruption rather than an exception. If you are not certain the loop is in bounds, `@inbounds` and `@turbo` are the same mistake with the same consequence.
The third is a loop that is not worth it. The package targets numerically simple loops. A loop with heavy allocations inside, unpredictable branches, or calls that dominate the arithmetic will not become faster, and the macro's analysis cost is paid at compile time on every call site.
The alternative to name is writing the SIMD-friendly loop by hand, either with explicit vector types and `@simd`, or by restructuring so LLVM vectorises it well, including padding the length so the tail disappears. The difference in approach is that the macro automates the analysis and the tail handling, while hand written code gives you full control over what the compiler is allowed to do to the floating point arithmetic. For a loop where reproducibility of the result matters more than the last factor of speed, the hand written route is the safer one, and the macro's reordering is the thing to be most careful about.
Editorial conclusion
Adopt LoopVectorization.jl when you have a numerically simple loop that dominates your runtime and that you can prove is free of order dependence, because the macro removes the manual work of writing SIMD-friendly code by hand. Do not adopt it for loops with side effects, reductions that depend on order, or nested loops with siblings, and do not treat it as a drop-in optimisation for arbitrary code. Verify first that your loop meets the four conditions the documentation lists, and check the generated numerics against the scalar version with tight tolerances, since the library trades memory safety checks for speed.
Frequently asked questions
What does the @turbo macro do in LoopVectorization.jl?
It may be used to prefix a for loop or a broadcast statement, and it then tries to vectorize the loop to improve runtime performance. It assumes that loop iterations can be reordered and currently supports simple nested loops with constant inner bounds and one loop per nesting level.
What are the four conditions for using @turbo safely?
You must not index an array out of bounds, since the macro performs no bounds checking; you must not iterate over an empty collection; you must not rely on a specific execution order, since operations and loops inside the scope can be re-ordered, which is why cumsum cannot be implemented with it; and you must not use multiple loops at the same level in nested loops.
How do I install LoopVectorization.jl?
Use the standard package manager with `using Pkg` and `Pkg.add("LoopVectorization")`. The library is supported on Julia 1.1 and later and is tested on Julia LTS, latest, pre-release and nightly.
Why does @turbo help with a dot product when LLVM already vectorizes it?
Because LLVM is slow at handling the tail, the remainder of length modulo the vector width. The documentation reports that the plain version slows to 36.530 ns on arrays of 255 Float64 elements while the @turbo version stays at 12.226 ns, essentially the same as the aligned case.
How is LoopVectorization.jl maintained?
The plan is to maintain it through the SciML Small Grants program. To get an issue fixed or support for another Julia patch release, you are asked to either donate to the program or fix the issue yourself with a pull request, applying for the grant first, and the program is explicitly not a mentorship program.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/juliasimd-loopvectorization-jl)Was this useful?