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

Apply Array#push's resizing heuristic to #unshift #10750

Merged
merged 4 commits into from
Jun 26, 2021
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
27 changes: 27 additions & 0 deletions spec/std/array_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1498,6 +1498,33 @@ describe "Array" do
a.@offset_to_buffer.should eq(0)
[email protected] eq(buffer)
end

it "repeated unshift/shift does not exhaust memory" do
a = [] of Int32
10.times do
a.unshift(1)
a.shift
end
[email protected] eq(3)
end

it "repeated unshift/pop does not exhaust memory (#10748)" do
a = [] of Int32
10.times do
a.unshift(1)
a.pop
end
[email protected] eq(3)
end

it "repeated unshift/clear does not exhaust memory" do
a = [] of Int32
10.times do
a.unshift(1)
a.clear
end
[email protected] eq(3)
end
end

it "does update" do
Expand Down
40 changes: 29 additions & 11 deletions src/array.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2031,17 +2031,7 @@ class Array(T)
# a.unshift(1) # => [1, "c", "a", "b"]
# ```
def unshift(object : T) : self
# If we have no more room left before the beginning of the array
# we make the array larger, but point the buffer to start at the middle
# of the entire allocated memory. In this way, if more elements are unshift
# later we won't need a reallocation right away. This is similar to what
# happens when we push and we don't have more room, except that toward
# the beginning.
if @offset_to_buffer == 0
double_capacity_for_unshift
end

# At this point we are sure @offset_to_buffer is greater than zero
check_needs_resize_for_unshift
shift_buffer_by(-1)
@buffer.value = object
@size += 1
Expand Down Expand Up @@ -2131,6 +2121,34 @@ class Array(T)
@size == remaining_capacity
end

private def check_needs_resize_for_unshift
return unless @offset_to_buffer == 0

# If we have no more room left before the beginning of the array
# we make the array larger, but point the buffer to start at the middle
# of the entire allocated memory. In this way, if more elements are unshift
# later we won't need a reallocation right away. This is similar to what
# happens when we push and we don't have more room, except that toward
# the beginning.

half_capacity = @capacity // 2
if @capacity != 0 && half_capacity != 0 && @size <= half_capacity
# Apply the same heuristic as the case for pushing elements to the array,
# but in backwards: (note that `@size` can be 0 here)

# `['c', 'd', -, -, -, -] (@size = 2)`
(root_buffer + half_capacity).copy_from(@buffer, @size)

# `['c', 'd', -, 'c', 'd', -]`
root_buffer.clear(@size)

# `[-, -, -, 'c', 'd', -]`
shift_buffer_by(half_capacity)
else
double_capacity_for_unshift
end
end

def remaining_capacity : Int32
@capacity - @offset_to_buffer
end
Expand Down