-
Notifications
You must be signed in to change notification settings - Fork 17.9k
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
proposal: cmd/vet: warning on some cases using declared loop variables of 3-clause for-loops #66156
Comments
There is a strong case for the copylock analyzer to warn on this. Example from https://go.dev/design/60078-loopvar#language-specification
is equivalent to:
A copy clearly happens at
copylock reports on range statements already. (This covers the possible 'exactly 0 iterations is known' objection [not a compelling objection. just a possible one]).
This is not a correctness issue and is outside of cmd/vet's criteria (README). Note this is issue applies almost equally well to range statements, which vet does not warn on.
It is sufficiently subtle I am not sure what the example is supposed to illustrate. Is it that the semantics are not identical? |
https://go.dev/cl/569955 has a proposed solution for the copying a loop variable case. |
For range statements, if there is a copy cost, then it is inevitable and compiler is helpless to avoid the cost. So this is a new kind of problem introduced by the new semantics of 3-clause for-loops.
Do you mean the code is correct? |
In fact, the 2nd point is not only performance degradation related, it is often also logic correctness related. |
I agree that the example linked from "A similar case" is indeed a problem. Committing to a new tool in the toolchain is a fairly heavy hammer. Both for the go team and the community. It would be nice to try to find a more natural home for performance issues first. @dominikh This looks like a plausible candidate for a staticcheck Performance issues check. (Good luck guessing the minimum size to start reporting! 1kb?)
I had not read the blog post so I did not have a great grasp on what you were trying to illustrate. Yes I would agree that that is a non-obvious data race. My suggestion for this would be to basically have a loopclosure style vet check to look for loop variable writes that race with the implicit loop read like this.
To keep the discussion productive, do you have a non-crafted example? Crafted examples have been known for a while and so far they are not compelling in the way a real correctness issue would be. |
So you agree that the 1st and 3rd points should be vetted, right?
Maybe you are right. Logic correctness problems often relate to loopvar capturing. But I still don't think it is a good idea to let 3rd party tools to detect such problems. |
I agree on vetting the 1st point, and I have proposed an implementation. For the 3rd point, we still need to decide on the criteria vet would use to detect such issues. In the previous loopclosure range case, the loop did a write to the loop variables each iteration so any read or write of the variable would race. This is subtly different as loop is doing an implicit read and we need to find a write to the variable. So we need a narrower criteria to detect issues in the closure. Once someone proposes solid criteria (low false positive, low false negative, intuitive enough, fast enough), I would be in favoring of adding this to vet. |
Does vet warn on any performance issues? I thought it was for correctness foot-guns. |
I'm not convinced this is a problem that needs to be solved. To address the original points:
It's true that one could construct a loop in which the only copy of the nocopy value is the original initialization, but why would anyone do that? I have never seen anyone initialize a WaitGroup that way and I would certainly flag it during review as bad style.
That's true of any assignment. Why is this one special? (And as @dr2chase points out, that's not vet's concern anyway.)
Can you give an example? If you're copying the value once in |
The syntax allows it, which means the possibility is there. In my honest opinion, the rarer it happens, the more dangerous it is.
The specialty is well explained in the blog article. It may cause silent performance degradation. This is a new kind of problem created by the new semantics.
The first comment gives such an example. |
That's the opposite from the ranking used to evaluate vet checks. According to the vet documentation, "the criteria applied when selecting which checks to add are: ... Frequency: ... A new check that finds only a handful of problems across all existing programs, even if the problem is significant, is not worth adding to the suite everyone runs daily."
It's not an asymptotic change in performance, just a single additional copy, so it's not a correctness issue, and thus not vet's concern. A single extra copy is also well beneath the threshold of what we usually consider to be a performance-affecting change in library code.
I meant an example from merged code, not one constructed to trigger the analyzer. |
I don't appreciate that attitude of facing the problem. Developing a language is a serious thing. If you want a case from some Go projects. just wait. I believe there will some reported later. BTW, why do you think the example is not real. Will you think it is also constructed when a case is reported later? :D
I fully acknowledged that the performance problem doesn't fit vet's criteria. The problem is a way is need to handle the new problem caused by the new semantics.
I don't have any words. Okay, just let some rare old code in some private projects be the victim. |
Let me try to summarize my understanding of the whole: Case 1. In a loop of this form: for x := f(); ...; ... {
// x' := x
use(&x) // implicitly refers to x'
// x = x'
} the compiler makes a copy (x') of x at the start of each loop and writes back its value at the end. The reference to x in the loop body is actually to x'. So, if the loop body depends on the identity of x being the same for each iteration, the language change will have introduced a bug. You are correct that there is a theoretical possibility of programs such as your example having the potential for new bugs. But in practice it never happens: during the compiler team's analysis, there was exactly one instance in all of the hundreds of thousands of loops in Google's Go code base where the semantic change introduced a regression. But in a large fraction of cases, the change fixed a latent bug. (In other words, it's much, much more common to implicitly assume that the identity of the variable is different for each iteration--which it now is.) That's why I was asking for evidence from a real program. I agree with @timothy-king that we should extend the copylocks check to detect no-copy types in the "init" clause. Case 2 concerns the additional cost of the implicit loop variable copies, In any case, it's not a vet problem. Case 3 seems to be a concurrent variant of case 1. The loop body implicitly assumes that there is only one variable i, and that is no longer the case. The same argument as in case 1 applies: the language change is overwhelmingly more likely to fix a data race in a loop than to introduce one. (Also, these errors may be detected by the So, I'm not saying these kinds of problems are impossible, just that they are vanishingly rare, and that alone means vet is not the appropriate tool to detect them. |
So you only scanned the code in Google, and got the conclusion that "in practice it never happens"? My example is real, certainly, in my opinion. :) I don't know why you think it is unreal.
If looks you haven't understood the problem well of case 2. Let's explain it again. The compiler may or may not succeed to make optimizations to avoid loop variable copies, depending on how the code is written. When you change the code a bit, the performance regression may appear or disappear. And the performance regression degree may be serious or not easy to detect. When it is not so serious, it might be not detected in time.
All you said is just your willing. Now, it is not the time to make conclusion. We need to wait, maybe for several years. The problem of case 3 is that, the case is valid before Go 1.22, but become invalid since Go 1.22. What I mean here is that people may still think the code is valid in Go 1.22 semantics. And depending on specific code, the effects of the invalid code might be exposed in time or not. This is what the subtlety is. |
IMO what case 3 is really is an instance of a
FWIW I am not that nervous about examples that transition from happened to have no races to having races. I am actually more nervous about patterns like the following:
The user likely heard/remembers 'each loop gets its own copy of the variable', slightly skims the details of how this works, decides on this basis to assign to
I am not sure if this will end up too rare to justify the frequency requirement or not. 3-clause for loop races definitely seem rarer than range races. And the WRITE requirement will make this less frequent still. The linked list thought experiment above does seem plausible enough to me that I'd say this is at least borderline to support in loopclosure. |
The compiler team scanned all the Go corpora hosted by Google, which means not only Google's proprietary code, but also the much larger set of modules in the Go Module Mirror, which to a first approximation is all the world's open-source Go code.
Thanks, I understand the problem, but I come back to the question of whether this theoretical possibility of a slowdown is ever actually noticeable in programs that weren't designed to exhibit the problem.
Yes, this language change has the potential to introduce bugs in bug-free code. But this issue is about whether vet can and should attempt to detect them, and that decision should be based on evidence that this is a real problem. You are right that we may not have such evidence for a while.
I don't understand the problem in the example. The statement annotated as a "problematic write" is not a write, it's only a read of |
Sorry, there is a large quantity of private Go code in the world. And again, not all patterns are analyzed. In fact, what I mean is that the manner to prove something by scanning all existing code in the world is totally wrong. If you don't want programmer to write some code you don't like, just forbid it from the syntax level. But the current reality is, when some people write the code you don't like but the syntax allows, then you just say it is constructed. It is ridiculous to me and an ugly objective fact, it makes Go less rigorous (the trend just happened since recent Go versions). Being less and less rigorous, I don't know what are the consequences for Go.
Is it lucky or unlucky if it is not noticeable? The problem here is that such a performance degradation problem may appear/disappear in an unexpected way, depending on whether the compiler optimization works or not. Objectively, this raise the bar to be a qualified Go programmer.
Again, being less and less rigorous, I don't know what are the consequences for Go.
The problem is, prior to Go 1.22, when there is a problem, it is explicit to users, but since Go 1.22, it is implicit so that people tend to not admit it is a problem. Up to now, the proven benefits of the semantic change on And I haven't seen any efforts from Go official to notify Go programmers on the potential damage of the change. What I saw are all to try to hide the potential problems instead. Let time tell whether or not the change is worth it. |
It sounds like you are frustrated with the fact that the language made a backward-incompatible change in go1.22, despite the Go project's longtime commitment to not breaking existing bug-free programs. In this case there were good reasons for it; they were carefully evidenced and publicly debated, and you can easily look them up. But this is not the place to re-litigate them. As I said several times already: if and when there is evidence of a real problem, we will investigate making changes to vet to help mitigate them.
Exactly. |
@go101 We understand that you do not like the loop variable change. You expressed strong opposition before the change was made. You continue to express strong opposition. We heard you. We considered your arguments. And we made a decision. We are not going to revisit that decision at this time. I encourage you to let this matter rest. Thank you. |
@ianlancetaylor
I'm sick of disputing on the rational of the change now. |
@timothy-king has split the two main ideas into separate issues. I'm going to close this one. |
Proposal Details
Go 1.22 changed the semantics of 3-clause for-loops. The loop variables declared in 3-clause for-loops will be (implicitly) copied once at the start of each iteration. So
So it would be great to let
go vet
warn on such cases.Here, some examples for such cases are listed below (from this blog article).
Another one: #66070
Similar cases: a serious one and a less serious one.
The 3rd is actually a continuation of #16520
The text was updated successfully, but these errors were encountered: