Skip to content
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

Optimize Deque#concat(Indexable) #13283

Merged
54 changes: 54 additions & 0 deletions spec/std/deque_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ end

private alias RecursiveDeque = Deque(RecursiveDeque)

# Yields multiple forms of `Deque{}`, `Deque{0}`, `Deque{0, 1}`, `Deque{0, 1, 2}`, ...,
# `Deque{0, 1, 2, ..., max_size - 1}`. All deques have *max_size* as their
# capacity; each deque is yielded *max_size* times with a different start
# position in the internal buffer. Every deque is a fresh instance.
private def each_queue_repr(max_size, &)
(0..max_size).each do |size|
max_size.times do |i|
x = Deque(Int32).new(max_size, 0)
x.rotate!(i)
x.pop(max_size - size)
x.fill &.itself
yield x
end
end
end

describe "Deque" do
describe "implementation" do
it "works the same as array" do
Expand Down Expand Up @@ -242,6 +258,44 @@ describe "Deque" do
a.concat((4..1000))
a.should eq(Deque.new((1..1000).to_a))
end

it "concats indexable" do
each_queue_repr(16) do |a|
size = a.size
b = Array.new(10, &.+(size))
a.concat(b).should be(a)
a.should eq(Deque.new(size + 10, &.itself))
end

each_queue_repr(16) do |a|
size = a.size
b = Slice.new(10, &.+(size))
a.concat(b).should be(a)
a.should eq(Deque.new(size + 10, &.itself))
end

each_queue_repr(16) do |a|
size = a.size
b = StaticArray(Int32, 10).new(&.+(size))
a.concat(b).should be(a)
a.should eq(Deque.new(size + 10, &.itself))
end

each_queue_repr(16) do |a|
size = a.size
b = Deque.new(10, &.+(size))
a.concat(b).should be(a)
a.should eq(Deque.new(size + 10, &.itself))
end
end

it "concats itself" do
each_queue_repr(16) do |a|
size = a.size
a.concat(a).should be(a)
a.should eq(Deque.new((0...size).to_a * 2))
end
end
end

describe "delete" do
Expand Down
75 changes: 74 additions & 1 deletion src/deque.cr
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,65 @@ class Deque(T)
end

# Appends the elements of *other* to `self`, and returns `self`.
def concat(other : Enumerable(T))
#
# ```
# deq = Deque{"a", "b"}
# deq.concat(Deque{"c", "d"})
# deq # => Deque{"a", "b", "c", "d"}
# ```
def concat(other : Indexable) : self
other_size = other.size

resize_if_cant_insert(other_size)

index = @start + @size
index -= @capacity if index >= @capacity
concat_indexable(other, index)

@size += other_size

self
end

private def concat_indexable(other : Deque, index)
Deque.half_slices(other) do |slice|
index = concat_indexable(slice, index)
end
end

private def concat_indexable(other : Array | StaticArray, index)
concat_indexable(Slice.new(other.to_unsafe, other.size), index)
end

private def concat_indexable(other : Slice, index)
if index + other.size <= @capacity
# there is enough space after the last element; one copy will suffice
(@buffer + index).copy_from(other.to_unsafe, other.size)
index += other.size
index == @capacity ? 0 : index
else
# copy the first half of *other* to the end of the buffer, and then the
# remaining half to the start, which must be available after a call to
# `#resize_if_cant_insert`
first_half_size = @capacity - index
second_half_size = other.size - first_half_size
(@buffer + index).copy_from(other.to_unsafe, first_half_size)
@buffer.copy_from(other.to_unsafe + first_half_size, second_half_size)
second_half_size
end
end

private def concat_indexable(other, index)
appender = (@buffer + index).appender
buffer_end = @buffer + @capacity
other.each do |elem|
appender << elem
appender = @buffer.appender if appender.pointer == buffer_end
end
end

# :ditto:
def concat(other : Enumerable(T)) : self
other.each do |x|
push x
end
Expand Down Expand Up @@ -593,13 +651,28 @@ class Deque(T)
@capacity * 2
end

private def calculate_new_capacity(new_size)
new_capacity = @capacity == 0 ? INITIAL_CAPACITY : @capacity
while new_capacity < new_size
new_capacity *= 2
end
new_capacity
end

# behaves like `resize_if_cant_insert(1)`
private def resize_if_cant_insert
if @size >= @capacity
resize_to_capacity(calculate_new_capacity)
end
end

private def resize_if_cant_insert(insert_size)
new_capacity = calculate_new_capacity(@size + insert_size)
if new_capacity > @capacity
resize_to_capacity(new_capacity)
end
end

private def resize_to_capacity(capacity)
old_capacity, @capacity = @capacity, capacity

Expand Down