Search code examples
stringjuliabyteallocationlow-latency

Reducing memory allocations when building Vector{UInt8} from parts


I am looking to build a Vector{UInt8} from different parts like so:

using BenchmarkTools
using Random

const a = Vector{UInt8}("Number 1: ")
const b = Vector{UInt8}(", Number 2: ")
const c = Vector{UInt8}(", Number 3: ")
const d = Vector{UInt8}(", Number 4: ")

function main(num2, num4)::Vector{UInt8}

    return vcat(
        a,
        Vector{UInt8}(string(rand(1:100))),
        b,
        Vector{UInt8}(string(num2)),
        c,
        Vector{UInt8}(string(rand(1:100))),
        d,
        Vector{UInt8}(string(num4)),
    )

end

@btime main(70.45, 12) # 486.224 ns (13 allocations: 1.22 KiB)
#Example output: "Number 1: 50, Number 2: 70.45, Number 3: 10, Number 4: 12"

It seems wrong to convert to string then Vector{UInt8}. I dont mind the 1 allocation that occurs when joining the Vectors.


Solution

  • Converting an integer to a vector of digits in UInt8 format can be done very efficiently. Converting a float is a bit more tricky.

    All in all, I think your code is already quite efficient. Here's a suggestion for speeding up the integer code. The floating point code, I haven't been able to improve:

    function tobytes(x::Integer)
        N = ndigits(x)
        out = Vector{UInt8}(undef, N)
        for i in N:-1:1
            (x, r) = divrem(x, 0x0a)
            out[i] = UInt8(r) + 0x30
        end
        return out
    end
    
    tobytes(x) = Vector{UInt8}(string(x))
    
    # notice that we generate random UInt8 values instead of rand(1:100), as this is faster. They still have to be converted according to the character interpretation, though.
    function main2(num2, num4)
        [a; tobytes(rand(0x01:0x64)); b; tobytes(num2); c; tobytes(rand(0x01:0x64)); d; tobytes(num4)]
    end
    

    tobytes for intergers are now close to optimal, the runtime is dominated by the time to pre-allocate the Vector{UInt8}.