1. 13
    1. 5

      In some Go issue Rob Pike said “I can be really fast if I’m allowed to give a wrong answer.” It’s a good quote.

      1. 4

        I first encountered it in MJD’s advice list (warning for use of language that was unfortunately common in programming forums/groups/lists of that era):

        #11912 Evidently it’s important to you to get the wrong answer as quickly as possible.

        and

        #11963 It’s easy to get the wrong answer in O(1) time.

      2. 2

        In some Go issue Rob Pike said “I can be really fast if I’m allowed to give a wrong answer.” It’s a good quote.

        On the first page of The Practice of Programming, Pike (and Kernighan) wrote, “A fast program that gets the wrong answer doesn’t save any time.” I bet Pike (and Kernighan) have been making variations of this joke for a long, long time.

      3. 1

        I have dug but cannot find the quote. I was hoping one of the LLMs would be able to dig out for me, but they just hallucinate answers that aren’t on the page, even Bard and Bing which are theoretically internet connected.

        1. 5

          Err- yes, that’s what LLMs are designed to do…

          1. 2

            Sure, but one would hope that when multibillion dollar companies put out a product it would do something related to the task at hand. :-/ I guess I’m naive.

            1. 4

              Yes, the product you’re looking for is a search engine.

              1. 1

                Search engines failed me already because I need semantic search, not keyword matching.

                1. 1

                  You mean the technology Google has been beating everyone at since 2005?

                  1. 3

                    Not sure why you’re arguing with me. I used Google and it did not find the quote. Nor did DDG. Presumably if I scraped all the issues and ran some kind of NLP on it (whether LLM based or traditional), I could find it in there somewhere, but that’s a lot of effort for a one off quote hunt.

            2. 2

              Err… no? It’s designed to create hype and so they can say they are “forging the future, not staying in place” but that’s for the advantage of the stockbrokers and investors, not you.

              1. 2

                That’s what :-/ means.

        2. 1

          I don’t see it on the Go Proverbs list either, although this was a good excuse to revisit it for fun.

    2. 4

      The C++ committee could take inspiration from Rust: An alternative to defining signed overflow is to define functions for doing arithmetic with wraparound.

      Ditto for unsigned, as it expresses intent. Situation: Clang’s UB sanitizer actually complains about unsigned overflow. Which I’m sure catches bugs, but is really an incorrect thing to do, and worse, removes the only way to do intentional wraparound portably – you have to use compiler specific builtins now.

      1. 5

        In practice in all the c++ code I’ve worked on, unsigned over / underflow has always, always been a bug outside of hash functions & similar, and this specific sanitization (-fsanitize=integer) has saved my ass a lot of times. It’s very uncommon to want N-1 to be a few billions when N=0 and causes a lot of issues down the line.

      2. 5

        They’ve made dereference of std::optional unchecked and UB when it’s empty.

        In C++ there’s no will to change the priorities from safety third.

        1. 3

          This is, unfortunately, largely unavoidable, because C++ doesn’t have anything like flow-sensitive typing. Consider the following:

          if (someOptional)
             doSomething(*someOptional);
          else
             doSomethingElse(*someOptional)
          

          If you had flow-sensitive typing, then you could make this a compile-time error: in the first branch, the type of someOptional is a valid optional, in the second branch it is an optional in an known state, and the dereference operator is defined only on the former. Within the C++ type system, you can’t express that.

          The other choice is to have the dereference operator (and value() method) throw an exception or abort if called with an invalid object. This can hurt performance on correct code, because you are relying on the compiler being able to inline the dereference method and CSE the check (which may or may not actually be possible depending on other aliasing properties of the code). Note that UB doesn’t mean ‘must explode’, a standard library implementation is free to insert a check and a branch to abort() here.

          In Rust, the type system gives enough flow sensitivity (modulo some soundness bugs that were still open a couple of years ago, not sure if they’re fixed now) that you can make this form a compile failure and it provides enough aliasing guarantees that, even if you made it a dynamic check, the optimiser has enough information to know that it’s safe to elide the second check (this is one of the reasons why you can’t circumvent the borrow checker in unsafe code: it will make these optimisations unsound and introduce incorrect behaviour in distant bits of code).

          In C++, the work-around is to use value_or or the monadic operations introduced in C++23. The latter tend to perform well because the compiler’s inlining heuristic will always inline a lambda if it is called in a single place (it is never better to outline code that is used only once, at least until code generation where you may want to move cold paths to a different page). I generally consider using the dereference operator or value() to be a sign that the surrounding code needs careful auditing in code review.

          1. 5

            Well yeah, the exception way would have had a performance impact. So they decided not to pursue it, because performance is the top priority in their language design process. Then comes palatability for compiler implementers and then maybe it’s safety third.

            And they make even more bewildering decisions: When they added std::span, they not only made the regular index operator unchecked on pain of UB, they discussed and then dismissed the idea of also offering a checked .at() method, even though it would have been easy to add and consistent with other types like std::vector or std::map. They’re not even giving users the choice between performance or safety. It’s full YOLO or nothing.

          2. 2

            Isn’t there something that could approximate Rusty if let Some?

            if (auto unwrapped = optional.unwrap()) { 
                doSomething(unwrapped);
            }
            
            1. 4

              The problem with that approach is that it relies on implicit conversion to Boolean. A lot of C++ types (including all primitive types) support Boolean decay and so you can’t just use that directly, because a present value may decay to false. The optional would have to return something that was, effectively, the optional itself: something that wraps the target and a validity thing and uses the validity bit for Boolean conversions and uses the dereference operator to return the value. And now you’re back where you started because it is UB to call the dereference operator on the new thing and there’s nothing other than programming style to ensure that you use it only in code of this structure where you check it first. Worse, this thing now takes a reference to the underlying optional and so makes it easier to introduce memory safety bugs by having it outlive the optional.

              This kind of structure is precisely what the dereference operator on optional was designed to support:

              if (auto optional = somethingReturningOptional())
              {
                  doSomething(*optional);
              }
              

              Your listing tools can (and do) check that you only use the dereference operator in places where it’s dominated by the check, but you can’t express that in the C++ type system.

            2. 2

              An ‘optional’ is a special case of a sum type (optional a is a+unit), which can in general be realised in a language like c++ by church-encoding them; I’m assuming this is basically what the ‘monadic operations’ do.

      3. 3

        Situation: Clang’s UB sanitizer actually complains about unsigned overflow.

        It doesn’t unless you tell it to. The OP in that question had enabled more than just the default “undefined” sanitizer, and it was another sanitizer option that was reporting the unsigned overflow. From the accepted answer:

        In -fsanitize=address,undefined,nullability,implicit-integer-truncation,implicit-integer-arithmetic-value-change,implicit-conversion,integer all except address and undefined enable UBsan checks which are not flagging actual undefined behavior, but conditions that may be unintentional in many cases

        i.e. “-fsanitize=undefined” won’t report unsigned overflow.

    3. 5

      Ugh. Firstly, no, C and C++ don’t prioritize performance over correctness, since turning undefined behaviour into a defined trap (for example) doesn’t make a program correct, it just makes the observable behaviour of the (incorrect) program different.

      Secondly:

      If you compile this with clang++ -O1, it deletes the loop entirely: main contains only the return 0. 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.

      This is such an awful mischaracterisation of the type that often comes from the “C(++) compilers are evil, why can’t they just do what I want” camp. Clang “notices the uninitialized variable” only in the sense that it notices that the usage in the loop must be invalid (the variable hasn’t been defined at that point) and so, yes, it erases the loop; no, it doesn’t “pretend i is always initialized above 10”, in fact it doesn’t even consider the entry condition to the loop, since the optimisation pass is looking for code paths it can eliminate and the presence of definite UB on a particular path is good enough for that. Yes, it assumes that the loop body will never execute and logically this would mean, in the absence of UB, that i > 10 at entry to the loop but of course UB isn’t absent and so that logic isn’t right (and the compiler isn’t using it, despite the claim that it is). Clang doesn’t report the error because it doesn’t know there is an error; even though it’s apparent to us, it would require additional analysis steps for the compiler. It’s not as smart as the author pretends (or believes) it is.

      I’ve said it before, but here we go again: compilers make use of the presence of UB to remove code because compilers are expected to optimise, optimisation is hard, and removing code is an easy win that’s sound in the absence of UB. It’s not about breaking the code; the code was broken to begin with. It might be reasonable to debate the precise allowances that the language does or should give to compilers in these situations, but there’s no excuse for attributing to compilers the kind of malice that this writing effectively does.

      1. 2

        Clang doesn’t report the error because it doesn’t know there is an error; even though it’s apparent to us, it would require additional analysis steps for the compiler.

        Yes, because it does precisely only the amount of analysis required for translation itself and performance optimization. It could do more analysis in order to warn the user of a correctness problem in their code, but this is not prioritized.

        1. 2

          That’s a choice in the design/behaviour of the compiler, though, not the language; anyway the diagnosis is always going to require further analysis than the optimisation, so it’s not a question of prioritisation even on the part of the compiler.

          1. 4

            Right, the language just chose to allow a lot of freedom in this on part of the compiler, which the compilers all chose to spend almost exclusively on performance. But the design of the language is driven for the most part by compiler vendors, who know exactly which carveouts they require for their performance goals. So the distinction between language design and compiler design is quite blurry now.

            Another recent example is Nvidia deciding that no, there is no guarantee that all threads eventually get scheduled, changing the entire deal around forward progress guarantees and throwing around their weight in the committee to make them declare in C++17 that your spinlock-based mutex implementation was actually Wrong All Along. Because this was the only way to keep their performance while being able to declare CUDA C++ “fully C++17 compliant”.

            Or even more recently, when the major vendors discovered that their compilation scheme of sequentially consistent atomics for POWER CPUs was not compliant with the rules from the standard. They did not fix their compilers to be compliant, because that would’ve cost them performance. Instead they worked in the committee to change the memory model, weaking its guarantees and breaking a bunch of previously correct code.

            There are no separate language design and compiler implementation processes. The vendors do what they must to achieve their performance goals, then the standard finds a way to make that legal. The overall system, whether by design or by happenstance, achieves performance at the cost of most other aspects.

            1. 2

              Right, the language just chose to allow a lot of freedom in this on part of the compiler, which the compilers all chose to spend almost exclusively on performance

              That’s not really true; witness the extensive sanitizers available with current GCC/LLVM, the options such as “-ftrapv”, and a plethora of other options specifically chosen to inhibit optimisation and/or improve diagnostics in the interest of program correctness or at least in restraining the effects of UB. If anything I’m seeing more and more effort being put into the latter and less and less on attempts to improve performance, in compilers, but even in the language spec: in C++ integers are now guaranteed to be 2’s complement, for example (even if overflow is still UB, this is a limitation on the leeway that compilers have, not an increase in it); the “unreachable” annotation introduced in C23 is specifically designed to allow compilers to be able to better distinguish between unintended and intended UB.

              As to the other claims you’ve made, I can’t say I know anything about them (this is the first I’ve heard of either case), but they sound more like “prioritising ability of vendors to claim standards compliance” than they do specifically about prioritising optimisation per se. Granted, this is arguably a finer line. In any case, going back to my original point, compilers are_n’t_ (edit) being malevolent when they employ these optimisations; they’re not knowingly making programs misbehave, as the article suggested.

      2. 2

        This, this, so much this.

        It’s always slightly surreal to me when people complain that their C compiler is generating buggy programs when they are compiling buggy C and passing in -O7 -fno-crapv --dont-help-me-i-am-an-expert.

        Your compiler will prioritize performance over helping you if that is what you tell it to do.

        I hear these arguments ad nauseam and they invariably arise from user error. That plus the multiple technical errors in the submission make me reluctant to even engage with the point it wants to make.

    4. 2

      This is why you should always, always compile with -Wall -Werror, and always, always build your debug/test binaries with address and UB sanitizers.

      Actually I’ve recently escalated to -Wpedantic, with good results. In Clang at least, it hasn’t been too anoying.

      1. 3

        The following example was a recent cold shower for me wrt effectiveness of warnings:

        $ cat main.cpp
        #include <cstddef>
        
        int main() {
          for (std::size_t i = 10; i > 0; --i) {
          }
        }
        
        $ clang++ --std=c++20 -Wall -Wextra -Wpedantic -Weverything main.cpp
        
        $
        
        1. 3

          Hmm I went to look up Hipp’s advice on compiler warnings, and it appears to have softened - https://www.sqlite.org/faq.html#q17

          I believe that FAQ used to say that sqlite doesn’t compile without warnings because static analysis doesn’t find bugs – dynamic testing finds bugs.

          I think he also said that fixing compiler warnings can introduce bugs.

          Now it doesn’t say that, and it looks like they do fix all the warnings they encounter. They used to not do that, because he didn’t value the warnings.

          But it still emphasizes testing and coverage.


          I share the dynamic analysis philosophy, but I have to admit that the C++ compiler caught some good bugs in Oils. The warnings are better than they were 20 years ago, for sure. (But C++ is still full of footguns so you have to test – really test everything. What helps is not writing very much C++ :-) )

        2. 3

          .. which warning are you expecting here though? this loops just iterates from 10 to 1 then stops at i == 0. Problems happen if you have i >= 0, but here clang warns:

          main.cpp:5:30: warning: result of comparison of unsigned expression >= 0 is always true [-Wtautological-unsigned-zero-compare]
            for (std::size_t i = 10; i >= 0; --i) {
          

          and for more complicated case, you can use -fsanitize=undefined which will detect unsigned over / underflow:

          int main(int argc, char** argv) {
            for (std::size_t i = 10; i > 0; i -= (std::size_t)argc) {
            }
          }
          
          $ clang++ -fsanitize=undefined -fsanitize=integer main.cpp
          $ ./a.out a b c
          main.cpp:5:37: runtime error: unsigned integer overflow: 2 - 4 cannot be represented in type 'unsigned long'
          SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior main.cpp:5:37
          

          and then you learn to never ever use unsigned / size_t for anything that does not explicitly require modulo arithmetic such as a hash function.

          1. 3

            Of course you are right! Apparently, I can’t be trusted even with writing buggy code.

            What I wanted to say is that all,extra,pedantic is not enough to get a warning for >= 0 case, you need -Weverything for that (or to explicitly enable this specific warning).

        3. 1

          Here’s what I use for my personal C++20 projects.

          • -Weverything
          • -Wno-c++98-compat
          • -Wno-c++98-compat-pedantic
          • -Wno-padded
          • -Wno-poison-system-directories
          • -Wno-pre-c++20-compat-pedantic
    5. 1

      A programmer would never accidentally cause a program to execute an infinite loop, would they? Consider this program:

      #include <stdio.h>
      
      int stop = 1;
      
      void maybeStop() {
          if(stop)
              for(;;);
      }
      
      int main() {
          printf("hello, ");
          maybeStop();
          printf("world\n");
      }`
      

      This seems like a completely reasonable program to write. Perhaps you are debugging and want the program to stop so you can attach a debugger. Changing the initializer for stop to 0 lets the program run to completion. But it turns out that, at least with the latest Clang, the program runs to completion anyway: the call to maybeStop is optimized away entirely, even when stop is 1.

      Isn’t this exactly the sort of thing that the volatile keyword was made for?

      1. 2

        Isn’t this exactly the sort of thing that the volatile keyword was made for?

        No, there are two problems here:

        • It is undefined behaviour to have an infinite loop (that doesn’t do I/O or a few other things) in C. This is really important because a lot of optimisations are unsound if they can’t guarantee forward progress.
        • The compiler can see the entire program from main and so knows that nothing reachable will ever set this value.

        The second is actually less important because the compiler is free to turn the loop into an undefined instruction and then subsequently remove a branch to that condition. Even if stop is written to from, for example, a signal handler, the compiler is free to remove the branch to it.

        1. 1

          Respectfully, this (the argument in this comment as well as the other one, both being representative of general sentiments from c++) is incoherent, and I have no sympathy whatsoever for the attendant concerns.

          The compiler has no licence to muck about in my code willy-nilly; if it would like to optimise, it had damn well better proven its optimisations were sound. It’s not as if termination is impossible for a compiler to reason about; I gave a basic treatment here (I’m not sure that’s the best approach for a c++ compiler—I designed it for my apl compiler, which, in particular, does not reason about unstructured control flow—but you get the idea). To instead make the language unsound is absurd.

          If you want an effective tool for shooting your foot off very quickly, assembly is right there. Unlike c++, it has semantics. I am not being glib; I write tight inner loops in assembly preferentially to c.

          If you want fast code (esp. under the ‘death-by-a-thousand-cuts’ model) without shooting your foot off, the solution is to take a step back from the $millions spent on brute-forcing llvm performance and instead design a language designed to be optimisable and a programming environment designed to be able to optimise it. For example, the lack of very-granular incremental compilation and re-optimisation means that optimisations are limited can be done in one shot without using inordinate computational resources. In turn, the lack of such incrementality derives, in part, from the desire to have reproducible binary artifacts (e.g. the thinlto paper discusses this), in part for trust-related reasons. Dispensing with such binary artifacts would avoid this problem entirely. It also seems not unlikely to me that sufficiently sophisticated(tm) whole-program analysis and optimisation of c++ programs would result in the compiler’s exploiting latent unsoundness that was previously opaque to it (this feeling being driven partly by anecdotal reports that this is already somewhat of a problem with lto), and hence that that would not even be desirable to c++ programmers.

          1. 4

            Please come up with a model where any optimisations are provably sound if you cannot guarantee that function calls make forward progress and then we can talk. Most higher level languages that I have worked on have made the same choice as C/C++ here. You can provide constructs that guarantee termination (C++ has a lot of these), but you can’t guarantee that an arbitrary function does not contain an infringe loop without severely crippling the expressiveness of a language. If you want to permit separate compilation (including incremental compilation, shared libraries, or plugins) then you cannot have features that require whole-program analysis. If you have separate compilation, you must also be able to handle cases where analysis is able to span multiple source-level boundaries.

            1. 1

              I’m a little bit disappointed, because I feel like you haven’t really engaged with the substance of my comment. (Ok, I opened somewhat abrasively; mea culpa on that.)

              First, an aside: I don’t understand why you think this assumption of termination is so important, and ask you the opposite question: what optimisations does it enable? I can only think of two: eliminating side-effect-free, potentially-nonterminating terms with no data dependents, and reordering side-effect-free, potentially-nonterminating terms with respect to side effects. If you make an opaque call, it does not matter if the callee is guaranteed to terminate or not, because you have to assume that the callee might take the far more drastic measure of performing a side effect (and in fact, the callee could legally fail to terminate in c++, instead performing infinitely many side effects, not that that changes anything). I’m also curious what languages you’ve used that behave the same as c++ in this respect, because I don’t know of any save julia (which, rather, lacks semantics entirely, inheriting c++’s by accident via llvm); some counterexamples are common lisp, java, and rust.

              Moving on. I explicitly advocated an incremental whole-program optimisation without separate compilation, and moving away from the opaque binary-artifact shared libraries we know. ‘Plugins’ can be implemented by invoking the compiler at runtime; incrementality means that this will not be unacceptably slow. This is effectively the model used by web browsers, except that the browser itself is written in c++, and even the highest jit tier cannot, for example, inline a c++ routine into a javascript routine.

              1. 3

                First, an aside: I don’t understand why you think this assumption of termination is so important, and ask you the opposite question: what optimisations does it enable?

                Most optimisations involving code motion rely on being able to assume that intermediate function calls will return. Optimisations are unsound when they move code from reachable to unreachable blocks or vice versa. If you permit a function to (especially nondeterministically) never return then the an axioms that these things all rely on become untrue. It’s such an inbuilt assumption in practically all optimisation that it’s never stated, which is why languages make it UB to infinite loop without side effects.

                Moving on. I explicitly advocated an incremental whole-program optimisation without separate compilation

                That’s not really a thing. Optimisation is analysis plus transformation. If you are doing whole program analysis (making infinite loops well defined requires this, and possibly solving the halting problem, depending on how you constrain your source language) then making it incremental is possible only in constrained cases. In particular, anything that allows taking function pointers or some equivalent requires whole program data flow analysis (a sufficiently strong type system can constrain this to a degree) to work out what dependencies have changed. You can easily make code generation incremental, and you can make transforms incremental, but you cannot make analysis incremental unless you also have deoptimisation paths and accept that you will do some invalid transforms and undo them later. This is what modern JavaScript runtimes do but it do,es with a large memory overhead and introduces weird nondeterminism into performance.

                1. 4

                  Most optimisations involving code motion rely on being able to assume that intermediate function calls will return

                  Once again: you cannot reorder those function calls anyway because you have to assume that they might perform side effects.

                  I suggest that you modify clang to not include the ‘mustprogress’ attribute in the ir it generates, and measure the difference in performance of the generated code. I expect you will find it to be negligible. The fact that no one from the ‘zero cost’ crowd has complained about the fact that rustc already does this is a good heuristic that it makes no difference at all.

                  you can make transforms incremental, but you cannot make analysis incremental unless you also have deoptimisation paths and accept that you will do some invalid transforms and undo them later

                  That’s backwards. It is difficult to undo a no-longer-valid transformation, but fairly easy by comparison to correct a now-mistaken assumption. (Ahem.) Fortunately, abstract interpretation springs eternal; the paper SSA Translation is an Abstract Interpretation is rather inspiring, for instance. Leaving the concrete ir close to the source code has another benefit, incidentally, on top of enabling incrementality: it makes it easier to support high-quality debugging, because logic for destroying information which was present in the source can be centralised and is therefore easier to reason about, characterise, and make configurable.

                  More generally: you are speaking too much in specifics here for a nonexistence proof.