Существует ли единый синтаксис для поэлементных операций на месте со скалярами и массивами в Julia?

#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 лет, так что, возможно, все изменилось.