Speedup using in-place evaluation

Ronny Bergmann

When it comes to time critical operations, a main ingredient in Julia is given by mutating functions, that is those that compute in place without additional memory allocations. In the following, we illustrate how to do this with Manopt.jl.

Let’s start with the same function as in πŸ”οΈ Get started with Manopt.jl and compute the mean of some points, only that here we use the sphere $\mathbb S^{30}$ and $n=800$ points.

We first load all necessary packages.

using Manopt, Manifolds, Random, BenchmarkToolsusing ManifoldDiff: grad_distance, grad_distance!Random.seed!(42);

And setup our data

Random.seed!(42)m = 30M = Sphere(m)n = 800Οƒ = Ο€ / 8p = zeros(Float64, m + 1)p[2] = 1.0data = [exp(M, p, Οƒ * rand(M; vector_at = p)) for i in 1:n];

Classical definition

The variant from the previous tutorial defines a cost $f(p)$ and its gradient $\operatorname{grad}f(p)$

f(M, p) = sum(1 / (2 * n) * distance.(Ref(M), Ref(p), data) .^ 2)grad_f(M, p) = sum(1 / n * grad_distance.(Ref(M), data, Ref(p)))
grad_f (generic function with 1 method)

We further set the stopping criterion to be a little more strict. Then we obtain

sc = StopWhenGradientNormLess(5.0e-9)p0 = zeros(Float64, m + 1); p0[1] = 1 / sqrt(2); p0[2] = 1 / sqrt(2)m1 = gradient_descent(M, f, grad_f, p0; stopping_criterion = sc);

We can also benchmark this as

@benchmark gradient_descent($M, $f, $grad_f, $p0; stopping_criterion = $sc)
BenchmarkTools.Trial: 85 samples with 1 evaluation per sample.
 Range (min … max):  47.206 ms … 327.192 ms  β”Š GC (min … max): 12.00% … 85.28%
 Time  (median):     49.606 ms               β”Š GC (median):    15.39%
 Time  (mean Β± Οƒ):   58.936 ms Β±  33.919 ms  β”Š GC (mean Β± Οƒ):  21.24% Β± 10.15%

  β–„β–ˆβ–„                                                           
  β–ˆβ–ˆβ–ˆβ–…β–β–β–β–β–β–β–β–…β–…β–…β–β–β–…β–β–β–β–β–β–β–β–†β–†β–β–β–…β–…β–β–β–β–β–β–β–…β–β–β–β–β–β–β–β–β–β–…β–β–β–β–β–β–β–β–β–β–β–β–β–… ▁
  47.2 ms       Histogram: log(frequency) by time       141 ms <

 Memory estimate: 125.21 MiB, allocs estimate: 837930.

In-place computation of the gradient

We can reduce the memory allocations by implementing the gradient to be evaluated in-place. We do this by using a functor. The motivation is twofold: on one hand, we want to avoid variables from the global scope, for example the manifold M or the data, being used within the function. Considering to do the same for more complicated cost functions might also be worth pursuing.

Here, we store the data (as reference) and introduce temporary memory to avoid reallocation of memory per grad_distance computation. We get

struct GradF!{TD, TTMP}    data::TD    tmp::TTMPendfunction (grad_f!::GradF!)(M, X, p)    fill!(X, 0)    for di in grad_f!.data        grad_distance!(M, grad_f!.tmp, di, p)        X .+= grad_f!.tmp    end    X ./= length(grad_f!.data)    return Xend

For the actual call to the solver, we first have to generate an instance of GradF! and tell the solver, that the gradient is provided in an InplaceEvaluation. We can further also use gradient_descent! to even work in-place of the initial point we pass.

grad_f2! = GradF!(data, similar(data[1]))m2 = deepcopy(p0)gradient_descent!(    M, f, grad_f2!, m2; evaluation = InplaceEvaluation(), stopping_criterion = sc);

We can again benchmark this

@benchmark gradient_descent!(    $M, $f, $grad_f2!, m2; evaluation = $(InplaceEvaluation()), stopping_criterion = $sc) setup = (m2 = deepcopy($p0))
BenchmarkTools.Trial: 166 samples with 1 evaluation per sample.
 Range (min … max):  29.546 ms …  32.534 ms  β”Š GC (min … max): 0.00% … 0.00%
 Time  (median):     29.755 ms               β”Š GC (median):    0.00%
 Time  (mean Β± Οƒ):   30.158 ms Β± 841.370 ΞΌs  β”Š GC (mean Β± Οƒ):  1.15% Β± 2.28%

   β–ˆβ–‡β–„β–ˆ                                                         
  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‡β–„β–ƒβ–ƒβ–β–β–ƒβ–ƒβ–ƒβ–β–β–β–β–ƒβ–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–ƒβ–β–„β–ƒβ–„β–„β–„β–„β–„β–β–ƒβ–β–ƒβ–ƒβ–ƒβ–β–β–ƒβ–β–β–ƒ β–ƒ
  29.5 ms         Histogram: frequency by time         32.4 ms <

 Memory estimate: 3.88 MiB, allocs estimate: 9519.

which is faster than the first solver-call and, more importantly, allocates far less memory: compare both the memory estimate and the number of allocations of the two runs. Note that the results m1 and m2 are of course the same.

distance(M, m1, m2)
4.323123714674778e-9
Technical Details

This tutorial is cached. It was last run on the following package versions.

Status `~/work/Manopt.jl/Manopt.jl/tutorials/Project.toml`
  [47edcb42] ADTypes v1.24.0
  [6e4b80f9] BenchmarkTools v1.8.0
  [5ae59095] Colors v0.13.1
  [a0c0ee7d] DifferentiationInterface v0.7.21
  [31c24e10] Distributions v0.25.131
  [26cc04aa] FiniteDifferences v0.12.34
  [f6369f11] ForwardDiff v1.4.5
  [8ac3fa9e] LRUCache v1.6.2
  [af67fdf4] ManifoldDiff v0.4.5
  [1cead3c2] Manifolds v0.11.29
  [3362f125] ManifoldsBase v2.5.1
  [0fc0a36d] Manopt v0.6.7 `.`
  [91a5bcdd] Plots v1.41.7
  [731186ca] RecursiveArrayTools v4.5.1
  [37e2e46d] LinearAlgebra v1.12.0
  [9a3f8284] Random v1.11.0

This tutorial was last rendered September 9, 2026, 5:28:27.