From ad7807b972ce822bea28bba529bce25fa3af38d3 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Sat, 1 Apr 2023 03:03:07 +0800 Subject: [PATCH 1/9] Refactor code for `Deque` buffer resizing --- src/deque.cr | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/deque.cr b/src/deque.cr index 938f60d993af..e677e379811b 100644 --- a/src/deque.cr +++ b/src/deque.cr @@ -363,7 +363,7 @@ class Deque(T) return unshift(value) if index == 0 return push(value) if index == @size - increase_capacity if @size >= @capacity + resize_if_cant_insert rindex = @start + index rindex -= @capacity if rindex >= @capacity @@ -474,7 +474,7 @@ class Deque(T) # a.push 3 # => Deque{1, 2, 3} # ``` def push(value : T) - increase_capacity if @size >= @capacity + resize_if_cant_insert index = @start + @size index -= @capacity if index >= @capacity @buffer[index] = value @@ -556,7 +556,7 @@ class Deque(T) # a.unshift 0 # => Deque{0, 1, 2} # ``` def unshift(value : T) : self - increase_capacity if @size >= @capacity + resize_if_cant_insert @start -= 1 @start += @capacity if @start < 0 @buffer[@start] = value @@ -581,15 +581,32 @@ class Deque(T) end end - private def increase_capacity + private INITIAL_CAPACITY = 4 + + 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 + + private def resize_if_cant_insert(insert_size = 1) + 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) unless @buffer - @capacity = 4 + @capacity = capacity @buffer = Pointer(T).malloc(@capacity) return end old_capacity = @capacity - @capacity *= 2 + @capacity = capacity @buffer = @buffer.realloc(@capacity) finish = @start + @size From 4c1ffb9df7ea873ec6db9a84e97edcd4dd347bdb Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Sat, 1 Apr 2023 06:02:43 +0800 Subject: [PATCH 2/9] split overloads --- src/deque.cr | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/deque.cr b/src/deque.cr index e677e379811b..892e85989e42 100644 --- a/src/deque.cr +++ b/src/deque.cr @@ -583,6 +583,13 @@ class Deque(T) private INITIAL_CAPACITY = 4 + # behaves like `calculate_new_capacity(@capacity + 1)` + private def calculate_new_capacity + return INITIAL_CAPACITY if @capacity == 0 + + @capacity * 2 + end + private def calculate_new_capacity(new_size) new_capacity = @capacity == 0 ? INITIAL_CAPACITY : @capacity while new_capacity < new_size From c56b7d2f7916a0d06d9a74cae2c15585e796ff91 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Wed, 5 Apr 2023 02:27:51 +0800 Subject: [PATCH 3/9] `Deque.half_slices` from #13280 --- src/deque.cr | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/deque.cr b/src/deque.cr index 892e85989e42..995fc65ea7b9 100644 --- a/src/deque.cr +++ b/src/deque.cr @@ -20,6 +20,7 @@ class Deque(T) @start = 0 protected setter size protected getter buffer + protected getter capacity # Creates a new empty Deque def initialize @@ -150,8 +151,8 @@ class Deque(T) # Removes all elements from `self`. def clear - halfs do |r| - (@buffer + r.begin).clear(r.end - r.begin) + Deque.half_slices(self) do |slice| + slice.to_unsafe.clear(slice.size) end @size = 0 @start = 0 @@ -339,9 +340,9 @@ class Deque(T) # # Do not modify the deque while using this variant of `each`! def each(& : T ->) : Nil - halfs do |r| - r.each do |i| - yield @buffer[i] + Deque.half_slices(self) do |slice| + slice.each do |elem| + yield elem end end end @@ -564,20 +565,21 @@ class Deque(T) self end - private def halfs(&) + # :nodoc: + def self.half_slices(deque : Deque, &) # For [----] yields nothing - # For contiguous [-012] yields 1...4 - # For separated [234---01] yields 6...8, 0...3 + # For contiguous [-012] yields @buffer[1...4] + # For separated [234---01] yields @buffer[6...8], @buffer[0...3] - return if empty? - a = @start - b = @start + size - b -= @capacity if b > @capacity + return if deque.empty? + a = deque.@start + b = deque.@start + deque.size + b -= deque.capacity if b > deque.capacity if a < b - yield a...b + yield Slice.new(deque.buffer + a, deque.size) else - yield a...@capacity - yield 0...b + yield Slice.new(deque.buffer + a, deque.capacity - a) + yield Slice.new(deque.buffer, b) end end From 83e2580d4d8087778aaf451a85903cee064d10d7 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Wed, 5 Apr 2023 03:04:55 +0800 Subject: [PATCH 4/9] Optimize `Deque#concat(Indexable)` --- spec/std/deque_spec.cr | 54 +++++++++++++++++++++++++++++++++++++ src/deque.cr | 60 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/spec/std/deque_spec.cr b/spec/std/deque_spec.cr index e1bc24bd4ed0..cbf361aa5bbd 100644 --- a/spec/std/deque_spec.cr +++ b/spec/std/deque_spec.cr @@ -37,6 +37,22 @@ end private alias RecursiveDeque = Deque(RecursiveDeque) +# Yields `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 @@ -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 diff --git a/src/deque.cr b/src/deque.cr index 995fc65ea7b9..a734509f37f2 100644 --- a/src/deque.cr +++ b/src/deque.cr @@ -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 From fc9f3c5352b3e49e39273ac21804e7ad349ce7d6 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Wed, 5 Apr 2023 04:47:30 +0800 Subject: [PATCH 5/9] Update spec/std/deque_spec.cr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johannes Müller --- spec/std/deque_spec.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/std/deque_spec.cr b/spec/std/deque_spec.cr index cbf361aa5bbd..9aad0b1e53cb 100644 --- a/spec/std/deque_spec.cr +++ b/spec/std/deque_spec.cr @@ -37,7 +37,7 @@ end private alias RecursiveDeque = Deque(RecursiveDeque) -# Yields `Deque{}`, `Deque{0}`, `Deque{0, 1}`, `Deque{0, 1, 2}`, ..., +# 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. From 14ad52b6f3261ec647be2000f961859a80ff05dc Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Wed, 5 Apr 2023 05:06:41 +0800 Subject: [PATCH 6/9] add workaround for 1.0.0 --- src/deque.cr | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/deque.cr b/src/deque.cr index a734509f37f2..a23be92fc880 100644 --- a/src/deque.cr +++ b/src/deque.cr @@ -634,10 +634,11 @@ class Deque(T) b = deque.@start + deque.size b -= deque.capacity if b > deque.capacity if a < b - yield Slice.new(deque.buffer + a, deque.size) + # TODO: this `typeof` is a workaround for 1.0.0; remove it eventually + yield Slice(typeof(deque.buffer.value)).new(deque.buffer + a, deque.size) else - yield Slice.new(deque.buffer + a, deque.capacity - a) - yield Slice.new(deque.buffer, b) + yield Slice(typeof(deque.buffer.value)).new(deque.buffer + a, deque.capacity - a) + yield Slice(typeof(deque.buffer.value)).new(deque.buffer, b) end end From c019c33c9789ca78eac4534c3df6ef1e61fdcda8 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Wed, 5 Apr 2023 05:16:14 +0800 Subject: [PATCH 7/9] split `#resize_if_cant_insert` --- src/deque.cr | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/deque.cr b/src/deque.cr index a23be92fc880..cf4390cf44b5 100644 --- a/src/deque.cr +++ b/src/deque.cr @@ -659,7 +659,14 @@ class Deque(T) new_capacity end - private def resize_if_cant_insert(insert_size = 1) + private def resize_if_cant_insert + new_capacity = calculate_new_capacity + if new_capacity > @capacity + resize_to_capacity(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) From 7a57c119ba5d9ab178c9a53a00d82a5ef850c651 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Wed, 5 Apr 2023 06:01:27 +0800 Subject: [PATCH 8/9] fixup2 --- src/deque.cr | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/deque.cr b/src/deque.cr index cf4390cf44b5..c48938a8be01 100644 --- a/src/deque.cr +++ b/src/deque.cr @@ -659,9 +659,11 @@ class Deque(T) new_capacity end + # behaves like `resize_if_cant_insert(1)` private def resize_if_cant_insert new_capacity = calculate_new_capacity - if new_capacity > @capacity + # `>=` instead of `>` to ensure there is room for one element + if new_capacity >= @capacity resize_to_capacity(new_capacity) end end From 09332cf3971016c994313a42cf1bd216c5476056 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Wed, 5 Apr 2023 06:18:43 +0800 Subject: [PATCH 9/9] nope --- src/deque.cr | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/deque.cr b/src/deque.cr index c48938a8be01..8eb46fcaf3e4 100644 --- a/src/deque.cr +++ b/src/deque.cr @@ -661,10 +661,8 @@ class Deque(T) # behaves like `resize_if_cant_insert(1)` private def resize_if_cant_insert - new_capacity = calculate_new_capacity - # `>=` instead of `>` to ensure there is room for one element - if new_capacity >= @capacity - resize_to_capacity(new_capacity) + if @size >= @capacity + resize_to_capacity(calculate_new_capacity) end end