Live data from Hacker News

“gcc will quietly break nearly half of all the packages that it compiles”

metzdowd.com

91–100 of 110 posts

Re: “gcc will quietly break nearly half of all the packages that it compiles”

#91
post #37
post #23

Earlier quoted context omitted.

IMO, it's a little subtler than that. It's not the compiler's fault, it's the language's . Plenty of languages have no undefined behavior that can be written by accident. Go and safe Rust, for instance, have just about no undefined behavior at all, and are both performance-competitive with C. (Go has UB if you cause race conditions, and Rust has UB within `unsafe` blocks analogous to C's UB.) A C compiler, meanwhile,…

Umm wait. Undefined behavior is where the language specification is not 100% precise, and compiler implementations can differ on produced code. Go and Rust only have single implementations. The specification for both are very brief. Are you claiming that a clean box implementation of Go and Rust would always behave identically? I have only one thing to say: I clicked through the Golang spec for 30 seconds and found t…

No, "undefined behavior" is a term with a specific meaning, because it is used for a very specific purpose. "Undefined behavior" means that a compiler can assume that a particular scenario will never happen in correct code, and therefore if it ever thinks it has to care about that scenario, it can in fact ignore it for the purpose of optimization.

For instance, if you have a bool in C++, the only defined behavior is for it to contain 0 or 1. If you somehow force the memory cell to contain 2 or 255 or anything else, you have triggered undefined behavior. This means the compiler never has to check for it. If it's faster for the compiler to implement, say, `if (b) x[1] = a; else x[0] = a;` as `x[b] = a`, it can do that. Even though the `b == 2` case might lead to a buffer overflow, the UB rules means that, as far as the compiler cares, the `b == 2` case cannot exist. If `x.operator[]` is a function that does a bounds check, the compiler can inline it and remove the bounds check. And so forth.

Hence, the rule that on encountering UB, the compiler may do anything. It is not so much that the compiler is intentionally doing anything, as that it is outputting code that assumes the UB can't happen. If the compiler chooses to implement an `if (b)` via a computed jump, the `b == 2` case may land on some completely ridiculous code to start executing. It is not that the compiler wants to land there to punish you, it's that the compiler's only responsibility is to make sure the `b == 0` and `b == 1` cases work.

What you're pointing to is unspecified values. This means that the implementation can return any value, but must actually return some coherent value. If a C++ function returning bool returns an "unspecified value", it is still only returning either 0 or 1. You can act with the resulting bool as if it is in fact a bool. There are no optimization gotchas to worry about. It's just that you don't know what it is.

For the particular case here, when you're inspecting a runtime error, the only useful thing to do with it is to call the Error() method from the error interface. The language guarantees you that it will work (i.e., you have defined behavior, that you have an object that indeed implements the error interface). It doesn't give you any particular guidance on exact error values or the resulting string. But that's no more "undefined behavior" than the result of reading from a file is "undefined".

The entire reason people care about undefined behavior is the fact that compilers can do arbitrarily-stupid(-seeming) things when it is present, which is solely because compilers want to optimize. In the case of unspecified values, there is nothing to optimize.

(Anyway, I am not a Go programmer at all. Maybe there is actually UB somewhere in safe Go. But what I have seen is not it.)

Re: “gcc will quietly break nearly half of all the packages that it compiles”

#92
post #23

The only reasonable thing to say about this was already said upthread of the page, and quoted here: > I have worked on many programs, and whenever I found such a problem (typically called a "portability problem"), where the code was assuming something that the language did not guarantee, I fixed the program rather than bitching about the compiler.

IMO, it's a little subtler than that. It's not the compiler's fault, it's the language's . Plenty of languages have no undefined behavior that can be written by accident. Go and safe Rust, for instance, have just about no undefined behavior at all, and are both performance-competitive with C. (Go has UB if you cause race conditions, and Rust has UB within `unsafe` blocks analogous to C's UB.) A C compiler, meanwhile,…

This makes it sound like Go and Rust have some secret sauce that enables C's performance without UB. But it is not so.

For example, consider an expression like (x*2)/2. clang and gcc will both optimize this to just x, but Go and Rust will actually perform the multiplication and division, as required by their overflow semantics. So the performance vs safety tradeoff is real.

Re: “gcc will quietly break nearly half of all the packages that it compiles”

#93
post #67

Earlier quoted context omitted.

> This is a case of following the letter of the law (in this case the C standard) while disregarding its spirit: all the undefined behaviour was so that C compilers could accomodate for odd architectures while remaining close to the metal, not so that compiler programmers could go out of their way to turn their compiler into a mine field. Computers don't have spirits; they work as you tell them to work, to the letter…

You don't need 4 decades of programming language research to specify that e.g. signed integer overflow either returns an implementation-defined value or the program aborting. There are languages older than C that allowed the useful forms of bit-twiddling but offered much stronger safety guarantees.

But you pay a performance cost for either of those decisions. Consider code like this:

    for (int i=0; i 
Most processors have special support for looping a fixed number of times, e.g. "decrement then branch if zero." If overflow is UB, the compiler can use this support.

But if overflow returns an implementation defined value, then it is possible that N is INT_MAX and the loop will not terminate. In this case the compiler cannot use the fixed-iteration form, and must emit a more expensive instruction sequence.

Re: “gcc will quietly break nearly half of all the packages that it compiles”

#94
post #23

Earlier quoted context omitted.

IMO, it's a little subtler than that. It's not the compiler's fault, it's the language's . Plenty of languages have no undefined behavior that can be written by accident. Go and safe Rust, for instance, have just about no undefined behavior at all, and are both performance-competitive with C. (Go has UB if you cause race conditions, and Rust has UB within `unsafe` blocks analogous to C's UB.) A C compiler, meanwhile,…

This makes it sound like Go and Rust have some secret sauce that enables C's performance without UB. But it is not so. For example, consider an expression like (x*2)/2. clang and gcc will both optimize this to just x, but Go and Rust will actually perform the multiplication and division, as required by their overflow semantics. So the performance vs safety tradeoff is real.

I can't speak for Go, but your intuition regarding Rust code is incorrect. See https://play.rust-lang.org/?gist=09b464627f856e0ebdcd&versio... , click the "Release" button, then click the "LLVM IR" button to view the generated code for yourself. TL;DR: the Rust code `let x = 7; let y = (x*2)/2; return y;` gets compiled down to `ret i32 7` in LLVM.

In fact, there is "secret sauce" here. The secret sauce is that Rust treats integer overflow specially: in debug mode (the default compilation mode), integer overflow is checked and will result in a panic. In release mode, integer overflow is unchecked. It's not "undefined behavior" in the C sense, because of the fact that it always returns a value--there are no nasal demons possible here (Rust disallows UB in non-`unsafe` code entirely, because memory unsafety is a subset of nasal demons). The exact value that it returns is unspecified, and the language provides no guarantee of backward compatibility if you rely on it (it also provides explicit wrapping arithmetic types if you explicitly desire wrapping behavior). And though it's a potential correctness hazard if you don't do any testing in debug mode, it's not a memory safety hazard, even in the unchecked release mode, because Rust's other safety mechanisms prevent you from using an integer like this to cause memory errors.

Re: “gcc will quietly break nearly half of all the packages that it compiles”

#95
post #94

Earlier quoted context omitted.

This makes it sound like Go and Rust have some secret sauce that enables C's performance without UB. But it is not so. For example, consider an expression like (x*2)/2. clang and gcc will both optimize this to just x, but Go and Rust will actually perform the multiplication and division, as required by their overflow semantics. So the performance vs safety tradeoff is real.

I can't speak for Go, but your intuition regarding Rust code is incorrect. See https://play.rust-lang.org/?gist=09b464627f856e0ebdcd&versio... , click the "Release" button, then click the "LLVM IR" button to view the generated code for yourself. TL;DR: the Rust code `let x = 7; let y = (x*2)/2; return y;` gets compiled down to `ret i32 7` in LLVM. In fact, there is "secret sauce" here. The secret sauce is that Rust t…

A constant expression is not a good test: any compiler worth its salt (which includes LLVM) will be able to optimise a case like that. Change the function to:

  pub fn test(x: i32) -> i32 {
      let y = (x*2)/2;
      y
  }
and you'll see the difference (e.g. compare against a similar function on https://gcc.godbolt.org/ ).

> The secret sauce is that Rust treats integer overflow specially

Debug vs. release mode is irrelevant: the panicking case is more expensive than any arithmetic, and, the RFC[0] was changed before it landed:

> The operations +, -, [multiplication], can underflow and overflow. When checking is enabled this will panic. When checking is disabled this will two's complement wrap.

The compiler still assumes that signed overflow may happen in release mode, and that the result needs to be computed as per two's complement, i.e. not unspecified.

[0]: https://github.com/rust-lang/rfcs/blob/master/text/0560-inte...

Re: “gcc will quietly break nearly half of all the packages that it compiles”

#96
post #11

Earlier quoted context omitted.

Actually it's still a useful question, I think. A simple example is function argument evaluation order. If you have: foo(bar(), baz()) A compiler is free to call these functions in either order. That means if baz relies on state mutated by bar, the program may behave differently if the compiler chooses to reorder evaluation.

There's a parable where a man goes to the doctor and says, "Doctor, whenever I drink my coffee with the spoon in the cup, the spoon handle pokes me in the eye and it hurts." And the doctor says, "Well, stop doing that." If you wrote `foo(bar(), baz())` and `baz()` relies on state mutated by `bar()`, your code is bad, and you should feel bad, because experiencing those bad feelings is the way you learn to not write ba…

The eye-spoon defence of C undefined and unspecified behaviours is very unfortunate and misleading: it makes it sound like avoiding them is as easy as just taking a spoon out of a cup.

Theoretically "stop doing that" works... but history has shown that it really doesn't work in practice, in C, in programming more broadly, and, really, in any human endeavour ("planes don't need safety procedures, just stop making mistakes").

Re: “gcc will quietly break nearly half of all the packages that it compiles”

#97
post #95
post #94

Earlier quoted context omitted.

I can't speak for Go, but your intuition regarding Rust code is incorrect. See https://play.rust-lang.org/?gist=09b464627f856e0ebdcd&versio... , click the "Release" button, then click the "LLVM IR" button to view the generated code for yourself. TL;DR: the Rust code `let x = 7; let y = (x*2)/2; return y;` gets compiled down to `ret i32 7` in LLVM. In fact, there is "secret sauce" here. The secret sauce is that Rust t…

A constant expression is not a good test: any compiler worth its salt (which includes LLVM) will be able to optimise a case like that. Change the function to: pub fn test(x: i32) -> i32 { let y = (x*2)/2; y } and you'll see the difference (e.g. compare against a similar function on https://gcc.godbolt.org/ ). > The secret sauce is that Rust treats integer overflow specially Debug vs. release mode is irrelevant: the p…

Bah, darn you for changing the RFC out from under me. :P

I don't understand the point of specifying wrapped behavior in unchecked mode rather than leaving the value unspecified. Surely we don't care about properly accommodating use cases that will panic in debug mode.

Re: “gcc will quietly break nearly half of all the packages that it compiles”

#98
post #97
post #95

Earlier quoted context omitted.

A constant expression is not a good test: any compiler worth its salt (which includes LLVM) will be able to optimise a case like that. Change the function to: pub fn test(x: i32) -> i32 { let y = (x*2)/2; y } and you'll see the difference (e.g. compare against a similar function on https://gcc.godbolt.org/ ). > The secret sauce is that Rust treats integer overflow specially Debug vs. release mode is irrelevant: the p…

Bah, darn you for changing the RFC out from under me. :P I don't understand the point of specifying wrapped behavior in unchecked mode rather than leaving the value unspecified. Surely we don't care about properly accommodating use cases that will panic in debug mode.

It makes it easier to have assurances about the behaviour of the program, even in error cases, https://github.com/rust-lang/rfcs/pull/560#issuecomment-6999...

Re: “gcc will quietly break nearly half of all the packages that it compiles”

#99
post #89
post #66

Earlier quoted context omitted.

>Computers don't have spirits But standards committee do. >Rust is extremely impressive because they've found so many ways to do high-level programming while maintaining low-level performance. But they can only do that because they have the benefit of the 4 decades of programming language research that have occurred since the basics of C were designed. I doubt rust could be ported to a 8bit PIC microcontroller, or to…

> I doubt rust could be ported to a 8bit PIC microcontroller, or to a 6502 keeping reasonable performance characteristics I don't believe this is correct. Most of why Rust avoids UB is that it uses static types much more effectively than C does. Static types are an abstraction between the programmer and the compiler for conveying intent, that cease to exist at runtime. So the runtime processor architecture should be…

> I don't believe this is correct.

Did rust find a way to defeat the halting problem and push all the array bound checks to compile time? How well does rust deal with memory bank switching where an instruction here makes that pointer there refer to a different area of memory?

Re: “gcc will quietly break nearly half of all the packages that it compiles”

#100
post #81

Earlier quoted context omitted.

> And it does that by implementing a compiler for a language with existing standard, which is ANSI. If you don't like the ANSI C standard (and I admit it's not perfect), don't use a compiler for ANSI C. I'm not arguing that GCC should violate the ANSI standard; rather it should provide additional guarantees above the what ANSI requires (which was always the intent of the standard; the standard defines the absolute mi…

> I'm not arguing that GCC should violate the ANSI standard; rather it should provide additional guarantees above the what ANSI requires ... The problem with that approach is that it introduces dependency on the compiler. The original code was ANSI C and thus should compile fine on all compilers compatible with ANSI C, the new code is not as each compiler will decide to handle undefined behavior differently. Either y…

> The original code was ANSI C and thus should compile fine on all compilers compatible with ANSI C, the new code is not as each compiler will decide to handle undefined behavior differently.

Except 40% of the original code already wasn't ANSI C.

> Either you'll make the exact compiler a hard dependency (i.e. it always has to be compiled with gcc and fails to build with everything else), or it will produce "correct" binaries on some compilers and "incorrect" binaries on others. That's hardly an improvement.

Having code that was broken under GCC not be broken under GCC absolutely is an improvement, particularly since in fact this kind of code often works on every other extant compiler.

> make the ANSI C more strict by adding the guarantees to the standard. Which is not going to happen, I guess.

Standards tend to codify existing practice. There's no reason the standard couldn't be made stricter - but the way we get to there from here is if the major compilers implement stricter restrictions and can show that they can be implemented consistently and users find them useful. GCC has been willing to do that kind of innovation for other parts of the standard.

Post reply on HN