Part A
I’m reading some data in from a file then storing it in a struct looking something like this:
# "Big" struct of SVectors
@with_kw struct BigStruct{N}
A :: SVector{N, Int16}
B :: SVector{N, Int16}
C :: SVector{N, Int16}
D :: SVector{N, Int16}
E :: SVector{N, Int16}
F :: SVector{N, Int16}
G :: SVector{N, Int16}
H :: SVector{N, Int16}
I :: SVector{N, Int16}
J :: SVector{N, Int16}
end
To fill the struct, I tried two methods. One requires repetitive code, but is zero-allocation. The other is nice and short, but is slow and allocates:
# Create Struct (Long, Fast)
function test(dat0)
@views dat = BigStruct(A = SVector{20}(dat0[:, 1]),
B = SVector{20}(dat0[:, 2]),
C = SVector{20}(dat0[:, 3]),
D = SVector{20}(dat0[:, 4]),
E = SVector{20}(dat0[:, 5]),
F = SVector{20}(dat0[:, 6]),
G = SVector{20}(dat0[:, 7]),
H = SVector{20}(dat0[:, 8]),
I = SVector{20}(dat0[:, 9]),
J = SVector{20}(dat0[:, 10]))
end
# Create Struct (Short, Slow)
function test2(dat0)
@views vals = [fieldnames(BigStruct)[i] => SVector{20}(dat0[:, i]) for i in 1:10]
dat = BigStruct(; vals...)
end
julia> dat0 = fill(Int16(0), 20, 10);
julia> dat1 = @btime test($dat0);
27.107 ns (0 allocations: 0 bytes)
julia> dat2 = @btime test2($dat0);
15.395 μs (68 allocations: 6.31 KiB)
julia> dat1 == dat2
true
Part B
Some, but not all, of the values in those SVectors
need to be updated. NB: In the real example the SVector
(s) to be updated for each loop iteration depends on some conditions.
I found no performance benefit from unpacking the struct first, and there appears to be some overhead that scales with the size of the struct. Presumably because @reset
replaces the entire thing:
# Update Values (All Together)
function plusone1(dat1)
@reset dat1.A .+= Int16(1)
return dat1
end
# Update Values (unpacked-packed)
function plusone2(dat1)
@unpack_BigStruct dat1
@reset A .+= Int16(1)
@reset dat1.A = A
return dat1
end
# Update Values (Separately)
function plusone3(A)
@reset A .+= Int16(1)
return A
end
Ie, it seems it would be best if I didn’t store the SVectors
in the struct at all:
julia> newvals1 = @btime plusone1($dat1);
9.026 ns (0 allocations: 0 bytes)
julia> newvals2 = @btime plusone2($dat1);
9.027 ns (0 allocations: 0 bytes)
julia> newvals3 = @btime plusone3($dat1.A);
3.248 ns (0 allocations: 0 bytes)
julia> newvals1.A == newvals2.A == newvals3
true
The Svectors
need to be passed to multiple functions, so it would be convenient to store them in one object I can access by variable name. However, it appears that substantially slows down my loop (the real loop is > 10x faster when I split my “big” struct up into 5 smaller ones, and I can probably get more with further splitting).
But then my code looks like:
dat1 = f1(dat1, dat2, dat3)
dat1 = f2(dat1, dat3)
dat3, dat4 = f3(dat3, dat4, dat5)
Part A
- Is there a way to get best of both worlds? Ie, create the struct with zero-allocations without any repetitive code?
- Even the struct definition is very repetitive, is there a way to clean that up?
Part B
- Can I get the best of both worlds and store these
SVectors
in the same object without taking a performance hit?
Full MWE
using StaticArrays, Accessors, Parameters, BenchmarkTools
## Part 1 ##
# "Big" struct of SVectors
@with_kw struct BigStruct{N}
A :: SVector{N, Int16}
B :: SVector{N, Int16}
C :: SVector{N, Int16}
D :: SVector{N, Int16}
E :: SVector{N, Int16}
F :: SVector{N, Int16}
G :: SVector{N, Int16}
H :: SVector{N, Int16}
I :: SVector{N, Int16}
J :: SVector{N, Int16}
end
# Create Struct (Long, Fast)
function test(dat0)
@views dat = BigStruct(A = SVector{20}(dat0[:, 1]),
B = SVector{20}(dat0[:, 2]),
C = SVector{20}(dat0[:, 3]),
D = SVector{20}(dat0[:, 4]),
E = SVector{20}(dat0[:, 5]),
F = SVector{20}(dat0[:, 6]),
G = SVector{20}(dat0[:, 7]),
H = SVector{20}(dat0[:, 8]),
I = SVector{20}(dat0[:, 9]),
J = SVector{20}(dat0[:, 10]))
end
# Create Struct (Short, Slow)
function test2(dat0)
@views vals = [fieldnames(BigStruct)[i] => SVector{20}(dat0[:, i]) for i in 1:10]
dat = BigStruct(; vals...)
end
dat0 = fill(Int16(0), 20, 10);
dat1 = @btime test($dat0);
dat2 = @btime test2($dat0);
dat1 == dat2
## Part 2 ##
# Update Values (All Together)
function plusone1(dat1)
@reset dat1.A .+= Int16(1)
return dat1
end
# Update Values (unpacked-packed)
function plusone2(dat1)
@unpack_BigStruct dat1
@reset A .+= Int16(1)
@reset dat1.A = A
return dat1
end
# Update Values (Separately)
function plusone3(A)
@reset A .+= Int16(1)
return A
end
newvals1 = @btime plusone1($dat1);
newvals2 = @btime plusone2($dat1);
newvals3 = @btime plusone3($dat1.A);
newvals1.A == newvals2.A == newvals3