If you look into the source code, you’ll see that wsample(w)
calls sample(default_rng(), weights(w))
, where weights(w)
creates an instance of the mutable Weights
. As @Benny pointed out, this allocates.
Relevant part of sampling.jl: lines 1044-1056
"""
wsample([rng], [a], w)
Select a weighted random sample of size 1 from `a` with probabilities proportional
to the weights given in `w`. If `a` is not present, select a random weight from `w`.
Optionally specify a random number generator `rng` as the first argument
(defaults to `Random.$(VERSION < v"1.3" ? "GLOBAL_RNG" : "default_rng()")`).
"""
wsample(rng::AbstractRNG, w::AbstractVector{<:Real}) = sample(rng, weights(w))
wsample(w::AbstractVector{<:Real}) = wsample(default_rng(), w)
wsample(rng::AbstractRNG, a::AbstractArray, w::AbstractVector{<:Real}) = sample(rng, a, weights(w))
wsample(a::AbstractArray, w::AbstractVector{<:Real}) = wsample(default_rng(), a, w)
Relevant part of weights.jl: lines 4-23; 69; 82-89
"""
@weights name
Generates a new generic weight type with specified `name`, which subtypes `AbstractWeights`
and stores the `values` (`V<:AbstractVector{<:Real}`) and `sum` (`S<:Real`).
"""
macro weights(name)
return quote
mutable struct $name{S<:Real, T<:Real, V<:AbstractVector{T}} <: AbstractWeights{S, T, V}
values::V
sum::S
function $(esc(name)){S, T, V}(values, sum) where {S<:Real, T<:Real, V<:AbstractVector{T}}
isfinite(sum) || throw(ArgumentError("weights cannot contain Inf or NaN values"))
return new{S, T, V}(values, sum)
end
end
$(esc(name))(values::AbstractVector{T}, sum::S) where {S<:Real, T<:Real} = $(esc(name)){S, T, typeof(values)}(values, sum)
$(esc(name))(values::AbstractVector{<:Real}) = $(esc(name))(values, sum(values))
end
end
@weights Weights
"""
weights(vs::AbstractArray{<:Real})
Construct a `Weights` vector from array `vs`.
See the documentation for [`Weights`](@ref) for more details.
"""
weights(vs::AbstractArray{<:Real}) = Weights(vec(vs))
weights(vs::AbstractVector{<:Real}) = Weights(vs)
By using StatsBase.weights(w::Weights) = w
and directly supplying a Weights
to wsample
, you skip this instantiation. Note that creating our Weights
instance still allocates. But wsample(w)
does not if w is Weights
.
So to improve your code snippet, you just need to move the Weights(wts)
outside of the loop:
using Accessors, StaticArrays, StatsBase, BenchmarkTools
StatsBase.weights(w::Weights) = w
function plusone(wghts, res)
for i in 1:100
idx = wsample(wghts)
@reset res[idx] += 1
end
return res
end
wts = @SVector rand(16)
wghts = Weights(wts)
res = @SVector fill(0, 16)
@btime plusone($wghts, $res);
# 2.400 μs (0 allocations: 0 bytes)
The reason why @Benny had no allocations in his @btime wsample(1:16, $(Weights(wts)))
is because of the interpolation using $
. For example,
julia> @btime wsample(1:16, $(Weights(wts)));
18.737 ns (0 allocations: 0 bytes)
julia> @btime wsample(1:16, Weights($wts));
30.452 ns (1 allocation: 144 bytes)
julia> @btime $(rand(10^6));
2.200 ns (0 allocations: 0 bytes)