# Copying a NTuple{UInt8} into a Memory{UInt8} or Vector{UInt8}

**URL:** https://discourse.julialang.org/t/copying-a-ntuple-uint8-into-a-memory-uint8-or-vector-uint8/139649
**Category:** General Usage
**Created:** [September 24, 2026, 7:35pm UTC](https://discourse.julialang.org/t/copying-a-ntuple-uint8-into-a-memory-uint8-or-vector-uint8/139649 "2026-09-24T19:35:28Z")
**Posts on this page:** 1
**Page:** 1

<div class="post-metadata">

### Author: ![mkitti](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mkitti/32/12459_2.png) [@mkitti](https://discourse.julialang.org/u/mkitti)
#### Post date: [September 24, 2026, 7:35pm UTC](https://discourse.julialang.org/t/copying-a-ntuple-uint8-into-a-memory-uint8-or-vector-uint8/139649/1 "2026-09-24T19:35:28Z")

</div>

In reviewing changes lead to [StaticStrings.jl v0.3](https://discourse.julialang.org/t/staticstrings-jl-v0-3-move-to-juliastrings/139648/1) I noticed the following code pattern would out perform other approaches including a plain for loop which does appear to vectorize.

To led to the following code which seemed to outperform other approaches:

```julia
@inline function bulk_copyto!(dest::AbstractVector{T}, doffs::Int,
                              src::NTuple{N,T}, soffs::Int, n::Int) where {T,N}
    @boundscheck begin
        isbitstype(T) && sizeof(NTuple{N,T}) == N*sizeof(T) || throw(ArgumentError("layout mismatch"))
        1 <= soffs && soffs+n-1 <= N && 1 <= doffs && doffs+n-1 <= length(dest) || throw(BoundsError())
    end
    ref = Ref(src)
    GC.@preserve ref dest begin
        p = Ptr{T}(Base.unsafe_convert(Ptr{Cvoid}, ref)) + (soffs-1)*sizeof(T)
        unsafe_copyto!(pointer(dest, doffs), p, n)
    end
    return dest
end

```

It seems that using this approach should be available via _safe_ methods so I create the following issue:

> <https://github.com/JuliaLang/julia/issues/63335>
>
> Copying the bytes of an \`NTuple{N,UInt8}\` into a \`Memory{UInt8}\` or \`Vector{UInt…8}\` is 3–10× slower than achievable. There are two compounding causes:
> 
> 1. There is no specialized \`copyto!\` for tuple sources, so \`copyto!(::Memory, ::NTuple)\` falls back to the generic iterate-based \`copyto!(dest::AbstractArray, src)\` (\`base/abstractarray.jl:935\` on 1.12).
> 2. Writing the loop by hand does not help much either, because \*\*copy loops with a tuple source are not vectorized\*\*, while the identical loop with an array source is.
> 
> Reproduced identically on 1.12.6, 1.13.0-rc1 and 1.14.0-DEV.2306.
> 
> \## Codegen
> 
> \`\`\`julia
> using InteractiveUtils
> vec(ir) = occursin(r"\<\\d+ x i8\>", ir)
> mcpy(ir) = occursin("llvm.memcpy", ir)
> function ir\_of(f, sig)
> io = IOBuffer(); code\_llvm(io, f, sig; optimize=true, debuginfo=:none); String(take!(io))
> end
> 
> @noinline function tuple\_to\_memory(src::NTuple{N,UInt8}) where N
> dst = Memory{UInt8}(undef, N)
> @inbounds for i in 1:N; dst\[i\] = src\[i\]; end
> return dst
> end
> @noinline function memory\_to\_memory(src::Memory{UInt8}, dst::Memory{UInt8})
> @inbounds for i in eachindex(src, dst); dst\[i\] = src\[i\]; end
> return dst
> end
> @noinline function vector\_to\_vector(src::Vector{UInt8}, dst::Vector{UInt8})
> @inbounds for i in eachindex(src, dst); dst\[i\] = src\[i\]; end
> return dst
> end
> 
> for (name, ir) in (("NTuple{256} -\> Memory", ir\_of(tuple\_to\_memory, (NTuple{256,UInt8},))),
> ("Memory -\> Memory", ir\_of(memory\_to\_memory, (Memory{UInt8}, Memory{UInt8}))),
> ("Vector -\> Vector", ir\_of(vector\_to\_vector, (Vector{UInt8}, Vector{UInt8}))))
> println(rpad(name, 24), "memcpy: ", rpad(string(mcpy(ir)),6), " vectorized: ", vec(ir))
> end
> \`\`\`
> 
> Identical output on all three versions:
> 
> \`\`\`
> NTuple{256} -\> Memory memcpy: false vectorized: false
> Memory -\> Memory memcpy: false vectorized: true
> Vector -\> Vector memcpy: false vectorized: true
> \`\`\`
> 
> The tuple source is the discriminator. It makes no difference whether the destination is freshly allocated or preexisting.
> 
> This is not a case of the source being unaddressable. At every size I checked (N = 4, 8, 16, 32, 64, 256) the tuple argument arrives as
> 
> \`\`\`llvm
> ptr nocapture noundef nonnull readonly align 1 dereferenceable(N) %"src::Tuple"
> \`\`\`
> 
> and a freshly allocated destination is \`noalias\`. For N ≤ 64 the loop fully unrolls into N scalar \`load i8\`/\`store i8\` pairs; at N = 256 it stays a scalar loop. Neither form becomes a \`memcpy\`.
> 
> \## Timings
> 
> \`tuple -\> Memory{UInt8}\`, including the \`Memory\` allocation, via BenchmarkTools at \`-O3\`. \`bulk\` is the prototype below.
> 
> Julia 1.12.6:
> 
> | N | bulk (prototype) | \`copyto!\` today | hand loop |
> |---|---|---|---|
> | 8 | 10.9 ns | 25.3 ns | 12.3 ns |
> | 64 | 30.6 ns | 97.5 ns | 28.5 ns |
> | 256 | \*\*92.8 ns\*\* | 320.1 ns | 197.0 ns |
> | 1024 | \*\*111.3 ns\*\* | 1088.2 ns | 736.3 ns |
> 
> Julia 1.14.0-DEV.2306:
> 
> | N | bulk (prototype) | \`copyto!\` today | hand loop |
> |---|---|---|---|
> | 8 | 10.8 ns | 21.5 ns | 12.1 ns |
> | 64 | 30.5 ns | 77.9 ns | 59.0 ns |
> | 256 | \*\*96.7 ns\*\* | 234.9 ns | 142.1 ns |
> | 1024 | \*\*152.3 ns\*\* | 870.8 ns | 516.2 ns |
> 
> \## Prototype
> 
> \`\`\`julia
> @inline function bulk\_copyto!(dest::AbstractVector{T}, doffs::Int,
> src::NTuple{N,T}, soffs::Int, n::Int) where {T,N}
> @boundscheck begin
> isbitstype(T) && sizeof(NTuple{N,T}) == N\*sizeof(T) || throw(ArgumentError("layout mismatch"))
> 1 \<= soffs && soffs+n-1 \<= N && 1 \<= doffs && doffs+n-1 \<= length(dest) || throw(BoundsError())
> end
> ref = Ref(src)
> GC.@preserve ref dest begin
> p = Ptr{T}(Base.unsafe\_convert(Ptr{Cvoid}, ref)) + (soffs-1)\*sizeof(T)
> unsafe\_copyto!(pointer(dest, doffs), p, n)
> end
> return dest
> end
> \`\`\`
> 
> Materialize once with \`Ref\`, then bulk copy. In the benchmarks above the \`Ref\` is fully elided — reported allocations equal the \`Memory\` alone.
> 
> One convenient property: because the source is an immutable value, \`Ref(src)\` is a fresh copy and the destination can never alias it. Unlike array→array \`copyto!\`, no overlap handling is needed.
> 
> \## Possible directions
> 
> 1. \*\*Add specialized methods\*\* — \`copyto!(dest::Memory{T}, src::NTuple{N,T})\` plus the \`Array\` and \`(doffs, soffs, n)\` forms, guarded on \`isbitstype(T) && sizeof(NTuple{N,T}) == N\*sizeof(T)\`. Small and immediately effective.
> 2. \*\*Fix the codegen\*\* so tuple-sourced loops optimize like array-sourced ones. Broader benefit. A specialized method would still be worth having, since it guarantees a \`memmove\` instead of relying on optimizer heuristics.
> 
> Happy to open a PR for (1) if that is the preferred direction.
> 
> \## Related
> 
> \- #11899 "SLP vectorization not working for tuples" (closed 2018)
> \- #15482 "Tuples getting unnecessarily copied to functions just reading a value"
> 
> \## Versioninfo
> 
> \`\`\`
> Julia Version 1.12.6
> Commit 15346901f00 (2026-04-09 19:20 UTC)
> Build Info: Official https://julialang.org release
> Platform Info:
> OS: Linux (x86\_64-linux-gnu)
> CPU: 8 × AMD FX(tm)-8350 Eight-Core Processor
> WORD\_SIZE: 64
> LLVM: libLLVM-18.1.7 (ORCJIT, bdver1)
> \`\`\`
> 
> Also checked on 1.13.0-rc1 and 1.14.0-DEV.2306 (same machine). Note this is an older microarchitecture (\`bdver1\`); the tuple-vs-array comparison is internally controlled, but I have not confirmed the same codegen on AVX2/AVX-512 hardware.
> 
> 🤖 Generated with \[Claude Code\](https://claude.com/claude-code)

I wanted to have a more general discussion here in case there is a reason for the current compiler behavior or the lack of a method to accelerate copying from a `NTuple` to `Vector` or `Memory`.
