10 ms·
> In effect, Clang has noticed the uninitialized variable and chosen not to report the error to the user but instead to pretend i is always initialized above 10
by muldvarp 3y ago
> In effect, Clang has noticed the uninitialized variable and chosen not to report the error to the user but instead to pretend i is always initialized above 10, making the loop disappear.
No. This is what I call the "portable assembler"-understanding of undefined behavior and it is entirely false. Clang does not need to pretend that i is initialized with a value larger than 10, there is no requirement in the C standard that undefined behavior has to be explainable by looking at it like a portable assembler. Clang is free to produce whatever output it wants because the behavior of that code is literally _undefined_.
Also, compilers don't reason about code the same way humans do. They apply a large number of small transformations, each of these transformations is very reasonable and it is their combination that results in "absurd" optimization results.
I agree that undefined behavior is a silly concept but that's the fault of the standard, not of compilers. Also, several projects existed that aimed to fully define undefined behavior and produce a compiler for this fully defined C, none of them successful.
- weinzierl 3y agoExactly, and in addition to that, making the loop disappear is not only good for performance but from a compiler perspective much easier than producing a helpful error message that explains the situation well.
- petergeoghegan 3y ago> No. This is what I call the "portable assembler"-understanding of undefined behavior and it is entirely false. "C has been characterized (both admiringly and invidiously) as a portable assembly language" - Dennis Ritchie The idea of C as a portable assembler is not without its problems, to be sure -- it is an oxymoron at worst, and a squishy idea at best. But the tendency of compiler people to refuse to take the idea seriously, even for a second, just seems odd. The Linux kernel's memory-barriers.txt famously starts out by saying: "Some doubts may be resolved by referring to the formal memory consistency model and related documentation at tools/memory-model/. Nevertheless, even this memory model should be viewed as the collective opinion of its maintainers rather than as an infallible oracle." Isn't that consistent with the general idea of a portable assembler? > I agree that undefined behavior is a silly concept but that's the fault of the standard, not of compilers. The people that work on compilers have significant overlap with the people that work on the standard. They certainly seem to share the same culture.
- qsort 3y ago> But the tendency of compiler people to refuse to take the idea seriously, even for a second, just seems odd. It's not taken seriously because it shouldn't be taken seriously. It's a profoundly ignorant idea that's entirely delusional about reality. Architectures differ in ways that are much more profound than how parameters go on the stack or what arguments instructions take. As a matter of fact the C standard bends over backwards in the attempt of not specifying a memory model. Any language that takes itself seriously is defined in terms of its abstract machine. The only alternative is the Perl way: "the interpreter is the specification", and I don't see how that's any better.
- petergeoghegan 3y ago> It's not taken seriously because it shouldn't be taken seriously I really don't know what you're arguing against. I never questioned the general usefulness of an abstract machine. I merely pointed out that a large amount of important C code exists that is in tension with the idea that of an all important abstract machine. This is an empirical fact. Is it not? You are free to interpret this body of C code as "not true ISO C", I suppose. Kind of like how the C standard is free to remove integer overflow checks in the presence of undefined behavior.
- mpweiher 3y agoThat's an interesting opinion. But it has very little to do with the C programming language.
- nickelpro 3y ago> As a matter of fact the C standard bends over backwards in the attempt of not specifying a memory model. I mean, C explicitly specifies a memory model and has since C11
- grotorea 3y agoI wonder what's the best solution here then. A different language that actually is portable assembly, or has less undefined behaviour or simpler semantics (e.g RIIR), or making -O0 behave as portable assembly?
- badsectoracula 3y ago> I agree that undefined behavior is a silly concept but that's the fault of the standard, not of compilers. While undefined behavior is the standard's fault, its interpretation is up to the compiler.
- jonas21 3y agoThat's why the author prefaced the sentence with "In effect". The effect in this case is the same as if the compiler had pretended i was initialized to a high value - the loop is omitted.
- plorkyeran 3y agoIt's still fundamentally a misunderstanding of what the compiler is doing, and thinking about it that way is just going out of your way to cause confusion. The compiler saw a use of an uninitialized value, concluded that it must not be reachable as that's the only way for it to be a legal C program, and then deleted the dead code. You can make up all sorts of other behaviors that the compiler could have done which would have had the same result in a specific scenario, but why would you?
- stanleydrew 3y agoI don't think the author misunderstands what the compiler is doing, just saying that it's probably unexpected behavior from the perspective of the program's author.
- afiori 3y agoI don't think that the compiler approach is try and fix the code > it must not be reachable as that's the only way for it to be a legal C program, and then deleted the dead code. Rather it would make more sense if the approach was more like "this branch is UB so I can whatever is most convenient in terms of optimization" in this case it was merging the 2 branches but discarding the code for the UB branch. But from a behavioral point of view all these formulations describe the same result
- plorkyeran 3y agoThe compiler isn't trying to "fix" anything and I'm not sure where that idea came from. The core concept is that the compiler assumes the program is valid and optimizes based on that assumption. If UB would occur if certain values are passed as an argument to a function, the compiler assumes that those values won't be passed. If UB occurs unconditionally in a function, the compiler assumes that the function won't be called.
- mpweiher 3y ago1. The people creating the C standard were adamant that just following the standard was not sufficient to produce a "fit-for-purpose" compiler. This was intentional. 2. They were also adamant that being a "portable assembler" with predictable, machine-level semantics was an explicit goal of the standard. 3. The C standard actually does have text giving a list of acceptable behaviours for a compiler and "silently remove the code" is not in that list. And this text used to be normative, but was later made non-normative. So I blame the people who messed with the standard, and guess who those people were?
- pwdisswordfishc 3y agoCitation needed. Which people? What did they actually say? What was the text that supposedly forbade this interpretation of UB? Please don't tell me this is again that tired wankery over "permissible" versus "possible". As if the choice of synonym mattered.
- 3836293648 3y agoIt's a rather infamous change between C89 and C99 where the description of UB was changed from basically don't do this to please do this and compilers can do whatever they want if you do
- _kst_ 3y agoThe definition of "undefined behavior" did not change in the way you describe between C89/C90 and C99. In both editions, one possible consequence of undefined behavior is "ignoring the situation completely with unpredictable results" -- i.e., compilers can do whatever they want. There is no "don't do this" or "please do this" in either edition. Both merely describe the possible consequences if you do. C90: undefined behavior: Behavior, upon use of a nonportable or erroneous program construct, of erroneous data, or of indeterminately-valued objects, for which the Standard imposes no requirements. Permissible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message). If a "shall" or "shall not" requirement that appears outside of a constraint is violated, the behavior is undefined. Undefined behavior is otherwise indicated in this Standard by the words "undefined behavior" or by the omission of any explicit definition of behavior. There is no difference in emphasis among these three; they all describe "behavior that is undefined." C99: undefined behavior behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this International Standard imposes no requirements NOTE Possible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message). EXAMPLE An example of undefined behavior is the behavior on integer overflow. (Some of the wording in the C90 definition was moved to the Conformance section in C99.)
- spookie 3y ago> Also, compilers don't reason about code the same way humans do. They apply a large number of small transformations, each of these transformations is very reasonable and it is their combination that results in "absurd" optimization results. So true. It baffles me how sometimes I hear colleagues of mine just "assuming" that the compiler will deal with something. Something that does require a really high level reasoning to come true. It's absurd.
- somenameforme 3y agoIMO it all comes down to the, 'Premature optimization is the root of all evil.' That saying was, at its best, not great. But it seems at some point down the road the 'premature' part of that sentence was lost to history and it became something much worse. It goes some way towards explaining why we now need machines that would be considered supercomputers, just a couple of decades prior, to run a text editor. Unreal Engine is a beautiful example of this. The source code of that engine is absolutely full of 'premature optimizations.' For instance repeatedly converting a Quaternion to a Rotator (Euler angles) is basically never going to get red-lined on a profiler, let alone be a bottleneck. Yet there's a caching class used in the engine for this exact purpose. It's all of these little 'premature optimizations' that end up creating a system that runs vastly better than most any other engine out there, feature for feature.
- mike_hock 3y agoIt's a dogma pendulum like "gotos are evil," "YAGNI," object-oriented design (is good/bad), etc. A pattern or anti-pattern gets identified and lessons drawn from it. Then people take the lesson as dogma and drive it to absurd extremes. Naturally this doesn't have the desired effect, and then the baby gets thrown out with the bathwater, people do the exact opposite and take that to absurd extremes. Caching potentially expensive (relatively speaking) operations isn't "premature optimization," it's a design principle. It's much harder to retrofit something like this into a project after the fact than having and using it from the start. "Premature optimization" is applying complicated micro-optimizations early on that won't survive any moderate code change.
- 3y ago
- User23 3y ago> Clang is free to produce whatever output it wants because the behavior of that code is literally _undefined_ The standard says no such thing. It lists a number of acceptable ways to handle undefined behavior and none of them are do anything you like.
- mrighele 3y ago> Clang is free to produce whatever output it wants because the behavior of that code is literally _undefined_. > They apply a large number of small transformations, each of these transformations is very reasonable One of those transformation detects that we are reading uninitialized memory and acts accordingly. Given that most of the case where we do it is by mistake, I think that doing anything other than raising an error (or at least a big warning) is not a very reasonable thing to do. For those case where such behavior is desired, a compiler flag could be provided. The fact that the standard allows the current behavior, doesn't mean that the compiler should do it.
- muldvarp 3y ago> One of those transformation detects that we are reading uninitialized memory and acts accordingly. This might be pedantic but variables do not have to correspond to memory locations. The compiler could decide to keep the variable in a register or the compiler might realize that the variable never changes and apply constant folding eliminating it entirely. Assuming that variables correspond to memory locations (or really any assumptions about how code is translated into assembly) is a part of the "portable assembler"-understanding of C that simply doesn't apply to (optimizing) C compilers. > Given that most of the case where we do it is by mistake, I think that doing anything other than raising an error (or at least a big warning) is not a very reasonable thing to do. I agree that the compiler should warn if it can prove that a programs behavior is undefined unconditionally. I don't think it's as easy as you make it out to be, but I encourage you to check out the source code of clang and fix this issue if that's possible. That said, in practice undefined behavior is often encountered conditionally (e.g. the behavior of this function is undefined if and only if the first argument is non-null and the second argument is null). Optimizing such programs is trivial (simply assume that undefined behavior cannot happen and optimize accordingly), detecting that the program actually calls that function with invalid parameters on the other hand is _literally_ impossible and even a best-effort approach would be computationally expensive and miss a lot of cases.
- overgard 3y ago> No. This is what I call the "portable assembler"-understanding of undefined behavior and it is entirely false. Clang does not need to pretend that i is initialized with a value larger than 10, there is no requirement in the C standard that undefined behavior has to be explainable by looking at it like a portable assembler. Clang is free to produce whatever output it wants because the behavior of that code is literally _undefined_. You're kind of lawyering this. Sure, it's "undefined", but is that useful to anyone outside of compiler writers? How useful is it to have a program that's very fast but entirely wrong? If the behavior is undefined, I want an error, not a free license for the compiler to do whatever the hell it wants.
- Vvector 3y agoDoesn't -Wall give you the error you want?
- robinsonb5 3y agoNot always - in some cases you need -Wextra or even -Weverything to get a warning. [Edit: was thinking of GCC here, and yesterday's similar thread - clang may be more forthcoming with warnings, I don't know.]
- muldvarp 3y ago> How useful is it to have a program that's very fast but entirely wrong? This only affects you if your program has undefined behavior, at that point your program was wrong anyway, you were just lucky that the compiler compiled it into something that works. Taking a different compiler or a different target architecture and you might not be lucky. So even the old "portable assembly" kind of undefined behavior is actually the opposite of portable. > Sure, it's "undefined", but is that useful to anyone outside of compiler writers? It may surprise you that compiler writers are not evil people that take joy in the suffering of their users. They implement these aggressive optimizations because they actually produce better results for valid programs. It just happens that this coincides with more "absurd" results if the program has undefined behavior. But at that point your program was wrong anyway. If you absolutely want "portable assembler"-semantics for your C program you can do that by using (very) old C compilers. Your program will not get optimized as aggressively, but "portable assembler"-semantics and optimizations are inconsistent with each other anyway.
- anon946 3y agoIs all UB silly? E.g., wouldn't fully defining what happens when one goes beyond the end of an array impose a non-trivial performance hit for at least some code?
- torstenvl 3y agoYes. But there's middle ground between fully-defined behavior (lots of slow checks) and what current compiler-writers think UB is (do whatever I want). Specifically, implement UB the way it is described in the standard: pretend it isn't UB, do it anyway, consequences be damned. That's what "ignore the situation with unpredictable results" actually means.
- muldvarp 3y ago> compiler-writers think UB is The current standard is _very_ explicit that undefined behavior is indeed undefined, i.e. "do whatever you want". > pretend it isn't UB, do it anyway, consequences be damned. This explicitly isn't a requirement, but even if it were, "ignoring the situation completely with unpredictable results" can be interpreted in numerous ways. One of these ways is "ignoring any cases in which UB is encountered" which is exactly what compilers are doing. Then again, saying "the compiler didn't ignore the situation and as a consequence I got results I didn't predict" isn't a strong argument when the standard specifically told you that you will get unpredictable results.
- torstenvl 3y ago"The Party told you to reject the evidence of your eyes and ears. It was their final, most essential command." There's a certain poetic consistency in ignoring arbitrary portions of the standard to justify ignoring arbitrary portions of the input code.
- muldvarp 3y agoWhich part of the standard is ignored? Again, the standard is _very_ explicit about what undefined behavior means. If you don't like that you can either try to change the standard or use the numerous command line options provided by most compilers to tell your compiler that you would like certain undefined behaviors have a defined meaning. Saying that compilers shouldn't ignore code with undefined behavior is like saying compilers shouldn't ignore the body of an if-statement just because the condition evaluated to false.
- grumpyprole 3y ago> Also, compilers don't reason about code the same way humans do. Not these compilers for sure. But I don't agree that all compilers are broken. > They apply a large number of small transformations, each of these transformations is very reasonable and it is their combination that results in "absurd" optimization results. Humans use techniques like natural deduction to apply a series of transformations that do not lead to absurd results.
- muldvarp 3y ago> Humans use techniques like natural deduction to apply a series of transformations that do not lead to absurd results. All men are immortal. Socrates is a man. Therefore, Socrates is immortal. If you were to give a group of CS students the exercise to determine whether the this deduction is valid, they would all answer yes. So no, humans that apply natural deduction can also derive absurd results. That said, I don't think all C programmers would agree that the result of that optimization is absurd in the first place.