#performance #julia #allocation #in-place
#Производительность #джулия #распределение #на месте
Вопрос:
Рассмотрим следующий тип накопителя, который работает как массив в том смысле, что вы можете использовать его, но отслеживает только его среднее значение:
mutable struct Accumulator{T}
data::T
count::Int64
end
function Base.push!(acc::Accumulator, term)
acc.data = term # <-- in-place addition
acc.count = 1
acc
end
mean(acc::Accumulator) = acc.data ./ acc.count
Я хочу, чтобы это работало для T
скалярного или массивного типа. Однако
оказывается, что для T
того, чтобы быть типом массива, добавление в push!
создает временный. Это связано с тем, что в Julia x =a
эквивалентно x=x a
, и я подозреваю, что Julia не может гарантировать это acc.data
и term
не использовать псевдоним.
Простое исправление — заменить =
на поэлементное добавление, . =
. Однако это приведет к нарушению скалярных типов, которые этого не допускают. Итак, единственный способ, который я придумал для решения этой проблемы, — добавить специализацию следующего вида:
function Base.push!(acc::Accumulator, term::AbstractArray)
acc.data . = term # <-- element-wise addition
acc.count = 1
acc
end
Однако это несколько уродливо, а также хрупко … кто-нибудь знает лучший способ сделать это, предпочтительно в общем виде и без временного создания?
Комментарии:
1. Может быть более элегантный ответ, чем мой ответ ниже, который использует такие свойства итератора , как
IteratorSize
иIteratorEltype
.2. Я обновил свой ответ, включив в него подход, который использует свойства итератора для общей обработки различных возможных сценариев.
Ответ №1:
Как ни странно, Number
s в Julia являются итеративными, но, похоже, это нам здесь не поможет, потому setindex!
что для s нет метода Number
.
Вот два разных подхода. Первый использует свойства итератора, а второй просто немного исправляет сигнатуры методов для решения угловых случаев.
Особенности итератора
Мы можем использовать IteratorSize
признак, чтобы различать скаляры и векторы. Для скаляров Base.IteratorSize(x)
возвращает Base.HasShape{0}
. Для массивов Base.IteratorSize(x)
возвращает Base.HasShape{N}
значение, где N
— количество измерений массива.
mutable struct Accumulator{T}
data::T
count::Int64
end
function Base.push!(acc::Accumulator{T}, term::S) where {T, S}
_push_acc!(Base.IteratorSize(T), Base.IteratorSize(S), acc, term)
end
function _push_acc!(::Base.HasShape{0}, ::Base.HasShape{0}, acc::Accumulator, term)
acc.data = term
acc.count = 1
acc
end
function _push_acc!(::Base.HasShape{N}, ::Base.HasShape{N}, acc::Accumulator, term) where {N}
acc.data . = term
acc.count = 1
acc
end
function _push_acc!(::Base.HasShape{M}, ::Base.HasShape{N}, ::Accumulator, ::Any) where {M, N}
throw(ArgumentError("Accumulator and term have inconsistent shapes"))
end
В действии в REPL:
julia> a = Accumulator(1, 0)
Accumulator{Int64}(1, 0)
julia> b = Accumulator([1, 2], 0)
Accumulator{Array{Int64,1}}([1, 2], 0)
julia> push!(a, 42)
Accumulator{Int64}(43, 1)
julia> push!(b, [3, 4])
Accumulator{Array{Int64,1}}([4, 6], 1)
julia> push!(a, [5, 6])
ERROR: ArgumentError: Accumulator and term have inconsistent shapes
Stacktrace:
[1] _push_acc!(::Base.HasShape{0}, ::Base.HasShape{1}, ::Accumulator{Int64}, ::Array{Int64,1}) at ...
[2] push!(::Accumulator{Int64}, ::Array{Int64,1}) at ...
[3] top-level scope at REPL[6]:1
julia> push!(b, 10)
ERROR: ArgumentError: Accumulator and term have inconsistent shapes
Stacktrace:
[1] _push_acc!(::Base.HasShape{1}, ::Base.HasShape{0}, ::Accumulator{Array{Int64,1}}, ::Int64) at ...
[2] push!(::Accumulator{Array{Int64,1}}, ::Int64) at ...
[3] top-level scope at REPL[7]:1
Исправление сигнатур методов
Вместо того, чтобы использовать свойства итератора, мы могли бы просто внести пару небольших изменений в push!
сигнатуры ваших методов, чтобы предотвратить перенос массива в скаляр.
mutable struct Accumulator{T}
data::T
count::Int64
end
function Base.push!(acc::Accumulator, term)
acc.data = term
acc.count = 1
acc
end
function Base.push!(acc::Accumulator{T}, term::AbstractArray) where {T <: AbstractArray}
acc.data . = term
acc.count = 1
acc
end
function Base.push!(::Accumulator, ::AbstractArray)
throw(ArgumentError("Can't push an array onto a scalar"))
end
Теперь мы получим разумное сообщение об ошибке, если попытаемся поместить массив в скаляр:
julia> a = Accumulator(42, 0)
Accumulator{Int64}(42, 0)
julia> push!(a, [1, 2])
ERROR: ArgumentError: Can't push an array onto a scalar
Комментарии:
1. Спасибо за ваш ответ и понимание важного углового случая! Я хотел бы оставить этот вопрос немного открытым на случай, если кто-то придумает что-то общее, особенно то, что работает с общими «итерируемыми» объектами. В качестве касательной: кто в девяти кругах ада решил, что это отдаленно приемлемая идея сделать числа итеративными ?!?
2. Я не очень внимательно следил за проблемой, но вот один аргумент в пользу итеративных чисел . Конечно, этому комментарию уже 5 лет, так что, возможно, все изменилось.