33 ms·
C and C++ prioritize performance over correctness
- philosopher1234 3y ago>For example, a common thing programmers expect is that you can test for signed integer overflow by checking whether the result is less than one of the operands, as in this program: #include <stdio.h> int f(int x) { if(x+100 < x) printf("overflow\n"); return x+100; } >Clang optimizes away the if statement. The justification is that since signed integer overflow is undefined behavior, the compiler can assume it never happens, so x+100 must never be less than x. Ironically, this program would correctly detect overflow on both ones'-complement and two's-complement machines if the compiler would actually emit the check. My god…
- boryas 3y ago-fwrapv and it's all good :)
- not2b 3y agoIt really isn't optional as a C developer to turn on warnings and get your code warning-free. This will eliminate the most common unbounded behavior issues, like uninitialized variables, though unfortunately not all of them.
- gavinhoward 3y agoThis is why I implemented two's-complement with unsigned types, that don't have UB on overflow.
- kragen 3y agolike x > 2147483547u in this case?
- gavinhoward 3y agoCorrect. It will still overflow, but it's not UB and won't be optimized away.
- kragen 3y agoby 'overflow' do you mean 'set the high bit'
- gavinhoward 3y agoCorrect. I have routines that take the unsigned values and interpret them as signed, two's-complement values. So you would write this: val = temp1 + temp2; if (y_ssize_lt(val, temp1)) y_trap(); That `y_ssize_lt()` function computes whether `val` is less than `temp1` as though they were signed, two's-complement integers. But because they are actually unsigned, the compiler cannot be malicious and delete that code.
- kragen 3y agoi see, thanks maybe we should develop a nonmalicious compiler
- gavinhoward 3y agohttps://groups.google.com/g/boring-crypto/c/48qa1kWignU/m/o8GGp2K1DAAJ https://groups.google.com/g/boring-crypto/c/48qa1kWignU/m/o8...
- pclmulqdq 3y agoThis is why people who put "C/C++" on their resume don't usually know either language. In C++, integers are defined to be two's complement now, so that check is acceptable.
- rsc 3y agoNot true, as the post explains.
- pclmulqdq 3y agoC++20 made the switch. See P1236. It appears C23 has followed suit.
- e4m2 3y agoIt made the switch, sure, but overflow remains undefined.
- rsc 3y agoIf P1236 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1236r1.html https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p12...) is really what it says ("alternative wording") then I don't believe that's true. Certainly P0907R3 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0907r3.html https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p09...) is clear: > Status-quo If a signed operation would naturally produce a value that is not within the range of the result type, the behavior is undefined. The author had hoped to make this well-defined as wrapping (the operations produce the same value bits as for the corresponding unsigned type), but WG21 had strong resistance against this. My understanding is that the 2s complement change in C++20 ended up defining bitwise operations but not arithmetic.
- uecker 3y agoTwo's complement does not mean signed overflow became defined. So the check is still wrong. And no, I do not think making it defined would lead to more correct programs. You then simply have really difficult wrap-around bugs instead of signed overflow which you can be found more easily with UB sanitizer or turn into traps at run-time.
- tempodox 3y agoThis has to be the worst part of UB: That the compiler can assume it never happens. This way UB can affect your program even if the code that would exhibit the behavior is never executed.
- AlotOfReading 3y agoWhat the article misses is that the code wouldn't work on all hardware. Take MIPS for example, where the signed overflow would generate a hardware exception that the OS might have implemented to do anything from nothing to killing the generating process. C was never standardized solely on the basis of what's most performant. The vast majority of explicit UB is there because someone knew of a system or situation where that assumption wasn't true.
- lelanthran 3y ago> C was never standardized solely on the basis of what's most performant. The vast majority of explicit UB is there because someone knew of a system or situation where that assumption wasn't true. So? They could have called it implementation defined. The reasoning you present was broken then, as it is broken now. Your MIPS example displays no reason for UB to exist.
- AlotOfReading 3y agoImplementation defined means it has a defined behavior on every implementation. In the MIPS case defining that behavior would force the compiler to generate strictly signed instructions for signed values and define how the runtime platform handles these interrupts, rather than leaving the compiler free to generate "whatever works". Look, a lot of UB is frankly stupid and shouldn't be in a modern language spec, including signed overflow. I'm not defending that, only giving an example where you have to break compatibility to eliminate it.
- lelanthran 3y ago> In the MIPS case defining that behavior would force the compiler to generate strictly signed instructions for signed values and define how the runtime platform handles these interrupts, I respectfully disagree: in this particular scenario, it's enough for the compiler vendor to write "generates an interrupt on overflow" with no indication of what the handling should be, and still be well within implementation-defined behaviour. After all, the standard specifies what `raise()` does, and what `signal()` does, but doesn't specify how the runtime is going to handle `raise(signum)` when the program has not yet called `signal(signum, fptr)`. This scenario you presented displays exactly why UB should be replaced with IB in the standard. I've yet to see one good reason (including performance reasons) for why the dereferencing of a NULL pointer (for example) cannot be documented for the implementation. With UB, we have real-world examples of a NULL pointer dereference causing compilers to omit code resulting in a program that continued to run but with security implications. If changed to IB, the compiler would be forced to emit that code and let the dereference crash the program (much better).
- lelanthran 3y agoI think what really annoys me is that this looks like actual malice on the part of the standards writers, and less severe malice on the part of the compiler authors. I know I should attribute it to stupidity instead but ... The standards writers could have made all UB implementation defined, but they didn't. The compiler authors could have made all Uab implementation defined, and they didn't. Take uninitialised memory as an example, or integer overflow: The standard could have said "will have a result as documented by the implementation". They didn't. The implementation can choose a specific behaviour such as "overflow results depend on the underlying representation" or "uninitialised values will evaluate to an unpredictable result." But nooooo... The standard keeps adding more instances of UB in each revision, and the compiler authors refuse to take a stand on what overflow or uninitialised values should result in. Changing the wording so that all UB is now implementation defined does not affect blegacy code at all, except by forcing the implementation to document the behaviour and preventing a compiler from simply optimising out code.
- nottorp 3y ago> less severe malice on the part of the compiler authors. This is optimization run amok and I'd call it malice. If C/C++ are designed to let me shoot myself in the foot, the compiler should let me shoot myself in the foot instead of erasing my code.
- torstenvl 3y agoThe standard does make all definable UB implementation defined. The compiler writers intentionally misread the standard to allow these kinds of optimizations. C17 Draft, § 3.4.3: "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)." Compiler writers like to language-lawyer this in two ways. First, they interpret "possible" to mean that this is a non-exhaustive list, but that isn't how interpretation of such a document typically works. Expressio unius est exclusio alterius -- the express inclusion of one is the exclusion of others -- means that any list is exhaustive unless explicitly indicated otherwise. In other words, the reality is that, according to the standard, these are the only possible options. Second, they interpret "ignoring the situation completely" to mean "actively seeking out the situation and deleting whole sections of code based on that." This is quite self-evidently a dishonest interpretation.
- pdw 3y agoThis is why I hate it when people describe C as "portable assembler". The Usenet comp.lang.c FAQ was already thirty years ago warning people not to write overflow checks like this.
- coliveira 3y agoYou cannot check for overflow like this, because you're causing it. There are other ways to do this without adding first: #include <limits.h> int safe_add(int a, int b) { if (a > 0 && b > INT_MAX - a) { /* deal with overflow... */ } else if (a < 0 && b < INT_MIN - a) { /* deal underflow.. */ } return a + b; }
- zer8k 3y agoI'm not convinced a "common" C program would ever do such a thing. Even the "Effective C" book (one of the best books on C, imo) discusses why this is wrong and why you should take advantage of `limits.h` for bounds checking. This is just bad programming. The compiler, like usual, is correct because you're not in reality checking anything. You've made an obviously non-sensical statement that the overflowed value will be less than the value. Compiler optimizes it away. You can argue the semantics of UB here but this particular UB is borderline a solved problem in any practical sense. To be fair, a static analyzer should be able to catch this.
- _dain_ 3y ago>You've made an obviously non-sensical statement— Therefore it should fail to compile. You can even spin it as a performance enhancement: the whole program can be optimized away!
- layer8 3y agoThe problem is, that’s not how the logical inference in a compiler/optimizer works. It’s very difficult to translate such an optimization back to “this statement has been optimized away”, in the general case.
- _dain_ 3y agoIf it's so difficult to figure out if the consequences of their optimization game are sensible or not, then they shouldn't play it in the first place. Who actually wants it to behave this way, other than compiler engineers competing on microbenchmarks? Who is this language even for?
- layer8 3y agoIt’s a side effect of desirable optimizations for UB-free code. You can’t have both at the same time.
- 3y ago
- raverbashing 3y agoIf anyone tried to pull this kind of language lawyering in my job they would be fired Maybe it's about time to fire the C compiler developers
- hot_gril 3y agoThis is a cool summary of UB, but speaking to the title... Well yeah. Did anyone suggest otherwise?
- philosopher1234 3y agoTo me this article is interesting because * it is thorough, detailed and very thoughtful * rsc’s essays usually end up with a major Go language change, so there’s a good chance this article is the seed of some change to Go and undefined behavior, or correctness or performance. Even if it’s not a major upcoming change (I bet it is, tho) rsc is an extremely insightful dev.
- hot_gril 3y agoYeah, I agree. I was editing my comment to praise the article's body as you were replying.
- rsc 3y agoThere is no major upcoming Go change related to this post. This is just a much longer version of https://research.swtch.com/plmm#ub https://research.swtch.com/plmm#ub that I drafted years ago and finally cleaned up enough to post.
- philosopher1234 3y agoIt’s fine, I’m not sad or anything
- uecker 3y agoIt should be "C and C++ Implementations" because nothing in the standard requires UB to be exploited for optimization instead of adding run-time checks.
- hot_gril 3y agoThe standard also doesn't require UB to have run-time checks, which is probably for the goal of performance. There was an implementation in mind when it was designed.
- deleted 3y ago[deleted]
- not2b 3y agoThe reason for leaving integer overflow undefined was not primarily because of one's complement machines. It was for loop optimization. Consider for (int i = 0; i < ARRAY_WIDTH; i++) out[i] = in[i + offset]; Assuming that i + offset, &out + i, and &in + i + offset do not overflow allows the loop to be cleanly vectorized without checking for wraparound. The compiler developers in the 80s were trying to come up with rules that didn't require C to be 5x slower than Fortran on scientific applications, and dealing with the consequences of a[i] being equivalent to *(&a + i).
- Filligree 3y agoMaybe the idea hadn't been invented back then, but it seems obvious to me that the correct response is an iterator protocol. Or at least a hardcoded for-in syntax.
- dahfizz 3y agoCare to explain? An iterator is a nice high level concept, but the CPU still has to do the &in + i + offset arithmetic. I don't see how replacing `i` with syntactic sugar changes the need to check for overflow.
- nostrademons 3y agoI think the point the GP is making is that with an iterator protocol, the iterator implementation itself is free to make a different choice on implementation strategies, based on the shape of the data and the hardware available, and this is transparent to client code. So for example, a container containing only primitive ints or floats on a machine with a NVidia Hopper GPU might choose to allocate arrays as a multiple of 64, and then iterate by groups of 64, taking advantage of the full warp without needing any overflow checks. Obviously a linked list or an array of strings couldn't do this, but then, they wouldn't want to, and hiding the loop behind an iterator lets the container choose the appropriate loop structure for the format and hardware. I've heard criticisms of C and C++ that they are simultaneously too high-level and too low-level. Too high-level in that the execution model doesn't actually match the sort of massively parallel numeric computations that modern hardware gives, and too low-level that the source code input into the compiler doesn't give enough information about the real structure of the program to make decisions that really matter, like algorithm choice. It's interesting that the most compute-intensive machine learning models are actually implemented in Python, which doesn't even pretend to be low-level. The reason is because the actual computation is done in GPU/TPU-specific assembly, so Python just holds the high-level intent of the model and the optimization occurs on the primitives that the processor actually uses.
- kazinator 3y agoCorrectness can come from the programmer in ways that performance cannot. We usually aim for correctness, and in small programs we often achieve it 100%. 100% maximum performance is rarely achieved in a high level language. If you try hard at absolute correctness, you can get there; not so with performance. So, obviously, that must mean performance is harder than correctness. If you turn a performance aspect of a system into a correctness requirement (e.g. real time domain, whether soft or hard) you have hard work ahead of you. In programming, we often make things easier for ourselves by sacrificing performance. Oh, this will be executed only three times over the entire execution lifetime of the program, so doesn't have to run fast. Thus I can just string together several library functions in a way that is obviously correct, rather than write loops that require proof. That said, if you have certain kinds of correctness requirements, C becomes verbose. What is easy to express in C is an operation on inputs which have already been sanitized (or in any case are assumed to) so that the operation will correctly execute. It's not a priority to make it convenient to do that validating, or to just let the operation execute with inputs and catch the problem. E.g. say that we want to check whether there would be overflow if two numeric operands were multiplied. It gets ugly.
- dralley 3y agoPerformance can absolutely come from the programmer. It's a question of defaults. One can default to correctness and opt-in to performance (in the places where critically needed, often a small portion of the program) much more easily than one can default to performance and opt-in to correctness.
- dahfizz 3y ago> Performance can absolutely come from the programmer. To a point, sure. But your language and tools dictate a upper bound on speed. You are never going to get a trading app written in Python to be faster than your competitor's app written in C.
- deleted 3y ago[deleted]
- 3y ago
- andai 3y agoThis needs a NSFW tag! Good lord...
- dale_glass 3y agoI think DJB at some point expressed the desire for a boring compiler with an absolute minimum of UB, and I concur. I want some sort of flag that disables all this nonsense. * Uninitialized variable? Illegal, code won't compile. * Arithmetic overflow? Two's complement * Null pointer call? Either won't compile, or NULL pointer dereference at runtime worst case. Yeah, I'm aware -fwrapw and the like exist. But it'd be nice to have a -fno-undefined-behavior for the whole set.
- coliveira 3y agoJust turn on all warnings in your C++ compiler, and make warnings the same as error. For example, uninitialized variables are easy to catch as a warning and then turned into an error. More sophisticated compilers can warn about the other issues too.
- UncleMeat 3y agoThis handles a few cases but nowhere near all of them. Null pointer dereference, use-after-free, data races, and much much more are all global properties with no hope of the compiler protecting you.
- coliveira 3y agoWe're only considering UB conditions here, not errors in general that may be impossible to detect. Every UB condition can be detected by the compiler because, after all, the compiler needs to check for UB to generate code. All it takes is for the compiler to generate warning/errors when this occurs. If this is not done by your compiler, ask them to add this feature instead of just complaining about UB in general.
- eatonphil 3y agoMaybe I'm completely misunderstanding things, but this article mentions arithmetic overflow as being a problem in C and C++ but isn't this exactly a problem in Go as well? There's no checked arithmetic in Go, right? I guess the difference between Go and C/C++ is that Go at least won't optimize away the overflow check? But it still doesn't insert it for you either.
- nteon 3y agoIn Go, overflow is not checked, but it is well-defined in the spec including `programs may rely on "wrap around"`: https://go.dev/ref/spec#Integer_overflow https://go.dev/ref/spec#Integer_overflow
- 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.
- grandinj 3y agorsc should really know better, he has been around long enough. C and C++ have as a basic design rationale "as close to machine performance as possible" "don't pay for what you don't use" "don't break backwards compatibility" The last one is what is what is responsible for most of the issues. It would be nice to fix stuff like UB, but the reason that C/C++ is so popular and still alive is precisely because it does care more about compatibility than fixing stuff likethat. Breaking existing code is extremely uncool for long-lived codebases.
- agwa 3y agoUndefined behavior permits backwards compatibility to be broken, because when a compiler adds a new optimization that exploits UB, existing programs can change behavior, breaking code that previously worked. The blog post cites infinite loops as an example. Thus, eliminating UB would improve backwards compatibility.
- layer8 3y agoCompatibility is defined in terms of a contract between program and language implementation. If the program violates the contract by containing UB, then compatibility does not apply. Taking your argument, compilers couldn’t even add any benign optimization without breaking compatibility, because that would change the runtime of the affected code.
- agwa 3y ago> Compatibility is defined in terms of a contract between program and language implementation. If the program violates the contract by containing UB, then compatibility does not apply. Indeed, furthering my point that having UB in a language undermines backwards compatibility. > Taking your argument, compilers couldn’t even add any benign optimization without breaking compatibility, because that would change the runtime of the affected code. That does not follow from what I said.
- layer8 3y ago
- zwieback 3y agoClearly we can have compilers and static analyzers that can catch a lot of what's UB in the standards. To me the only real question is: what should the default be for your compiler? I don't want to be flooded with warnings when I'm working on a small file of handcrafted near-assembly code but I'm willing to change my compiler/tool options away from some more conservative, non-standard default settings.
- eatonphil 3y agoIt isn't super obvious as someone interested in C or C++ what compiler flags and static analyzers I should turn on. I mean (and I'm asking because I'd genuinely like to bookmark someplace) is there a group that keeps an up-to-date list of aggressive flags and static analyzers to use?
- zwieback 3y agoNot that I know of, probably something you'll have to dig into based on your use case. I've used Coverity for static analysis, which is great but pricey. We have a corporate license so no-brainer for me.
- soulbadguy 3y ago-Wall -Werror (in cland and in think on GCC) is a good way to start. In general, clang is great at warning at UB. Also, address-san is a great tool
- yaantc 3y agoFor static analysis I use CodeChecker, it's a wrapper on top of the Clang static analyzer and Clang tidy (linter). Now also supports cppcheck, but I disabled it (too many false alarms). It's free and open source, and I find it useful. Make sure you use it with a version of LLVM/Clang with support of Microsoft z3 enabled (it's the case in Debian stable, so should be OK in most distros). For the flags I would start with "-Wall -Werror", then maybe disable some warnings based on the code base / use. All this assuming a GCC/Clang friendly code base.
- dbremner 3y ago
- pogopop77 3y agoC and C++ are most often used when performance is the highest priority. Undefined behavior is basically the standards committee allowing the compiler developers maximum flexibility to optimize for performance over error checking/handling/reporting. The penalty is that errors can become harder to detect. It appears the author is a Go advocate. I assume they are valuing clearly defined error checking/handling/reporting (the authors definition of correctness) over performance. If that's what you are looking for, consider Go.
- kelnos 3y agoI think this allowance is a mistake. I suspect that there is some huge number of developer hours that have been wasted, and huge amount of money wasted, on cleaning up after security breaches and finding and fixing security issues. I suspect that those numbers dwarf any losses that might have arisen due to reduced developer productivity or reduced performance when using a (hypothetical) C-like language that doesn't allow the compiler to do these sorts of things.
- PaulDavisThe1st 3y agoThe allowance wasn't "tradeoff performance time with developer time". The point of undefined behavior is "if we specify this, we cause problems for some of the compile targets for this language".
- patmorgan23 3y agoYes, but then complier vendors started abusing UB to increase performance and while silently decreasing safety/correctness. If the compiler creates a security bug by optimizing away a bounds check the programmer explicitly put there, that's a problem. https://thephd.dev/c-undefined-behavior-and-the-sledgehammer-guideline https://thephd.dev/c-undefined-behavior-and-the-sledgehammer...
- jerf 3y ago"a (hypothetical) C-like language that doesn't allow the compiler to do these sorts of things." It's not very hypothetical in 2023. There are plenty of languages whose compilers don't do this sort of thing and attain C-like performance. There isn't necessarily a single language that exactly and precisely replaces C right now, but for any given task where you would reach for C or C++ there's a viable choice, and one likely to be better in significant ways. I also feel like this is missed by some people who defend this or that particularly treatment of a particular undefined behavior. Yeah, sure, I concede that given the history of where C is and how it got there and in your particular case it may make sense. But the thing is, my entire point is we shouldn't be here in the first place. I don't care about why your way of tapping a cactus for water is justifiable after all if you consider the full desert context you're living in, I moved out of the desert a long time ago. Stop using C. To a perhaps lesser but still real extent, stop using C++. Whenever you can. They're not the best option for very many tasks anymore, and if we discount "well my codebase is already in that language and in the real world I need to take switching costs into account" they may already not be the best option for anything anymore.
- kelnos 3y agoOn one hand this is sort of "duh" (none of the examples in the article were surprises to me). But I think it's very useful to phrase it this way. So many people seem to shrug off the dangers in using C (C++ of course has its issues, though I'd argue it's easier these days to use C++ correctly), especially for security-critical code. It may be a helpful argument to point out that C & C++ were not designed with the correctness of programs in mind.
- coliveira 3y agoThe issue of UB has everything to do with how compilers implement it. If people are having problems, they should complain to compiler writers. They always have the option of creating slower code that checks for obvious problems like initialized variables. However, if a company/project writes a compiler that is a little slower than the competitor, people with almost always complain that it is a bad compiler. So the result is what you have nowadays: they're always looking for every small opportunity to generate faster code at the expense of safety.
- weinzierl 3y ago"[..] this International Standard imposes no requirements on the behavior of programs that contain undefined behavior. " What you describe fits for "implementation defined behavior". If you want to write code that works with different compilers, a single compiler with every UB defined, gains you nothing. If you don't need that flexibility, you can just use a language without UB in the first place.
- dale_glass 3y agoYou do. Take the EraseAll example Ideally it's a compile time error. Less ideally it jumps to a NULL pointer and immediately crashes. Either means the programmer must fix the code, which also stops being a problem on other compilers.
- weinzierl 3y ago"Ideally it's a compile time error." I agree with that, but the standard does not agree with both of us. My point is that choosing C or C++ makes sense if you see an advantage in programming against an ubiquitous and almost universal standard. If you have the freedom to implement against a particular compiler implementation there is no good argument to not also taking the freedom to choose a different language altogether. I know in the real world this is not so easy. I worked in automotive with their certified C compilers long enough and I wouldn't have had the choice to select another language. Doesn't mean everyone wouldn't have been better off with a language without UB in the first place and I think we are getting there.
- oude 3y agoI guess we are rediscovering the 40 year old wisdom: https://www.dreamsongs.com/RiseOfWorseIsBetter.html https://www.dreamsongs.com/RiseOfWorseIsBetter.html
- beltsazar 3y ago> To some extent, all languages do this: there is almost always a tradeoff between performance and slower, safer implementations. Well, probably except Rust, from which you get both performance and safety. Sometimes it's with a cost of ergonomics, though. Some people, including the Rust's creator himself, would trade performance for better ergonomics in the case of asynchronous programming. They prefer having a non-zero-cost green threads to the current state of async prog in Rust.
- jakobnissen 3y agoNot always all of the performance. Rust encourages array access with superfluous bounds checks, unnecessary UTF8 validation, and uses tagged sum types over the more memory efficient raw union types.
- cmovq 3y agoRust does tradeoff performance for safety. Example: integer division [1]. [1]: https://godbolt.org/z/71sxnfff5 https://godbolt.org/z/71sxnfff5
- tester756 3y ago>The problem is that C++ defines that every side-effect-free loop may be assumed by the compiler to terminate. That is, a loop that does not terminate is therefore undefined behavior. This is purely for compiler optimizations, once again treated as more important than correctness. It is crazy that people decided to do it. It not only weird, but also wrong. There's no such a thing like "side-effect-free" code. There's always side-effect like CPU usage, temperature, mem usage, etc. There is whole class of attacks based on those.
- dragonwriter 3y ago> There's always side-effect like CPU usage, temperature, mem usage, etc. If you are writing a loop in your code with no classic side effects hoping to regulate temperature, that’s probably a bad and unreliable design to start with, whether or not the compiler optimizes it away. If you read a temperature sensor to validate that the loop is doing the right thing and adjust frequency based on the result, its still a little crazy but its less completely insane and no longer free of classix side effects.
- tester756 3y agoAll I'm saying is that code doesn't have to perform stuff like prints, file write, http call in order to generate side effect You can run heavy computation and do not do anything with the result just in order to increase CPU usage, right? (which then should result in temp increase ;))
- tylerhou 3y agoThe C abstract machine isn't a physical machine, so temperature is not a side effect :)
- klodolph 3y ago> There's no such a thing like "side-effect-free" code. This kind of like “Ceci n'est pas une pipe.” In that it’s useful to remind people that there is no such thing as side-effect-free code, strictly speaking, but as soon as you remind people, you go back to talking about whether a particular piece of code has side effects. Someone points at object depicted in the painting and asks what it is, and you say “It’s a pipe.” Basically, to reason about code, it’s useful to ignore certain classes of side effects, most of the time.
- xjay 3y agoC = Control I think in the world of C, the compiler assumes the author knows what they're doing, and historically, you were probably supposed to use a separate tool, a linter [1] (static analysis tool), to help catch mistakes. HN's hard disks were a recent (2022-07) victim of a firmware developer's lack of understanding of integer overflow exceptions. [2] The firmware was likely written in C, or "C++ C". A fix was released in 2020. Another reminder to update the firmware of these disks. [3] ++crash; [1] https://en.wikipedia.org/wiki/Lint_(software) https://en.wikipedia.org/wiki/Lint_(software) [2] https://en.wikipedia.org/wiki/Integer_overflow https://en.wikipedia.org/wiki/Integer_overflow [3] https://www.thestack.technology/ssd-death-bug-40000-hours-sandisk/ https://www.thestack.technology/ssd-death-bug-40000-hours-sa...
- agwa 3y agoUndefined behavior is the opposite of programmer control. As the examples in the blog post show, you can write code that explicitly deferences a NULL pointer, or enters an infinite loop, and the compiler will think surely the programmer didn't mean to do that, and literally remove code that you wrote. It's true that historically C has not done much to protect programmers from their mistakes, but historically mistakes just meant suffering the natural consequences (such as a SIGSEGV on NULL pointer dereference). But these days, when you make a mistake, C compilers will exploit your mistake to the maximum extent possible, including changing the meaning of your programs.
- Rexxar 3y agoIMHO, having an additional debug mode where all optimisations steps are performed but with asserts inserted to validate all preconditions would mitigate a lot of problems. For example adding a "assert(x < MAX_INT - 100)" in first example. Running test suites on this debug binaries would find a lot of problems.
- thesuperbigfrog 3y ago"Performance versus correctness" is the same design tradeoff as "Worse is Better": https://www.dreamsongs.com/RiseOfWorseIsBetter.html https://www.dreamsongs.com/RiseOfWorseIsBetter.html However our tolerance to accept "worse" over "better" is waning since we have more capable hardware, better tools, and "worse" leads to more problems later such as security vulnerabilities.
- deleted 3y ago[deleted]
- _dain_ 3y agoone day, a bug like this will take down an airliner or a power grid and kill thousands of people. and there will be a congressional hearing about it. and some ageing compiler engineers will have to explain to a panel of irate and disbelieving senators the concept of UB. they will have to explain that, while computers can add two numbers correctly, we choose to make them not do that. for performance.
- Joel_Mckay 3y agoProfessional C/C++ coders tend to recognize the history of cross-platform compatibility issues, and tend to gravitate to a simpler subset of the syntax to avoid compiler ambiguity (GNU gcc is ugly, but greatly simplified the process). Try anything fancy, and these languages can punish you for it later on down the life-cycle. However, claiming it lacks correctness shows a level of ignorance. Best of luck, =)
- haberman 3y ago> There are undeniably power users for whom every last bit of performance translates to very large sums of money, and I don’t claim to know how to satisfy them otherwise. That is the key, right there. In the 1970s, C may have been considered a general-purpose programming langauge. Today, given the landscape of languages currently available, C and C++ have a much more niche role. They are appropriate for the "power users" described above, who need every last bit of performance, at the cost of more development effort. When I'm working in C, I'm frequently watching the assembly language output closely, making sure that I'm getting the optimizations I expect. I frequently find missed optimization bugs in compilers. In these scenarios, undefined behavior is a tool that can actually help achieve my goal. The question I'm always asking myself is: what do I have to write in C to get the assembly language output I expect? Here is an example of such a journey: https://blog.reverberate.org/2021/04/21/musttail-efficient-interpreters.html https://blog.reverberate.org/2021/04/21/musttail-efficient-i... I created the https://github.com/protocolbuffers/upb https://github.com/protocolbuffers/upb project a long time ago. It's written in C, and over the years it has gotten to a state where the speed and code size are pretty compelling. Both speed and code size are very important to the use cases where it is being used. It's a relatively small code base also. I think focused, performance-oriented kernels are the area where C makes the most sense.
- ianlevesque 3y agoThanks for giving an actual example of such optimizations. In my personal experience my C++ (and Rust) code was often outperformed by the JVM's optimizations so I've found it hard to relate to the tradeoffs C++ developers assume are obvious to the rest of us.
- grumpyprole 3y ago+1. Part of the problem is that x86 assembly is hardly programming the metal anymore. The performance characteristics of processors has changed over the years also, compilers can be more up-to-date than humans.
- overgard 3y ago
- mannyv 3y agoIf you don't like undefined behavior then avoid it. It's not hard, unless you're not paying attention.
- deadletters 3y agoThere are a lot of warning and error options. Turn the guiderails and sanitizers on during development and testing. Turn the optimizations on when you ship. Check out: -wall -werror https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
- zzo38computer 3y agoI have some of my opinions about how it should do. > Uninitialized variables The compiler should be allowed to do one of: 1. Assume any arbitrary value, as long as it remains consistent until a new value is assigned (which might or might not depend on the previous value). (However, the exact value need not be known at compile-time, and might not even be known at run-time if the program doesn't care about the exact value (in the example if the loop is removed then the exact value doesn't matter at run-time). It might be different every time the program runs.) Removing the loop in the example is valid; it would also be valid to start at a negative number (since you did not specify an unsigned type), but if for example you declare i before the loop instead of inside and then you add printf("%d\n",i); after the loop, then the program is required to print a number which is at least 10 and can fit in the int type; if standard library optimizations are enabled then it would be allowed to e.g. replace it with puts("42"); instead (but if the loop is not removed, then the number printed by this must be 10). 2. Read the value from a register or memory which is then used as the storage for that variable (for whatever time it needs to, just as though it was given a value which was returned from a function with no side-effects), but without initializing it. 3. Do whatever the target instruction set does (which seems very unlikely to me to do anything other than the above). > Arithmetic overflow The C specification should specify two's complement (it is the only good representation of integers anyways). Fortunately GCC has -fwrapv and -ftrapv, and I often use -fwrapv to avoid problem with signed overflow. > Infinite loops There is a rationale with some sense, but maybe better would be nevertheless not optimizing out the loop entirely like that. However, if standard library optimizations are enabled then it should be allowed to replace the loop with: for(;;) pause(); > Null pointer usage In this case, the program should just actually attempt to read, write, or call the null address, which should result in a segfault or other run-time error if possible (instead of being optimized out). However, the compiler is allowed to assume that such a call can never return, if it wishes (whether or not that is actually true on the target computer). In the case of a read or write that is not marked as volatile, the compiler may substitute any value for a read and may optimize out a write, although it does not have to; it may also do what is specified above. However, the optimization shown in the article should be allowed if Do is a local variable which is uninitialized, rather than being initialized as null. (You can disable optimizations (or possibly change some options) if you don't like that.)
- omoikane 3y agoRegarding uninitialized variables, there is a proposal to make them default to zero-initialized: http://wg21.link/P2723 http://wg21.link/P2723 Under "5. Performance" section, it claims the performance impact to be negligible, and there is also a mechanism to opt-out.
- crabbone 3y agoThis is a really, really misguided take on C and C++... They don't prioritize anything. They are just bad languages with a random assortment of features mostly reflecting how people thought about computers in the 70's. Just think about this: vectorization is one of the obvious ways to get better performance in a wide range of situations. Neither C nor C++ have any support for that. Parallelism -- C++ kinda has something... C has zilch. Memory locality anyone? I mean, Common Lisp has a bunch of tools to aid compiler in optimizing code in the language itself, whereas C has like... "inline" and the ability to mess with alignment in structs, and I cannot really think about anything else. Stuff like UB isn't making the language perform better or easier to optimize. It's more of a gimmick that compiler authors for C and C++ found to produce more efficient code. It's a misfeature, or a lack of feature, that allowed for accidental beneficial side-effects. Intentional optimization devices in a language are tools that prove the code to be intentionally correct first, and that proof allows the compiler to elide some checks or to generate a more efficient equivalent code, based on the knowledge of the correctness of the proof (or, at least, upon explicit instructions from the code author to generate "unsafe" code).
- fooker 3y agoWow, don’t get so invested in hating a technology, it’s just a tool.
- kazinator 3y ago> vectorization is one of the obvious ways to get better performance in a wide range of situations. Or, well, in a narrow range of situations where you have certain kinds of hardware with vectorization support. > C has zilch C has operating systems written in it which use parallel processing internally and make parallel processing available to applications. True, that may be done using techniques that are not described in ISO C. Common Lisp has undefined behavior, a familiar example being (rplacd '(1 . 2) 3): clobbering a literal object. Optimizations in Common Lisp are exactly like UB in C: you make declarations which specify that certain things hold true, and in an unsafe mode like (declare (optimize (safety 0) (speed 3))), the compiler blindly trusts your assertions and generates code accordingly. If you asserted that some variable is a fixnum, but it's actually a character string, that character string value's bit pattern will likely be treated as a fixnum. Or other consequences. Common Lisp is nice in that you can control speed and safety tradeoffs on a fine granularity, but you do trade safety to get speed. Common Lisps (not to mention Schemes) have extensions, just like C implementations. In plenty of code you see things like #+allegro (do this) #+sbcl (do that).
- k4st 3y ago> It would certainly not hurt performance to emit a compiler warning about deleting the if statement testing for signed overflow, or about optimizing away the possible null pointer dereference in Do(). I think that the nature of Clang and LLVM makes this kind of reporting non-trivial / non-obvious. Deletions of this form may manifest as a result of multiple independent optimizations being brought to bear, culminating in noticing a trivially always-taken, or never-taken branch. Thus, deletion is the just the last step of a process, and at that step, the original intent of the code (e.g. an overflow check) has been lost to time. Attempts to recover that original intent are likely to fail. LLVM instructions may have source location information, but these are just file:line:column triples, not pointers to AST nodes, and so they lack actionability: you can't reliably go from a source location to an AST node. In general though, LLVM's modular architecture means that you can't assume the presence of an AST in the first place, as the optimization may be executed independent of Clang, e.g. via the `opt` command-line tool. This further implies that the pass may operate on a separate machine than the original LLVM IR was produced, meaning you can't trust that a referenced source file even exists. There's also the DWARF metadata, but that's just another minefield. Perhaps there's a middle ground with `switch`, `select`, or `br` instructions in LLVM operating on `undef` values, though.
- strangescript 3y agoIf only we had a modern language that did both... (which was strangely omitted from this article)
- ngrilly 3y agoMasterful conclusion in the last paragraph!
- renox 3y agoNote that in Zig an unsigned overflow is also an UB in release unchecked mode. Don't like it? Don't use unchecked mode or use wrapping operators..
- metadat 3y agoThis is likely in response to this relevant related submission discussed yesterday: A Guide to Undefined Behavior in C and C++ (2010) https://news.ycombinator.com/item?id=37165042 https://news.ycombinator.com/item?id=37165042 (166 comments) A worthwhile read, IMO. Cheers.
- justincredible 3y ago[dead]
- aatd86 3y ago> C and C++ do not require variables to be initialized on declaration (explicitly or implicitly) like Go and Java. Reading from an uninitialized variable is undefined behavior. Took me some time to understand that part :) I always thought that an unassigned variable of type slice T was deemed uninitialized. (but then, what of assigning nil to a previously non-nil variable? Is it considered deinitialized?) In fact, at the language level it could be considered uninitialized/deinitialized. That's the variable typestate. (cf definite assignment, ineffectual assignment analysis) For the compiler, it still "points" to a memory location so it is initialized. Am I right? (more than 344 comments, no one will find this one question lol :-)
- markhahn 3y agothe basic premise is wrong: it's not a tradeoff, which would require someone to say "doing this correct and expected thing blows up because another correct and expected thing can go faster". the problem is mixing in incorrect or unexpected things (like someone altering a global via a debugger). to some extent, it's fair to complain about archaic nonportabilities (such as whether dereferencing NULL will fault, or long-gone numeric representations) - the standards committees should take a deep breath and DTRT in those cases.
- lenkite 3y agoThe C++ standard library prioritises backward compatibility first and fore-most over performance or correctness. Thats why some elements of the C++ standard library are slower than their counterparts in slow, interpreted languages.