# Computing tuple replacements

I have some code to compute a tuple replacement

``````function set(tuple::Tuple{T1, T2}, t, i::I) where {T1, T2, I}
if i == 1
(t, tuple[2])
elseif i == 2
(tuple[1], t)
else
tuple
end
end

function set(tuple::Tuple{T1, T2, T3}, t, i::I) where {T1, T2, T3, I}
if i == 1
(t, tuple[2], tuple[3])
elseif i == 2
(tuple[1], t, tuple[3])
elseif i == 3
(tuple[1], tuple[2], t)
else
tuple
end
end
``````

Is this as good as it gets (Iβm considering performance, not beauty)?
As always: thanks!

``````julia> set(t::Tuple,i,val) = ntuple(j -> j == i ? val : t[j], length(t))
set (generic function with 1 method)

julia> tup = (1,2,3);

julia> set(tup,2,0)
(1, 0, 3)

julia> @btime set(\$tup,2,0)
0.022 ns (0 allocations: 0 bytes)
(1, 0, 3)

``````

I donβt think you need all those type annotations, because the tuple is a specialized for each type, anyway.

edit: you can also use:

``````julia> using Setfield

julia> tup = (1,2,3)
(1, 2, 3)

julia> @set! tup[2] = 0
(1, 0, 3)

``````

(I would not bet in any difference in performance for any of these options, including yours)

3 Likes

First test: I dropped you proposal into my test bed

``````function set(tuple::Tuple, t, i::I) where I
ntuple(j -> j == i ? t : tuple[j], length(tuple))
end
``````

Performance approximately identical, better to extend: +1

1 Like

The critical point for performance is having the length of the output tuple to be known at compile time. That is why `ntuple` is a good choice, and it works for input tuples or static arrays because the function will specialize to those, and their lengths are part of their types.

1 Like

Second test:

``````function _set(tuple::Tuple{T1, T2}, t, i::I) where {T1, T2, I}
@set! tuple[i] = t
end

function _set(tuple::Tuple{T1, T2, T3}, t, i::I) where {T1, T2, T3, I}
@set! tuple[i] = t
end
``````

Performance slightly worse. When manually inlining the functions to improve the effect of the macros I run into some trouble. Have to read about Setfield.jl and investigateβ¦

Thanks a lot!

You can also manually code a recursive solution:

``````using Base: tail

function rset((x, ys...)::Tuple, t, i::Integer)
if i == 1
return (t, ys...)
else
return (x, rset(ys, t, i-one(i))...)
end
end
``````

In terms of performance, it looks like itβs the same as yours and const-folds if the arguments are known in advance.

1 Like

This already exists in `Base`.

``````help?> Base.setindex
setindex(c::Tuple, v, i::Integer)

Creates a new tuple similar to x with the value at index i set to
v. Throws a BoundsError when out of bounds.

Examples
β‘β‘β‘β‘β‘β‘β‘β‘β‘β‘

julia> Base.setindex((1, 2, 6), 2, 3) == (1, 2, 2)
true

βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ

setindex(nt::NamedTuple, val, key::Symbol)

Constructs a new NamedTuple with the key key set to val. If key
is already in the keys of nt, val replaces the old value.

julia> nt = (a = 3,)
(a = 3,)

julia> Base.setindex(nt, 33, :b)
(a = 3, b = 33)

julia> Base.setindex(nt, 4, :a)
(a = 4,)

julia> Base.setindex(nt, "a", :a)
(a = "a",)
``````
4 Likes

And we of course can see how it was implemented:

It is with the `ntuple` approach but with `ifelse`, probably to allow simd.

3 Likes

Probably because the apparent βmutationβ is somewhat misleading. The tuple is immutable, thus you are always doing

``````new_tup = (old_tup[1]...)
``````

When using

``````tup = @set tup[1] = 0
``````

or even

``````@set! tup[1] = O
``````

This last one is confusing if used inside a function, it does not mutate the original tuple.

1 Like

For reliable high runtime performance for large-ish Tuples, your best option is likely a generated function though.

``````julia> @generated function set(t::Tuple{Vararg{Any, N}}, x, i) where {N}
Expr(:tuple, (:(ifelse(\$j == i, x, t[\$j])) for j in 1:N)...)
end
set (generic function with 1 method)
``````
``````julia> foreach(4:8:36) do n
t = Ref(ntuple(_-> rand(), n))
@show n
x = Ref(rand())
i = n - 2
ir = Ref(i)
println("literal index");
@btime set(\$t[], \$x[], \$i)
@btime Base.setindex(\$t[], \$x[], \$i)
println("Variable index")
@btime set(\$t[], \$x[], \$ir[])
@btime Base.setindex(\$t[], \$x[], \$ir[])
println()
end
n = 4
literal index
3.099 ns (0 allocations: 0 bytes)
4.379 ns (0 allocations: 0 bytes)
Variable index
2.839 ns (0 allocations: 0 bytes)
4.379 ns (0 allocations: 0 bytes)

n = 12
literal index
4.123 ns (0 allocations: 0 bytes)
6.435 ns (0 allocations: 0 bytes)
Variable index
3.864 ns (0 allocations: 0 bytes)
6.435 ns (0 allocations: 0 bytes)

n = 20
literal index
11.039 ns (0 allocations: 0 bytes)
13.342 ns (0 allocations: 0 bytes)
Variable index
11.039 ns (0 allocations: 0 bytes)
13.342 ns (0 allocations: 0 bytes)

n = 28
literal index
13.092 ns (0 allocations: 0 bytes)
15.394 ns (0 allocations: 0 bytes)
Variable index
13.092 ns (0 allocations: 0 bytes)
15.394 ns (0 allocations: 0 bytes)

n = 36
literal index
15.906 ns (0 allocations: 0 bytes)
54.089 ΞΌs (1510 allocations: 34.28 KiB)
Variable index
15.906 ns (0 allocations: 0 bytes)
55.389 ΞΌs (1510 allocations: 34.28 KiB)
``````

Once a tuple gets the 32-element recursion limit, your only real hope is a generated function. Itβll save orders of magnitude of performance (though they have other. tradeoffs)

4 Likes

I shortly looked into `Setfield.jl` and realized that it does some funky Haskellish stuff. I imagine it is more suited for nested immutable data, which I currently do not have.

1 Like

Thanks a lot, Mason.

For now I settled with with `Base.setindex`, which is βgood enoughβ and easier to manage for me.

On a related note: I noticed

• tuple destructuring vs. tuple indexing has a subtle impact on performance (probably depending on the number of elements you are working with)
• measuring small performance optimizations with @btime is difficult due to around 5% variability between runs even if I `GC.gc()` in between.

It is, though I would be surprised with any performance difference relative to these alternatives (except of course the generated functions for large tuples - although if that was the case, probably one should be considering using preallocated vectors instead). Note that benchmarking these functions can be tricky, because you have to avoid the compiler from constant propagate stuff or cache the values. That is why in the benchmark above mason used all those `Ref` values.

``````julia> using BenchmarkTools, Setfield

julia> x = (1,2,3); xr = Ref(x);

julia> v = 0; vr = Ref(v);

julia> i = 2; ir = Ref(i);

julia> @btime Base.setindex(\$xr[],\$vr[],\$ir[])
4.389 ns (0 allocations: 0 bytes)
(1, 0, 3)

julia> set(x,v,i) = @set! x[i] = v
set (generic function with 1 method)

julia> @btime set(\$xr[],\$vr[],\$ir[])
4.352 ns (0 allocations: 0 bytes)
(1, 0, 3)

``````
1 Like

Addendum: unfortunately it seems that `Base.setindex()` is only type stable for homogeneous tuples. A counterexample would be

``````Base.setindex((1, Int8(2)), 3, 1)
@code_warntype Base.setindex((1, Int8(2)), 3, 1)
``````

with

``````(3, 2)
Variables
#self#::Core.Const(Base.setindex)
x::Tuple{Int64, Int8}
v::Int64
i::Int64
@_5::Bool

Body::Tuple{Int64, Union{Int64, Int8}}
1 β       goto #8 if not \$(Expr(:boundscheck))
2 β %2  = (1 <= i)::Bool
βββ       goto #4 if not %2
3 β %4  = Base.length(x)::Core.Const(2)
β         (@_5 = i <= %4)
βββ       goto #5
4 β       (@_5 = false)
5 β %8  = @_5::Bool
βββ       goto #7 if not %8
6 β       goto #8
7 β %11 = Base.BoundsError(x, i)::Any
βββ       Base.throw(%11)
8 β       nothing
β   %14 = Core.tuple(v, i)::Tuple{Int64, Int64}
β   %15 = Core._apply_iterate(Base.iterate, Base._setindex, %14, x)::Tuple{Int64, Union{Int64, Int8}}
βββ       return %15
``````

I canβt assess if this is expected behaviour or an issue?

This canβt possibly be type stable for a non-homogenous Tuple if the index isnβt known at compile time.

In your example, the index could by any number, so it canβt be known what type will end up where.

But `getproperty` on `struct` works AFAIU, isnβt that the same situation?

it works because the property is usually statically known.

Compare

``````julia> @code_warntype getproperty(Foo(1, "hi"), :y)
MethodInstance for getproperty(::Foo, ::Symbol)
from getproperty(x, f::Symbol) in Base at Base.jl:42
Arguments
#self#::Core.Const(getproperty)
x::Foo
f::Symbol
Body::Union{Int64, String}
1 β      nothing
β   %2 = Base.getfield(x, f)::Union{Int64, String}
βββ      return %2
``````

against

``````julia> get_y(f::Foo) = getproperty(f, :y)
get_y (generic function with 1 method)

julia> @code_warntype get_y(Foo(1, "Hi"))
MethodInstance for get_y(::Foo)
from get_y(f::Foo) in Main at REPL[3]:1
Arguments
#self#::Core.Const(get_y)
f::Foo
Body::String
1 β %1 = Main.getproperty(f, :y)::String
βββ      return %1
``````
1 Like

We can do the same thing with your `setindex` example.

``````julia> set1(t, x) = Base.setindex(t, x, 1)
set1 (generic function with 1 method)

julia> @code_warntype set1((1, Int8(2)), 3)
MethodInstance for set1(::Tuple{Int64, Int8}, ::Int64)
from set1(t, x) in Main at REPL[7]:1
Arguments
#self#::Core.Const(set1)
t::Tuple{Int64, Int8}
x::Int64
Body::Tuple{Int64, Int8}
1 β %1 = Base.setindex::Core.Const(Base.setindex)
β   %2 = (%1)(t, x, 1)::Tuple{Int64, Int8}
βββ      return %2
``````

An important thing to understand here is that when you do

``````@code_warntype f(x, y)
``````

that is just a nice syntax for actually writing

``````code_warntype(f, Tuple{typeof(x), typeof(y)})
``````

That is, it only knows about the types of the function call, not the values. This is why I donβt really like `@code_warntype` and think it is misleading (though itβs often useful)

1 Like

Related discussion of `@code_warntype` here:

1 Like