-
Notifications
You must be signed in to change notification settings - Fork 18.1k
cmd/compile: counter-intuitive comparison of zero-sized pointers wrapped in interfaces #65878
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
Hm, on second thought, the spec allows this, strictly speaking:
I thought the exception was just that two zero-sized variables are allowed to have the same address, but if there is an exception for the pointer-comparison itself as well, that means a Go implementation would technically be allowed to return the result of a coin-flip, whenever such pointers are compared and literally any behavior here is correct. TBQH, that still seems a pretty confusing situation. I would personally vote to at least in |
@Merovius I don't think that part of the spec can be applied here. If
Both printlns must print true, or false. It should not be the case that one prints true, and the other false. The case here is: |
Funny enough printing the pointers, make it return true. a, b := new(struct{}), new(struct{})
x, y := any(a), any(b)
fmt.Printf("%p %p\n", a, b)
fmt.Println(a == b, x == y, x.(*struct{}) == y.(*struct{})) // true true true Also: a, b := new(struct{}), new(struct{})
x, y := any(a), any(b)
runtime.KeepAlive(&a)
fmt.Println(a == b, x == y, x.(*struct{}) == y.(*struct{})) // true true false a, b := new(struct{}), new(struct{})
x, y := any(a), any(b)
runtime.KeepAlive(&a)
runtime.KeepAlive(&b)
fmt.Println(a == b, x == y, x.(*struct{}) == y.(*struct{})) // true true true a, b := new(struct{}), new(struct{})
x, y := any(a), any(b)
fmt.Println(uintptr(unsafe.Pointer(a))) // 824634001135
fmt.Println(uintptr(unsafe.Pointer(b))) // 824634232559
fmt.Println(a == b, x == y, x.(*struct{}) == y.(*struct{})) // false true false |
That is not something the spec guarantees, no.
It's certainly confusing, but it is working as specified. There is nothing in the spec saying "a comparison of two values must always return the same thing". FTR, that's why I made the distinction above. There are two places where pointers to zero-sized values are special-cased:
Your intuition would be right, if only the first was the case: In that case, every variable has a (consistent) address and the pointer-comparison would always have to compare those addresses. But with the second exception, the comparison can return literally anything. There is no qualification on that "may or may not". |
@bserdar To be clear, according to that spec sentence, it would be correct for And note that this is not the only case where |
The current implementation seems to return var escapes *struct{}
func main() {
a, b := new(struct{}), new(struct{})
x, y := any(a), any(b)
escapes = a
escapes = b
fmt.Println(a == b, x == y, x.(*struct{}) == y.(*struct{})) // true true true
}
|
@Merovius I see the point about NaN, but note that the behavior of NaN is consistent. My point here is that a program should not evaluate a==b differently for each comparison instance. If a==b and nothing modifies them, then another comparison of a==b should return the same. What we are observing here is that a!=b, but any(a)==any(b). |
@bserdar If you want to argue that the program behaves incorrectly, you have to argue from the spec, not from your expectation. You say that comparisons "should" behave a certain way - but if the spec does not say that comparisons behave that way, the program behaves correctly. Again, to be clear: I agree that it's confusing and that it should be fixed. But the program behaves in accordance with the language specification. |
I don't think that's the case. The spec talks about pointers to distinct zero-size variables. When comparing This also means that if you do something like this: s := struct{}{}
a, b := &s, &s
|
@liennie fair enough. You are right. |
A bug introduced in Go 1.9. package main
type T struct {}
func main() {
var a, b = &T{}, &T{}
var x, y interface{} = a, b
println(a == b, x == y)
}
|
The behavior of Go 1.9 and 1.10 is more weird. package main
type T struct {}
func main() {
var a, b = &T{}, &T{}
if (a != b) { println(0) }
if (a == b) { println(1) }
println(a == b || a != b)
}
|
FWIW this is the issue where the spec phrasing was decided. It was pre Go 1.0. And Russ' comment is pretty clear, that there are no guarantees made whatsoever. So, I disagree strongly with the level of certainty in the statement "this is a bug". It's counter-intuitive, but it is working as specified and it was intended that there are no guarantees made. I believe any program relying on pretty much any behavior for comparison to zero-sized pointers (except To me, the only question is if we want to be nice and provide more predictable behavior than the spec defines. But even then, we should be aware that after a conversion to |
The title of the issue is not correct. This problem is interface unrelated. package main
import "unsafe"
type T struct {
_ struct{ x [0]func() }
}
func main() {
var a, b = &T{}, &T{}
println(uintptr(unsafe.Pointer(a)) == uintptr(unsafe.Pointer(b))) // true
println(a == b) // false
} |
@go101 as the person who filed the proposal, I reserve the right to decide what it's about (though if someone with more intimate knowledge about the root cause wants to rename it, they can, of course). In particular, the [edit] then again, it is actually incorrect in a different way [/edit] |
Comparisons of zero-sized pointers wrapped in interfaces is not counter-intuitive, comparisons of zero-sized pointers is. |
New one: package main
var a, b [0]int
var p, q = &a, &b
func main() {
println(&a == &b, p == q) // false true
} Totally interface unrelated. [edit]: package main
var a, b [0]int
var p, q = &a, &b
func main() {
if (p == q) {
p, q = &a, &b
println(p == q) // false
}
} package main
var a, b [0]int
var p, q = &a, &b
func main() {
if p == q {
x, y := &a, &b
if x == p && y == q {
println(x == y) // false
}
}
} |
I suspect most of the confusion here comes from this optimization: given two distinct variables in the program, the compiler is allowed to assume their addresses are different.
Even though, when For example:
This program prints I'm of the opinion that there's nothing to do here. The compiler is adhering to the spec. No one should be depending on equality of pointers to zero-sized variables. There are other weird cases for zero-sized variables having to do with escaping, allocation, stack slots, etc. Here are two fun examples:
When run, it prints The second weird example:
When run, it prints |
The spec has no ambiguities on whether or not the comparison results should be consistent between different builds/runs. But it is not clear enough on the point within the same run. |
@randall77 I agree that the compiler adheres to the spec. My argument is that "always evaluate comparisons of pointers to zero-sized variables to I don't have strong opinions either way. But I am kind of curious why that's not what's done right now. If nothing else, it seems it would save a few instructions and CPU cycles here and there. |
Is the optimization specifically made for pointers to zero-size values? |
@go101 What makes it needless in your opinion? Would you expect And note that zero-sized types still have behavior. For example, putting a Once you accept that some zero-sized variables need to be allowed to have the same address, it seems to me a natural consequence that you can no longer really meaningfully specify the result of comparing their pointers. I don't think it is practical to remove the flexibility the spec provides. Which is why, if anything, we should talk about what the implementation can do to make this less confusing. The advantage of arguing that |
You completely misinterpreted what I said. All you said in the last comment is unrelated to what I said. |
I think you mean |
If you declare that two pointers to zero-sized things should always compare equal, then you're going to have weird cases in other ways. Like:
Will then print It also means a bunch of special cases in the compiler, reflect, etc. that have to deal with this new special case. |
Yes. Sorry, it is a mistake. Fixed now. Is your last comment replied to me? I don't declare that two pointers to zero-sized things should always compare equal. |
No, that's in response to @Merovius' suggestion of making all such comparisons return |
I think that pointers are not the issue. What can be confusing is when non nil distinct pointers are assigned to interface values. https://go.dev/play/p/zMky3K0tS1l I believe that's what @Merovius was initially pointing at in my understanding and what actually @go101 tries to hint at as well. Namely, if each interface holds:
Then the interface value comparison should also return false? Or is there a special optimization here? (If however the interface value being created is simply another fat pointer to the same value that was pointed at, it could be understood that it might be optimized to reuse its own, previously created, internal pointer value, and such interface values comparisons would always return true) Other than that I think that the behavior for 0 sized types and regular references is not surprising and the spec does well to not force equality. Basically, the bit on interface comparisons is actually explained in @randall77 comment A few more comparisons: |
This issue is not related to interfaces in general: #65878 (comment) |
@atdiar, I think you are overthinking it. An interface is, conceptually, just a pair of a type and a value of that type. That type need not be a pointer type, and the value need not be a pointer. In the implementation of interfaces sometimes there is an additional indirection, but semantically that indirection is undetectable (unless you play unsafe shenanigans). |
Yes, I think I am getting confused, a little. Let me step back: If I assign a pointer value to an interface variable, the interface should hold a value that is a pointer value plus a pointer type, right? Or does it hold a pointer type and the value (the implementation may use a pointer value set to point to the same variable). That's just me being confused. I think it's the second option as you explained. In which case there is indeed nothing to do here. https://go.dev/play/p/LXQ0FvJlIxM That's why Essentially, assigning a pointer to an interface just creates a fatter pointer. |
The current Again, how interface is implemented is unrelated to this issue. |
@go101 sorry my mistake. It allocates a new pointer value. Not a new value. Will fix it. Edit: so yes, completing your example, that seems to be another issue. This is strange but that's another question (added some comparisons to your code example) : https://go.dev/play/p/beMBI3V54Bi |
Likewise, if it can prove that two pointers contain the same address, it is allowed to optimize the comparison to TBH I still think that while, yes, there will always be weird cases, this is still a particularly weird one and IMO worthy addressing one way or another. But I don't really have good arguments for why it is important. It's not like programs can start relying on any behavior. And I don't have any benchmarks to claim that there is a performance benefit. So this is really at best about minimizing the number of people we have to explain the corner-case of zero-sized variables to. And it does seem that it would require special-casing this in some way (if only by re-ordering passes or teaching the compiler to know about the same-address-optimization earlier). So… I guess I'm inclined to close this. I don't really see how anyone's mind would be changed. |
I'd tend to think yes. As soon as a type has a size, it should force the addresses to be different. (unless unsafe things are done)
I don't know. Seems to me that since we don't know whether the addresses will or will not be the same, they have been declared different in a first time, to mirror any other non zero-sized values. |
The compiler knows if the things it points to are zero-sized or not. It just currently doesn't care - it performs the same logic either way (addresses of distinct variables are not equal, addresses of the same variable are equal). It is true that if they are different zero-sized variables, the compiler doesn't necessarily yet know whether their runtime addresses will be equal or not. Some of that layout code runs pretty late. So it would be nontrivial (but maybe not that hard?) to have the constant-folding always agree with any runtime comparison. |
This gets reported about once a year; e.g., #23440, #47950, #52772, etc. I agree it's counter-intuitive, but I don't see any action to take within cmd/compile here, so I'm going to close this issue. Perhaps cmd/vet should warn about taking the address of zero-sized variables and/or comparing pointers to zero-sized types. If folks are concerned about this issue, I'd recommend looking into whether either of those checks would meet the threshold for inclusion in cmd/vet. |
This issue does provide new information which were not presented in the old issues. |
Change https://go.dev/cl/594515 mentions this issue: |
Provide a reference to help clear up periodic confusion: https://groups.google.com/g/golang-nuts/c/MXcOqW-Mf-c/m/2-24iQXSAwAJ https://groups.google.com/g/golang-nuts/c/JBVqWYFdtC4/m/EqZxT9EYAQAJ For golang/go#2620 For golang/go#11925 For golang/go#23440 For golang/go#47950 For golang/go#52772 For golang/go#58483 For golang/go#65878 Change-Id: I0986b35d02f2fe88d5a08af1f7d1f50510c39d7c Reviewed-on: https://go-review.googlesource.com/c/website/+/594515 Reviewed-by: Rob Pike <[email protected]> Auto-Submit: Ian Lance Taylor <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> Reviewed-by: Robert Griesemer <[email protected]> Commit-Queue: Ian Lance Taylor <[email protected]>
Go version
go version go1.22.0 linux/amd64
Output of
go env
in your module/workspace:What did you do?
Playground:
What did you see happen?
false true false
What did you expect to see?
Either
false false false
ortrue true true
.The spec defines comparison for interfaces as:
Now, from this definition, the observed behavior should clearly be impossible. The dynamic values are not equal - either before or after wrapping them into
any
. However, the interfaces compare as equal.I suspect there is an optimization going on, where the comparison function stored in the rtype short-circuits for pointers to zero-sized values, based on the permission for all zero-sized values to have the same address. But that is only correct, if the compiler does the same short-circuiting for
==
, in my opinion.Originally mentioned on golang-nuts.
The text was updated successfully, but these errors were encountered: