ensmallen: a header-only C++ optimizer library that hands you the iteration loop
A header-only C++ library for numerical optimization --
At a glance
- What is it?
- ensmallen provides gradient-based, gradient-free and constrained optimizers as C++14 headers built on Armadillo. The design gives you a decomposed objective function and a callback hook rather than a training framework, which is either exactly what you want or the wrong shape entirely.
- Who is it for?
- Adopt ensmallen if your project is already C++ and already links Armadillo, and you need an optimizer you can drop into an existing loop without adding a build dependency. Do not adopt it if you are working in Python or R and only want a training routine, or if you cannot accept a C++14 toolchain and Armadillo 10.8.2 or later as hard requirements.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly C++, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The gap ensmallen fills: optimizers without a framework
Most C++ projects that need numerical optimization face the same choice. Either pull in a full machine learning framework and accept its build system, its tensor types and its opinions about how training is organized, or write a gradient descent loop by hand and maintain it. ensmallen is aimed at the second group. The README describes it as a header-only C++ library for non-linear numerical optimization, and the installation instructions confirm the practical consequence: manual installation means copying include/ensmallen.hpp and the include/ensmallen_bits directory to a location your compiler searches. There is no shared object to link against, no runtime to initialize, and no framework object graph to construct before you can minimize a function.
The intended audience is narrow and identifiable. You are writing C++14 or later, you already use Armadillo for matrix types, and you want to call an optimizer on a function you defined yourself. The README lists L-BFGS, SGD, CMAES and Simulated Annealing as examples across gradient descent, gradient-free and constrained categories, which tells you the library is not organized around one algorithm family. If your work lives in Python or R, this is the wrong layer entirely; nothing in the material suggests bindings to other languages.
How the optimizer, the objective and the callback fit together
The architecture visible in the repository is a header decomposition. include/ensmallen.hpp is the single entry point, and include/ensmallen_bits holds the per-algorithm implementation headers. Including the top-level header pulls the optimizers into your translation unit; the compiler then instantiates only what you call. That is the mechanism behind the header-only claim, and it also explains the build-time cost: template instantiation happens in your code, not in a prebuilt library.
The interface contract has three parts. You supply an objective function, the optimizer supplies the iteration, and callbacks observe or intervene during that iteration. The README states that ensmallen allows optional callbacks to customize the optimization process. This is the design decision that separates it from a training framework. The library does not own your data, does not define your model, and does not decide when to stop printing diagnostics. It calls your function with parameters and expects an objective value in return, and the callback mechanism is where logging, early stopping or parameter inspection would attach.
What the supplied material does not describe is the exact signature of that objective function or the callback interface. The README points to example.cpp for L-BFGS usage in a linear regression setting, and that file is the place to read the concrete types. Anyone evaluating the library should treat example.cpp as the real specification, because the README stops at naming the concept.
Installing ensmallen with cmake, and compiling against it
The README gives two cmake paths. With root access, from a build directory: cmake .. followed by sudo make install. Without root, the documented form sets the prefix explicitly, for example cmake .. -DCMAKE_INSTALL_PREFIX:PATH=/home/blah/ followed by make install, which places all ensmallen headers under /home/blah/include/. The cmake route also checks the requirements, and the README notes it can optionally build the tests.
To build and run those tests after configuring, the documented commands are make ensmallen_tests and then ./ensmallen_tests --durations yes. The --durations yes flag is worth noting because it is the only performance-adjacent output the README mentions, and it reports test timings rather than optimizer benchmarks.
Compilation is a single g++ invocation. For a standard install location the README gives g++ prog.cpp -o prog -O2 -larmadillo. For a non-standard prefix you add the include path, for example g++ prog.cpp -o prog -O2 -I /home/blah/include/ -larmadillo. Two things follow from those lines. Armadillo is linked with -larmadillo even though ensmallen itself is header-only, because the matrix types come from Armadillo. And -O2 appears in both examples, which matters more here than in a typical library: optimizer inner loops are template code compiled into your binary, so optimization level directly affects the code you ship.
The dependency floor is the real cost of adoption
ensmallen's requirements section is short and strict. A C++ compiler with C++14 support, Armadillo version 10.8.2 or later, and OpenBLAS or Intel MKL or LAPACK as the BLAS layer Armadillo itself needs. The Armadillo minimum is a hard floor, not a suggestion, and it is the constraint most likely to block adoption in an older codebase. If your project pins an earlier Armadillo for other reasons, ensmallen is not a drop-in addition; you are looking at an Armadillo upgrade that ripples through everything else that includes it.
The header-only property is often read as meaning zero integration cost. That reading is incomplete. There is no link step for ensmallen, but there is a compile-time cost, because every optimizer you instantiate is compiled into your translation units. A project that uses several optimizers across many files will pay that cost repeatedly unless it isolates the instantiations. The README does not discuss build times or techniques for containing them, so this is a trade-off you should measure on your own code rather than assume.
The third constraint is the BLAS backend. The README defers to the Armadillo site for details, which is reasonable, but it means the actual numerical performance characteristics of anything you build sit with Armadillo and its backend choice, not with ensmallen. Choosing Intel MKL versus OpenBLAS is a decision made at the Armadillo layer.
Where ensmallen is the wrong tool
The library is for non-linear numerical optimization on functions you write. It is not a modelling library. If you want to describe a neural network, a gradient boosted tree or a generalized linear model and have something fit it, ensmallen gives you none of that vocabulary. You would define the objective and the gradient yourself, and at that point you are writing the model layer that a framework would have provided.
Gradient-free optimizers such as CMAES and Simulated Annealing are listed, which means the library does not require derivatives. That is useful when gradients are unavailable or unreliable, but gradient-free methods generally need many more objective evaluations than gradient-based ones. The README does not discuss evaluation budgets or convergence guarantees for any algorithm, so anyone choosing between the gradient-based and gradient-free families is choosing without guidance from the project's own documentation.
A second boundary is language. There is no indication in the supplied material of Python, R, Julia or other bindings. If your pipeline is scripted, adopting ensmallen means introducing a compiled component and the interface between it and your scripting layer. That is a real architectural change, not a library swap.
A third boundary is that the README does not state convergence criteria, default tolerances or termination behaviour for the optimizers. Those details matter when an optimizer silently stops early or runs long, and they are not answerable from the material available here.
Alternatives and how the approach differs
The most direct comparison is with a full C++ machine learning framework such as mlpack itself. The relationship is worth stating plainly: ensmallen is developed in the mlpack organization and is used as an optimization component, and the citation list in the README includes Ryan Curtin and Marcus Edel, who are also associated with mlpack. The difference in approach is scope. A framework defines datasets, models and training procedures, and the optimizer is one internal piece. ensmallen extracts that piece and exposes it against a user-supplied function. If you want the model abstractions, the framework is the shorter path. If you already have a model and only need the minimization step, ensmallen avoids carrying the rest.
Against a general-purpose numerical library such as a C++ scientific computing stack, the difference is the optimizer catalogue. A general library gives you linear algebra primitives and expects you to assemble the minimization. ensmallen ships the named algorithms: L-BFGS, SGD, CMAES, Simulated Annealing and others across gradient-based, gradient-free and constrained categories. The trade-off is that you inherit the Armadillo type system to use them.
Against writing your own optimizer, the honest comparison is that a hand-written L-BFGS with correct line search and curvature updates is not a small piece of code. Replacing that with a maintained header is the main argument for the library. The counterargument is that a hand-written loop has no dependency floor and no template instantiation cost, which matters if your Armadillo version is old or your build times are already tight.
Maintenance, releases and licence status
Release cadence is visible in the version history: 3.10.0 in September 2025, 3.11.0 in December 2025, and 3.11.1 in July 2026, with the repository showing a push in August 2026 and not archived. That pattern suggests active maintenance rather than a dormant project, though the material does not describe what changed between versions, so upgrade risk cannot be assessed from the release names alone. The version labels are whimsical ("Sunny Day", "Unexpected Rain") and carry no semantic signal.
Upgrade cost is shaped by the header-only design. Because optimizers are compiled into your code, a version bump means recompiling everything that includes ensmallen.hpp. There is no ABI to worry about in the usual sense, since nothing is linked, but there is also no binary compatibility to lean on as a buffer. The README does not provide a changelog or deprecation policy, so pinning a version and reading the diff between tags is the only way to judge whether an upgrade touches interfaces you use.
On licensing, the README states that unless stated otherwise the source is licensed under the 3-clause BSD license, with a copy in LICENSE.txt. The repository metadata supplied here reports the licence as NOASSERTION, which means the automated classifier did not resolve it to a standard identifier. That discrepancy is worth resolving by reading LICENSE.txt in the checkout you actually use. This is a description of what the files say, not legal advice; if the licence terms matter to your distribution model, have someone qualified read them.
Editorial conclusion
Adopt ensmallen if your project is already C++ and already links Armadillo, and you need an optimizer you can drop into an existing loop without adding a build dependency. Do not adopt it if you are working in Python or R and only want a training routine, or if you cannot accept a C++14 toolchain and Armadillo 10.8.2 or later as hard requirements. Before committing, verify three things: that your compiler and Armadillo version satisfy the stated minimums, that your objective can be expressed through the function interface the library expects, and that the licence file shipped in your checkout matches the 3-clause BSD statement in the README, since the repository metadata reports the licence as NOASSERTION.
Community notes