-
Notifications
You must be signed in to change notification settings - Fork 18k
proposal: allow append to update slice capacity #16976
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
Comments
I think it exposes too much implementation details,
and it ties the language to a specific runtime
implementation.
|
This seems too minor an optimization to introduce a new language feature to support it. You could have a big template slice I think it would be nice to have make([]int, n) be allowed to allocate a cap>n if it wants (whereas make([]int, n, c) would need to allocate a cap of exactly c). |
@minux Perhaps it should be an implementation detail? I looked through the spec and didn't see anything that appending had to not return a modified capacity slice, only that it could. Other implementations could still behave as they do today and leave it as a noop. @randall77 I am not tied to the particular implementation, I would just like a way to use all the memory that has been allocated. The template slice is a nice idea, but won't that pay the penalty of also having to read from template? I was aiming at a zero copy implementation. Definitely agree about the two arg make being allowed to pick a smarter capacity, but as you said it would have to wait until Go 2. It seems like my proposal would be a backwards compatible workaround in the mean time. |
Yes, the template slice would mean an extra copy you wouldn't need otherwise. Should be easily cacheable though. |
@randall77 I believe the performance difference is non trivial. I made a change that makes Using that, I changed Before:
After:
The first number in the name is the size of the slice, in order to try and cause degenerate allocation patterns. The second number is how many times in the loop said buffer was written to the Once there are 8 writes or more, using the entire capacity of the underlying span becomes important. A common use case would be reading data off the network, which typically comes in small chunks, but may been to be compacted. |
This is too low-level and tricky. People have trouble understanding append as it is, and the proposal would make its behavior harder to understand. I admit a bias, though: changes like this to a language to exploit a hidden detail of the implementation bother me. |
@robpike I admit it is tricky, but before I put this proposal together, I actually tried it expecting the described behavior. It has always been an expectation of mine that As for revealing implementation details: I think your argument would be stronger if it was earlier in the history of Go, when the runtime was changing more often. There is still performance on the table, and the alternative I think would be overall worse: copy pasting the current buffer allocation scheme. Once people do this, they will either be locked into it and miss out as the allocator evolves, or have to check the Go minor version to pick which copy of the allocator to use. By not doing inside Go, you don't stop it from happening; you just force it into everyone's application, and badly. The application and the runtime need to align at least somewhat to not waste CPU cycles. The implementation details eventually become the API for better or worse. |
Friendly ping. Are there plans (or even remote possibility) for the memory allocator to change substantially in ways that would make this proposal obsolete? Are there alternatives that would still accomplish the "give me a slice of exactly this length, but I'm flexible on the capacity"? |
Even if there's no plan to change the memory allocator, I still think this
language change ties the language too closely to one specific memory
allocator implementation.
What we could do instead is to change the append implementation so that it
first stretches the cap to the currently available maximum (if it fits the
new data) before abandoning the current backing array and allocate a new
one. That is, we add a tiny linear growth step before exponential growth.
|
@minux, that would be nice but I don't know if it is possible.
and
In the first situation you can reuse the backing array, but in the second case you can't. But you can't tell based on simply the type passed to append and the rounded-up size of the allocation area. |
If len==cap, the maximum amount available is cap, not whatever is in the underlying array. Ignoring cap in this case during append would eliminate the semantics of the x[i:j:k] slice operation, which was added for a reason. I agree with Keith that it would be nice if make([]T, n) could return cap>n to begin with, but it cannot today. If exposing the underlying array size returned by make is important, then instead of overloading append, a better way would be to adjust the calls to make to ask for it explicitly, not try to reconstruct it later. For example, a make-with-maximal-cap syntax might be make([]T, n, _) or make([]T, n, ...). |
@rsc If the make syntax was overloaded, what would be the appropriate thing to do for Channels may not meaningfully benefit from having a flexible cap (they could, I just didn't think about it before now). They would fill the available size up, but channels don't resize. Maps would also be somewhat odd, since they are not as simple as blunt slices. Is it possible to make better allocation decisions for either of these types if you had flexibility? |
I was thinking about having append expand into unused space,
but now I understand it's both difficult to implement (because
our gc bitmap doesn't keep track of the object boundary at
byte granularity), and also hard to implement efficiently (because
expanding backing array into unused space will result in a race
if more than one goroutine tries to expand one slice, which result
in unnecessary contention.)
Expanding the make syntax for slices doesn't mean we also have
to accept the new syntax for other makeable types (channel, map)
too. For example, the make([]T, len, cap) has no directly counterpart
for make(chan T, cap), make(map[K]V, init_cap). That being said,
I still think it ties the language to the implementation too tight and
the spec can't guarantee anything about what the capacity for
make([]int, N, ...) would be (except that it's >= N).
|
I remain opposed. We should try not to expose what are in essence implementation details through the language. |
@minux & @robpike Would you prefer a library function instead? Perhaps something like Also, would you mind clarifying your opposition? The change I was originally proposing is an implementation change, not a language change. It would be possible to make |
I think Russ already explained the reason why the original proposal can't
|
@minux Fair enough. What do you think about the library call? If the Go team doesn't agree this is a problem, there probably isn't much point in continuing on this proposal. |
I'm going to decline this proposal because it sounds like it's either not feasible for Go 1 or exposes too many internals. This could be done in a third-party library, though. Just hard-code the slab sizes for each (Go version, arch) pair, and make your own But then Go isn't bound to that detail if the allocator changes later and e.g. stops using fixed slab sizes. |
Background
When allocating byte slices, the
len
andcap
values of the slice are set based on the arguments passed in tomake
:From my understanding of the memory allocator, it appears that there is actually a small amount of wasted space in the underlying span if the size doesn't match up properly. It seems that the only way currently to expand the capacity to match the available underlying is by calling
append
, which allocates a slice that does fill the span and sets thecap
appropriately. This causes an unnecessary copy and reallocationProposal
I would like to propose a minor, language level change that alters the
append
function. If the passed in slice haslen == cap
, and there are no arguments given toappend
, then the returned slice should have the capacity set to the maximum amount available. For example:Note that this overload would be distinct from the vararg version that may have an empty slice provided. The user intended behavior is less clear in that scenario, and it may be surprising to have the capacity change when the argument is empty. The intent is much more clear when there are no extra arguments to the
append
call at all.As before,
bar = append(foo)
will not alterfoo
. Though I am not strongly tied to this exact solution, there needs to be some good answer for optimizing Go program memory that doesn't involve knowing the exact underlying span sizes.A secondary use case for this would be for implementing a "prepend" operation on slices. When a slice needs to be expanded for a prepend operation, the newly resized slice needs to be as big as possible in order to leave as much room as possible at the beginning. Reallocating the slice has to be done manually, which means using
make
, which forces the capacity to less than the max that the analogousappend
operation would use.A third place where this would be useful is in
bytes.Buffer
. The functionBuffer.grow
implements slice resizing manually, meaning that it also has to usemake
. It could expand its reallocated slices more effectively if the capacity was set to the true maximum.The text was updated successfully, but these errors were encountered: