Skip to content
Open
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
101 changes: 99 additions & 2 deletions src/build/roc/Builtin.roc
Original file line number Diff line number Diff line change
Expand Up @@ -3736,10 +3736,103 @@ Builtin :: [].{
}
}

## Create a list with space for at least capacity items
## Returns a list of the specified capacity without any items.
##
## This is like calling [List.reserve] on an empty list. It's intended for
## building up a list incrementally, for example by calling [List.append] on it:
##
## ```roc
## expect {
## var $squares = List.with_capacity(5)
## for n in 1..=5 {
## $squares = $squares.append(n * n)
## }
## $squares == [1, 4, 9, 16, 25]
## }
## ```
##
## When the final length is known up front, [List.with_capacity] guarantees that
## the appends which follow will not need to reallocate. Whether this is faster
## than starting from `[]` is often marginal: [List.append] grows capacity
## geometrically on its own, so the reallocations it performs are few and
## amortized. The benefit is most pronounced when reallocation would otherwise
## force a full copy of the list.
##
## If you don't know the exact capacity, passing a value larger than necessary
## still avoids reallocation, at the cost of using more memory than is needed.
##
## For more details, see [List.reserve].
with_capacity : U64 -> List(item)

## Ensure this list has room for at least spare additional items.
## Increase a list's capacity by at least the given number of additional items.
##
## When you already know how many items you are about to append, one
## [List.reserve] up front replaces every reallocation those appends would
## otherwise perform along the way:
##
## ```roc
## expect {
## ids = [1.U64, 2, 3]
##
## # 1000 more items are coming, so make room for them all at once
## var $all = ids.reserve(1000)
## for id in 4..=1003 {
## $all = $all.append(id)
## }
##
## $all.len() == 1003
## }
## ```
##
## `reserve(spare)` sizes the allocation to hold exactly `List.len(list) + spare`
## items; it trusts the request instead of rounding it up. If the list is not
## shared and already has room for `spare` more items, it does nothing.
## Otherwise it always performs a heap allocation and copies the existing items
## into it.
Comment on lines +3787 to +3791

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Allocation guarantees are overstated

The documentation promises that reserve(spare) always allocates exactly List.len(list) + spare capacity and copies the existing items, but reserving one slot on a full exclusive list invokes geometric growth, while allocator reallocation can extend storage in place without copying. Users therefore cannot rely on either guarantee when reasoning about capacity or allocation costs.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

##
## Note that the reserve above sits before the loop. Sizing the allocation exactly
## makes [List.reserve] a poor fit for use inside one: a reserve that then gets
## filled completely leaves no room for the next one, so every iteration
## reallocates and copies the whole list, and the loop takes time quadratic in the
## final length:
##
## ```roc
## expect {
## # Quadratic: the two appends fill the list back up to its exact capacity,
## # so the next `reserve(2)` reallocates and copies every item so far.
## var $xs = []
## while $xs.len() < 10 {
## $xs = $xs.reserve(2)
## $xs = $xs.append(0)
## $xs = $xs.append(0)
## }
## $xs.len() == 10
## }
## ```
##
## Reserve the whole amount once before the loop instead, or start from
## [List.with_capacity]. A loop of plain [List.append] calls needs no help at
## all: when an append has to grow the list, it grows the capacity
## geometrically, which keeps such a loop linear in the number of items
## appended.
##
## Whether reserving is actually faster depends on the system allocator: many
## allocators can extend an existing allocation in place, in which case the
## reallocations that appends do on their own are cheap and [List.reserve] makes
## little observable difference. The benefit is most pronounced when reallocation
## would otherwise force a full copy of the list.
##
## [List.reserve] is not free—when more capacity is needed, it always performs a
## heap allocation. Only use it when you actually expect to make use of the extra
## capacity.
##
## When you don't know exactly how many items you'll need, choosing a value
## somewhat higher than necessary is usually safe; a value that's too low may
## force later reallocation, while a value much higher than necessary just wastes
## memory.
##
## If you plan to use [List.reserve] on an empty list, use [List.with_capacity]
## instead.
reserve : List(item), U64 -> List(item)
reserve = |list, spare| list_reserve(list, spare)

Expand Down Expand Up @@ -5580,6 +5673,10 @@ Builtin :: [].{
}

## Ensure this dictionary has room for at least this many additional entries.
##
## Like [List.reserve], this sizes the entry allocation exactly, so call it once
## with the total number of entries you expect to add rather than repeatedly
## inside a loop.
reserve : Dict(k, v), U64 -> Dict(k, v)
where [k.to_hash : k, Hasher -> Hasher]
reserve = |dict, additional| match dict {
Expand Down
Loading