-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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
bytes, strings: add Clone #45038
Comments
If accepted,
|
Would this be a better fit for the likely generic slices package, so it could work on any |
There is already a generic way to do it in one line.
This approach is even faster because the allocated slice will not be initialized to zero values. |
@ulikunitz I think that's an argument for adding a |
For simple cases like in the example code at the thread start (no complex computation in length argument) make + copy should be faster than append since go1.15: #26252 (comment) as the go compiler avoids the zero value intialization there too. |
I think that even with b2 := append([]byte(nil), b...)
// vs.
b2 := slices.Clone(b) |
@DeedleFake |
While that works, I think it's definitely confusing, especially if somebody else owns That said, does this proposal also require |
@ericlagergren strings are immutable, there's no need to copy them. |
There is need to copy them. For example: s := "big long string"
x := s[:3]
// no more uses of s, but x is kept
// alive for a long time, keeping the
// entirety of s in memory until x is
// collected because the GC does not
// collect partial objects. This has implications for things like string interning. |
In Go programming, there are many cases in which a small piece of a memory block is active prevents the whole memory block being released, not limited to slices and strings. |
See #25834. |
The append trick works well, but you have to have read the Go slices tricks wiki page to know to do it. If generics add a slices package, it would make sense to have |
This proposal has been added to the active column of the proposals project |
It would be nice to get a sense of how widely used this idiom is. As it stands, a Clone() function saves almost no lines of code, and it's a new API which contributes to the system complexity of the STL. I think this can only be worthwhile if it is extremely widely used. |
IMHO, a builtin |
Whatever we add here will need to be in strings too (forcing a copy, to address #40200) for symmetry. Clone seems OK. Any objections to Clone? |
That being said, I don't think that it has to exist just because |
I think that has benefit in itself. I occasionally see |
|
That's fair. I just used |
So there's no need to copy strings in that case. You probably meant cases where strings are built at runtime? |
@creker yes. Sorry, I should have been more exacting. |
Based on the reactions to last week's comment, retitled to include bytes.Clone and strings.Clone, as in:
and
|
I dont think we need more CLs for bytes.Clone. One has existed since Oct 2021: https://go-review.googlesource.com/c/go/+/359675 but was not approved for merge in go1.18. If approvable it can now. |
The two PR differs at that one returns nil for an empty slice, the other returns an empty slice instead. |
Both Clone functions in CL 392194 and CL359675 when given a nil slice as input return a nil slice as ouput. Which is aligned with the behaviour of String.Clone. Update: They also return nil slices for empty slices. |
Ah, yes, their behaviors are the same.
There is not nil strings, so this should be a slice specific detail. Personally, I recommend to return an empty slice for an empty slice. |
There is no nil string but there is a stringheader with length and backing array pointer of 0 which can not be compared differently as string but is different as struct header value from a string with length 0 and backing array pointer that is not nil.
What should the backing array point of that empty slice be? If its the same as input it is not really a clone. If its a different one each time it requires an allocation. When only returning nil for nil we only have that special case for nil. I like the consistency with strings. If length is zero then nil struct header is returned both for strings and bytes clone. |
Here's an example of the nil behaviour being confusing: package main
import (
"encoding/json"
"fmt"
)
func Clone(b []byte) []byte {
if len(b) == 0 {
return nil
}
c := make([]byte, len(b))
copy(c, b)
return c
}
func main() {
b := []byte{}
data, _ := json.Marshal(b)
fmt.Println("Original:", string(data))
data, _ = json.Marshal(Clone(b))
fmt.Println("Cloned: ", string(data))
} https://go.dev/play/p/v-1dAVqDoGt
cap(make([]byte, 0)) // is 0 No backing array is allocated. |
Make with 0 uses mallocgc which uses runtime.zerobase as backing array. Which makes the property of not reusing the backing array when cloning not true anymore even for non 0 backing array pointers.
I guess this generally in the direction of APIs differing for nil and empty slices which is generally discouraged in code bases I work on. I guess it cant be changed for json anymore and it is how it is. |
I think the questions about nil and whether bytes.Clone should use the same cap, cap == len, or cap == size class point in the direction of just doing nothing. Cloning a string is tricky because you might think you did it correctly, but then the something about the runtime changes in Go N+1 and it isn't copied anymore. Having strings.Clone ensures that you won't get bitten after Go changes. bytes.Clone is just a convenient way to do something you could already do, but at the cost of being potentially misleading if the user has an incorrect expectation about the behavior. |
Why does this matter? |
Because thats what Clone in this proposal is supposed to be in contrast to a copy according to the earlier comment and discussion: #45038 (comment)
The reason to have a new backing array is that even a zero len and cap slice with a pointer into a memory allocation keeps that allocation from being garbage collected by the Go runtime. If that pointer is from a former large slice it may keep a huge amount of memory from being reused. To avoid those references users can use strings.Clone and bytes.Clone to make sure there isnt more memory used by the slice then needed and the old memory is not reused. So we want the new slice to have a different backing array, but we also want to avoid unnecessary allocations. Nil is already a language concept with a special name and the null pointer at any rate will exist often in a program. There is usually also nothing new allocated or written to what it points to. So its easy to carve out the one exception in documentation and behaviour to when the new slice will not have a new backing array and that is when passing in a nil slice. |
Now that slices.Clone exists I propose to not implement bytes.Clone at all. The original requests intention of wanting a simple helper to create a copy of a slice has been met by slices.Clone. The slices.Clone seems to be a slice copy with the added property that the new cap can be larger/smaller than the old cap depending on the length of the input. But gives no explicit guarantees of not reusing memory. e.g. a 0 len, 0 cap slice might just be returned as is. So we seem to have 3 options:
|
@martisch yes, I understand what |
Because Clone would not have the property of returning a new backing array for slices with zerobase as it is also returns the slice with zerobase again. So it would return the identical slice, not a new one. That means the documentation needs to clarify that excemption and document that this isnt not met when one passes in special slices with a backing array pointing to zerobase. But that is an implementation detail of the current runtime and it will be hard to comprehend for users what that means. It also means the runtime can not change in that regard without changing the semantics of the Clone function. Instead giving the property that the excemption is only for nil slices is runtime independent and its easier to comprehend what slices this applies to as nilness is defined in the language and can be tested. Rather than exposing a function with semantics that need to document runtime internals such as runtime.zerobase I rather not expose such a function at all. |
The slices.Clone docs don't mention any of that. // Clone returns a copy of the slice.
// The elements are copied using assignment, so this is a shallow clone.
func Clone[S ~[]E, E any](s S) S {
// Preserve nil in case it matters.
if s == nil {
return nil
}
return append(S([]E{}), s...)
} |
It seems like we should still implement bytes.Clone, for parallelism with strings.Clone. It's also worth noting that slices.Clone is in x/exp, not the standard library. It also seems fine to me to have |
Because slices.Clone has different semantics than discussed here and does not guarantee a new backing array is returned. It also can return a different cap. I updated https://go-review.googlesource.com/c/go/+/359675 but only mentioned for len and cap != 0 a new backing array is returned as for other slices it might be the same as I do not see how we can describe without mentioning and pinning runtime internals (which we likely dont want to expose or forever keep the same) for which ones. |
@martisch do you plan on continuing work on https://go-review.googlesource.com/c/go/+/359675 ? |
This comment was marked as abuse.
This comment was marked as abuse.
This comment was marked as resolved.
This comment was marked as resolved.
The new Clone function returns a copy of b[:len(b)] for the input byte slice b. The result may have additional unused capacity. Clone(nil) returns nil. Fixes golang#45038 Change-Id: I0469a202d77a7b491f1341c08915d07ddd1f0300 Reviewed-on: https://go-review.googlesource.com/c/go/+/359675 Run-TryBot: Martin Möhrmann <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Joseph Tsai <[email protected]> Reviewed-by: Martin Möhrmann <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> (cherry picked from commit 7b45edb)
Working directly with
[]byte
is very common and needing to copy it comes up fairly often. AClone
helper would be nice to have.What did you expect to see?
What did you see instead?
Implementation
The text was updated successfully, but these errors were encountered: