Skip to content

Add append!/push!/pushfirst!/pop!/popfirst! for BlockVector #116

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/src/lib/public.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ BlockArray
undef_blocks
UndefBlocksInitializer
mortar
blockappend!
blockpush!
blockpushfirst!
blockpop!
blockpopfirst!
Base.append!
Base.push!
Base.pushfirst!
Base.pop!
Base.popfirst!
```


Expand Down
4 changes: 4 additions & 0 deletions src/BlockArrays.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export undef_blocks, undef, findblock, findblockindex

export khatri_rao

export blockappend!, blockpush!, blockpushfirst!, blockpop!, blockpopfirst!

import Base: @propagate_inbounds, Array, to_indices, to_index,
unsafe_indices, first, last, size, length, unsafe_length,
unsafe_convert,
Expand Down Expand Up @@ -48,5 +50,7 @@ include("blockarrayinterface.jl")
include("blockbroadcast.jl")
include("blocklinalg.jl")
include("blockproduct.jl")
include("blockreduce.jl")
include("blockdeque.jl")

end # module
333 changes: 333 additions & 0 deletions src/blockdeque.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
"""
blockappend!(dest::BlockVector, sources...) -> dest

Append blocks from `sources` to `dest`. The number of blocks in `dest` are
increased by `sum(blocklength, sources)`.

This function avoids copying the elements of the blocks in `sources` when
these blocks are compatible with `dest`. Importantly, this means that
mutating `sources` afterwards alters the items in `dest` and it may even
break the invariance of `dest` if the length of `sources` are changed.

The blocks in `dest` must not alias with `sources` or components of them.
For example, the result of `blockappend!(x, x)` is undefined.

# Examples
```jldoctest
julia> using BlockArrays

julia> blockappend!(mortar([[1], [2, 3]]), mortar([[4, 5]]))
3-blocked 5-element BlockArray{Int64,1}:
1
2
3
4
5
```
"""
blockappend!(dest::BlockVector, s1, s2, sources...) =
foldl(blockappend!, (s1, s2, sources...); init = dest)

function blockappend!(dest::BlockVector{<:Any,T}, src::BlockVector{<:Any,T}) where {T}
append!(dest.blocks, src.blocks)
offset = last(dest.axes[1]) + 1 - src.axes[1].first
append!(dest.axes[1].lasts, (n + offset for n in src.axes[1].lasts))
return dest
end

function blockappend!(
dest::BlockVector{<:Any,<:AbstractArray{T}},
src::PseudoBlockVector{<:Any,T},
) where {T}
if blocklength(src) == 1
return _blockpush!(dest, src.blocks)
else
return blockappend_fallback!(dest, src)
end
end

blockappend!(
dest::BlockVector{<:Any,<:AbstractArray{T}},
src::T,
) where {T<:AbstractVector} = _blockpush!(dest, src)

blockappend!(dest::BlockVector{<:Any,<:Any}, src::AbstractVector) =
blockappend_fallback!(dest, src)

blockappend_fallback!(dest::BlockVector{<:Any,<:AbstractArray{T}}, src) where {T} =
blockappend!(dest, mortar([convert(T, @view src[b]) for b in blockaxes(src, 1)]))

"""
blockpush!(dest::BlockVector, blocks...) -> dest

Push `blocks` to the end of `dest`.

This function avoids copying the elements of the `blocks` when these blocks
are compatible with `dest`. Importantly, this means that mutating `blocks`
afterwards alters the items in `dest` and it may even break the invariance
of `dest` if the length of `blocks` are changed.

# Examples
```jldoctest
julia> using BlockArrays

julia> blockpush!(mortar([[1], [2, 3]]), [4, 5], [6])
4-blocked 6-element BlockArray{Int64,1}:
1
2
3
4
5
6
```
"""
blockpush!(dest::BlockVector, blocks...) = foldl(blockpush!, blocks; init = dest)

blockpush!(dest::BlockVector{<:Any,<:AbstractArray{T}}, block::T) where {T} =
_blockpush!(dest, block)

blockpush!(dest::BlockVector, block) = _blockpush!(dest, _newblockfor(dest, block))

_newblockfor(dest, block) =
if Iterators.IteratorSize(block) isa Union{Base.HasShape,Base.HasLength}
copyto!(eltype(dest.blocks)(undef, length(block)), block)
else
foldl(push!, block; init = eltype(dest.blocks)(undef, 0))
end

function _blockpush!(dest, block)
push!(dest.blocks, block)
push!(dest.axes[1].lasts, last(dest.axes[1]) + length(block))
return dest
end

"""
blockpushfirst!(dest::BlockVector, blocks...) -> dest

Push `blocks` to the beginning of `dest`. See also [`blockpush!`](@ref).

This function avoids copying the elements of the `blocks` when these blocks
are compatible with `dest`. Importantly, this means that mutating `blocks`
afterwards alters the items in `dest` and it may even break the invariance
of `dest` if the length of `blocks` are changed.

# Examples
```jldoctest
julia> using BlockArrays

julia> blockpushfirst!(mortar([[1], [2, 3]]), [4, 5], [6])
4-blocked 6-element BlockArray{Int64,1}:
4
5
6
1
2
3
```
"""
blockpushfirst!(A::BlockVector, b1, b2, blocks...) =
foldl(blockpushfirst!, reverse((b1, b2, blocks...)); init = A)

blockpushfirst!(dest::BlockVector{<:Any,<:AbstractArray{T}}, block::T) where {T} =
_blockpushfirst!(dest, block)

blockpushfirst!(dest::BlockVector{<:Any,<:Any}, block) =
_blockpushfirst!(dest, _newblockfor(dest, block))

function _blockpushfirst!(dest, block)
pushfirst!(dest.blocks, block)
dest.axes[1].lasts .+= length(block) - 1 + dest.axes[1].first
pushfirst!(dest.axes[1].lasts, length(block))
return dest
end

"""
blockpop!(A::BlockVector) -> block

Pop a `block` from the end of `dest`.

# Examples
```jldoctest
julia> using BlockArrays

julia> A = mortar([[1], [2, 3]]);

julia> blockpop!(A)
2-element Array{Int64,1}:
2
3

julia> A
1-blocked 1-element BlockArray{Int64,1}:
1
```
"""
function blockpop!(A::BlockVector)
block = pop!(A.blocks)
pop!(A.axes[1].lasts)
return block
end

"""
blockpopfirst!(dest::BlockVector) -> block

Pop a `block` from the beginning of `dest`.

# Examples
```jldoctest
julia> using BlockArrays

julia> A = mortar([[1], [2, 3]]);

julia> blockpopfirst!(A)
1-element Array{Int64,1}:
1

julia> A
1-blocked 2-element BlockArray{Int64,1}:
2
3
```
"""
function blockpopfirst!(A::BlockVector)
block = popfirst!(A.blocks)
n = popfirst!(A.axes[1].lasts)
A.axes[1].lasts .-= n
return block
end

"""
append!(dest::BlockVector, sources...)

Append items from `sources` to the last block of `dest`.

The blocks in `dest` must not alias with `sources` or components of them.
For example, the result of `append!(x, x)` is undefined.

# Examples
```jldoctest
julia> using BlockArrays

julia> append!(mortar([[1], [2, 3]]), mortar([[4], [5]]))
2-blocked 5-element BlockArray{Int64,1}:
1
2
3
4
5
```
"""
Base.append!(dest::BlockVector, sources...) = foldl(append!, sources; init = dest)

Base.append!(dest::BlockVector, src) = append_itr!(dest, Base.IteratorSize(src), src)

function append_itr!(dest::BlockVector, ::Union{Base.HasShape,Base.HasLength}, src)
block = dest.blocks[end]
li = lastindex(block)
resize!(block, length(block) + length(src))
# Equivalent to `i = li; for x in src; ...; end` but (maybe) faster:
foldl(src, init = li) do i, x
Base.@_inline_meta
i += 1
@inbounds block[i] = x
return i
end
Comment on lines +237 to +243
Copy link
Member Author

@tkf tkf May 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using foldl is faster when src is a block vector.

With this PR:

julia> @btime append!(x, $(mortar([rand(3) for _ in 1:100]))) setup=(x = mortar([[0.0]]));
  912.130 ns (5 allocations: 9.29 KiB)

With this PR but using for loop:

julia> @btime append!(x, $(mortar([rand(3) for _ in 1:100]))) setup=(x = mortar([[0.0]]));
  11.031 μs (7 allocations: 2.58 KiB)

da, = dest.axes
da.lasts[end] += length(src)
return dest
end

function append_itr!(dest::BlockVector, ::Base.SizeUnknown, src)
block = dest.blocks[end]
# Equivalent to `n = 0; for x in src; ...; end` but (maybe) faster:
n = foldl(src, init = 0) do n, x
push!(block, x)
return n + 1
end
da, = dest.axes
da.lasts[end] += n
return dest
end

# remove empty blocks at the end
function _squash_lasts!(A::BlockVector)
while !isempty(A.blocks) && isempty(A.blocks[end])
pop!(A.blocks)
pop!(A.axes[1].lasts)
end
end

# remove empty blocks at the beginning
function _squash_firsts!(A::BlockVector)
while !isempty(A.blocks) && isempty(A.blocks[1])
popfirst!(A.blocks)
popfirst!(A.axes[1].lasts)
end
end

"""
pop!(A::BlockVector)

Pop the last element from the last non-empty block. Remove all empty
blocks at the end.
"""
function Base.pop!(A::BlockVector)
isempty(A) && throw(Argument("array must be nonempty"))
_squash_lasts!(A)
x = pop!(A.blocks[end])
lasts = A.axes[1].lasts
if isempty(A.blocks[end])
pop!(A.blocks)
pop!(lasts)
else
lasts[end] -= 1
end
return x
end

"""
popfirst!(A::BlockVector)

Pop the first element from the first non-empty block. Remove all empty
blocks at the beginning.
"""
function Base.popfirst!(A::BlockVector)
isempty(A) && throw(Argument("array must be nonempty"))
_squash_firsts!(A)
x = popfirst!(A.blocks[1])
ax, = A.axes
if isempty(A.blocks[1])
popfirst!(A.blocks)
popfirst!(ax.lasts)
else
ax.lasts[1] -= 1
end
return x
end

"""
push!(dest::BlockVector, items...)

Push items to the end of the last block.
"""
Base.push!(dest::BlockVector, items...) = append!(dest, items)

"""
pushfirst!(A::BlockVector, items...)

Push items to the beginning of the first block.
"""
function Base.pushfirst!(A::BlockVector, items...)
pushfirst!(A.blocks[1], items...)
A.axes[1].lasts .+= length(items)
return A
end
18 changes: 18 additions & 0 deletions src/blockreduce.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# * Those seemingly no-op `where {F, OP}` for forcing specialization.
# See: https://github.com/JuliaLang/julia/pull/33917
# * Per-block reduction strategy is correct only for vectors.

# Let mapping transducer in `Base` compose an efficient nested loop:
Base.mapfoldl(f::F, op::OP, B::BlockVector; kw...) where {F, OP} =
foldl(op, (f(x) for block in B.blocks for x in block); kw...)

Base.mapreduce(f::F, op::OP, B::BlockVector; kw...) where {F, OP} =
mapfoldl(op, B.blocks; kw...) do block
mapreduce(f, op, block; kw...)
end

Base.mapfoldl(f::F, op::OP, B::PseudoBlockArray; kw...) where {F, OP} =
mapfoldl(f, op, B.blocks; kw...)

Base.mapreduce(f::F, op::OP, B::PseudoBlockArray; kw...) where {F, OP} =
mapreduce(f, op, B.blocks; kw...)
2 changes: 2 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ using BlockArrays, LinearAlgebra, Test
include("test_blockbroadcast.jl")
include("test_blocklinalg.jl")
include("test_blockproduct.jl")
include("test_blockreduce.jl")
include("test_blockdeque.jl")
end
Loading