tl;dr
- Is there a built-in way to interpret (or instantiate from) a byte-packed buffer to a Julia struct?
- If not, is there an efficient, non-allocating method to instantiate a new struct from the buffer data?
I am replacing (and trying to add significant functionality to) some legacy Matlab and python code that reads tcp telemetry from some instrumentation. The messages are byte-packed (which is out of my power to change). It was straightforward to read the incoming packets to a byte-array, but I am struggling to make that data quickly readable by downstream consumers.
The read-in buffer will be rapidly updated and a number services will access the data asynchronously. Each acquires a lock before doing so (which prevents buffer updates), so I want the access to be as fast as possible. My initial hope was to just use reinterpret
to view the data as a Julia struct (making accessing the data simple), but because the data is byte-packed that does not generally work. For instance, when trying to interpret a buffer as
struct Tlm
a::UInt16
b::UInt32
c::UInt32
d::UInt32
e::UInt32
f::UInt32
g::UInt32
h::UInt32
i::UInt32
j::UInt32
k::UInt32
end
the incoming message has 42 bytes, but the Julia structure has padding make it 44 bytes (after searching through the forums I think I understand why now, but its still a blocker).
Given this, i have pursued two paths
- Renterpret each field independenty and then make the object using the constructor
- Make a wrapper structure that uses getfield calls to reinterpret individual fields whenever requested
Option 2 reproduces a lot of functionality of structures and had some added complications, so I am leaning away from it. For option 1 the code is simple, but makes dozens of allocations and takes 10 us in my initial attempt, for which an MWE is
function unpack(::Type{T}, buffer::Vector{UInt8}, buffer_lock) where {T}
N = fieldcount(T)
ft = fieldtypes(T)
nbs = sizeof.(ft)
idx2 = cumsum(nbs)
idx1 = (0, idx2[1:end-1]...)
idx1 = idx1 .+ 1
newT = lock(buffer_lock) do
T(Tuple(reinterpret(ft[i], buffer[idx1[i]:idx2[i]])[1] for i in 1:N)...)
end
return newT
end
I havenβt been able to make the object without first collecting the arguments with Tuple, and splatting them back out; profiling indicates there are a lot of inference calls there (and maybe the tuple is getting put on the heap?). Either way, each unpack takes ~10 us and has over 50 allocations for several kilobytes. Given they way it will be used, this is on the boundary of acceptable, but seems way less efficient than it should be. So, that brings me to my questions. Is there an efficient way to either interpret a byte-packed buffer as a Julia struct, or at least to efficiently convert it? More specifically on my MWE, is there a way to make an object with its constructor using a generator, or in some other way not needing to collect the arguments and splat them back out again?
Thanks!