Live data from Hacker News

The Problem with Friendly C

blog.regehr.org

41–50 of 174 posts

Re: The Problem with Friendly C

#41
post #27

Tone: I do not mean this as sarcasm or merely chasing fashion, I'm quite serious. As both theory and practice are showing, you're never going to be able to get the consensus you want out of C. There's no "saving" C... not because that's somehow mathematically impossible, but simply because the project is too staggeringly large for us to even wrap our heads around. It would literally be easier to get people to start u…

Reghr came to a Bay Area Rust Meetup long ago, we're all generally fans of his work. :)

Re: The Problem with Friendly C

#42

Earlier quoted context omitted.

I think the problem is that some of these optimizations aren't just compiler makers being greedy, they're actually a huge benefit. IMO what's missing is the ability to mark areas "unsafe" -- IE, tell the compiler "it's ok to take advantage of certain optimizations here" while marking other areas "please don't goof with this" (ie, security critical code). You can kind of do this with pragmas, but not really. Here's a…

For example, knowing that INT_MAX+1 is undefined allows optimizing "X+1 > X" to "true". If a programmer writes "X+1 > X", chances are this is an overflow check. Doing this is perfectly defined by the standard if X is an unsigned integer, but not if it's signed. That's what doesn't make sense, since they could've made the unsigned case undefined as well. which allows a broad range of loop optimizations to kick in What…

> If a programmer writes "X+1 > X", chances are this is an overflow check.

If the programmer writes

    for (i = a; i 
then it's not clear if you need to check 'i' for overflow; Assuming that 'i++' is greater than 'i' allows the compiler to, say, unroll the loop.

Re: The Problem with Friendly C

#43
post #3

If ((uint32_t)x But why can't it just produce something different on each system? Allow me to call it as I see it: the modern interpretation of undefined behaviour is bullshit. What compilers do today should be the recourse of absolute last resort, and the sort of thing that makes its authors feel bad. But it seems to be treated as a matter of course. I don't know what to say. Mandatory reading: http://robertoconcert…

> But why can't it just produce something different on each system?

It could. That is indeed how Rust, for example, defines it.

But in C "unspecified values" have a way of turning into undefined behavior really quickly.

Here's an actual bug we have in Rust [1]:

    let index = 1.04E+17 as u8;
    let array = vec![1, 2, 3, 4, 5];
    println!("{}", array[index as usize]); // segfault
Why does that segfault instead of emitting a safe panic? Because the IR looks something roughly like this (in C notation):

    double tmp = 1.04e+17;
    uint8_t index = (uint8_t)tmp;
    vector *array = make_a_vector();
    if (index >= array->length)
        panic();
    printf("%d\n", array->ptr[index]);
The optimizer correctly determines that the cast from double to uint8_t results in an undefined value. In LLVM terminology, undefined values can arbitrarily change value over their live range, and this is allowed per the C specification. So LLVM is allowed to replace the conditional "index >= array->length" with "false", and this is indeed what it does, leading to this code:

    double tmp = 1.04e+17;
    uint8_t index = (uint8_t)tmp;
    vector *array = make_a_vector();
    printf("%d\n", array->ptr[index]);
And with that, LLVM optimizes out our bounds check, and safe code is able to perform arbitrary memory reads and writes. Note that all we needed was an unspecified value resulting from a floating point cast!

The fix is probably going to be to stop using LLVM's floating point conversion instructions and to instead use some "safe conversion" intrinsics that we'll define.

> What compilers do today should be the recourse of absolute last resort, and the sort of thing that makes its authors feel bad. But it seems to be treated as a matter of course. I don't know what to say.

You might think that I would think this way too after fighting bugs like the above, but I actually don't. I appreciate the performance that undefined behavior brings to C, and I think we'd have much slower programs without it. Undefined behavior resulting from strict aliasing is the reason why a for loop setting an array to zero can optimize to a 10x faster memset, for example. And heavily inlined series of functions, as well as generic libraries like the C++ STL, frequently require these optimizations to get reasonable performance. I think it's not an exaggeration to say that much of the software performance we take for granted—stuff you care about, like codecs, games, browser implementations—is the result of optimizations that are enabled by undefined behavior.

[1]: https://github.com/rust-lang/rust/issues/10184

Re: The Problem with Friendly C

#44
post #13
post #3

If ((uint32_t)x But why can't it just produce something different on each system? Allow me to call it as I see it: the modern interpretation of undefined behaviour is bullshit. What compilers do today should be the recourse of absolute last resort, and the sort of thing that makes its authors feel bad. But it seems to be treated as a matter of course. I don't know what to say. Mandatory reading: http://robertoconcert…

What modern interpretation? As long as C has been standardized, undefined behaviour has meant nasal demons. There's an argument to be had that some undefined behaviour should rather be unspecified or implementation-defined, but compilers making use of it for aggressive optimization? That's by design. Quoting another article by John Regehr: My view is that exploiting undefined behavior can sometimes be OK if a good de…

How about being build-system defined? As in, let me specify a "target behavior profile" as a switch to the compiler, which would be shorthand for a bunch of switches that define specific results for specific undefined behaviors: "-foverflow=checked" (add explicit checks) vs. "-foverflow=wrap" (do what most ISAs already do, but even on target ISAs that don't do that) vs. "-foverflow=clip" (do something weird that no target ISA does, like making short 65535 + 1 = 65535.)

I would expect that a single-pass compiler like gcc would look at the toolchain's target ISA, compare it to these switches, and then generate extra shim code to adapt any cases of target-arch-does-X into code-does-Y-on-X, while letting do-X-on-X just translate directly. Basically the same way floating-point code gets compiled: architectures that support it get it, architectures that don't get a shim.

Meanwhile, a two-stage compiler with an intermediate representation, like clang, should generate IR with granular types specifying the chosen behavioral semantics on each operation. The second step in such a toolchain becomes much more formalized: rather than taking code that can contain undefined behavior as input, and then "defining" it during translation in some random heuristic manner, you just take code that already specifies the behavior it wants in full, and then attempt to generate target-arch code that does whatever it says in the most efficient manner possible while not breaking any of the guarantees the IR asks for. In other words, your code-generation stage becomes a static recompiler, like this one[1] for the NES ISA.

And you can always specify a switch at code-gen time that will error out the compilation if you've done something that has caused shims to be inserted. Or even a switch to throw away the behavioral semantics the IR asks for and substitute your own, like happens in most performance-focused emulator software.

[1] http://andrewkelley.me/post/jamulator.html

Re: The Problem with Friendly C

#45
post #25
post #17

Earlier quoted context omitted.

Undefined behaviour has meant undefined behaviour. That doesn't mean nasal demons. That means this: 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 i…

the Standard imposes no requirements on undefined behaviour. The list you quoted is informative, to give us a basic idea what to possibly expect, not a list of things we may rely upon. Rejecting translation outright aside, undefined behaviour is the strongest language the standard uses for illegal constructs. I do not see it as undefined in the sense of lacking a common definition (eg is 0 a natural number, or not) b…

You'd think that'd make things clear, then: if you want a compiler to error out on trying to compile a literal 0/0, then you'd probably also want the same behavior of trying to compile a literal dereference of 0, or a literal (INT_MAX * 2), or whatever else.

Re: The Problem with Friendly C

#46
post #44
post #13

Earlier quoted context omitted.

What modern interpretation? As long as C has been standardized, undefined behaviour has meant nasal demons. There's an argument to be had that some undefined behaviour should rather be unspecified or implementation-defined, but compilers making use of it for aggressive optimization? That's by design. Quoting another article by John Regehr: My view is that exploiting undefined behavior can sometimes be OK if a good de…

How about being build-system defined? As in, let me specify a "target behavior profile" as a switch to the compiler, which would be shorthand for a bunch of switches that define specific results for specific undefined behaviors: "-foverflow=checked" (add explicit checks) vs. "-foverflow=wrap" (do what most ISAs already do, but even on target ISAs that don't do that) vs. "-foverflow=clip" (do something weird that no t…

As long as the option -foverflow=undefined is kept around, you have my vote. Now, it's just a simple matter of programming ;)

Re: The Problem with Friendly C

#47
post #14
post #3

If ((uint32_t)x But why can't it just produce something different on each system? Allow me to call it as I see it: the modern interpretation of undefined behaviour is bullshit. What compilers do today should be the recourse of absolute last resort, and the sort of thing that makes its authors feel bad. But it seems to be treated as a matter of course. I don't know what to say. Mandatory reading: http://robertoconcert…

Apparently a (uint32_t) shifted by 32 is license to become completely insane. int main(int argc, char **argv) { uint32_t x = (uint32_t)0x12345678

> Apparently a (uint32_t) shifted by 32 is license to become completely insane.

That's easy to explain. The optimizer replaced the instruction with an undef [1], which has no live range. So it printed the value of some random register, which happened to be 0, 0x5a788cb0, or 0x54049be8.

[1]: http://llvm.org/docs/LangRef.html#undefined-values

Re: The Problem with Friendly C

#48
post #45
post #25

Earlier quoted context omitted.

the Standard imposes no requirements on undefined behaviour. The list you quoted is informative, to give us a basic idea what to possibly expect, not a list of things we may rely upon. Rejecting translation outright aside, undefined behaviour is the strongest language the standard uses for illegal constructs. I do not see it as undefined in the sense of lacking a common definition (eg is 0 a natural number, or not) b…

You'd think that'd make things clear, then: if you want a compiler to error out on trying to compile a literal 0/0, then you'd probably also want the same behavior of trying to compile a literal dereference of 0, or a literal (INT_MAX * 2), or whatever else.

You missed the point of the analogy: It's not about how compilers treat the expression 0/0, but how mathematicians do.

If that expression turns up in your calculation, you did something you were not supposed to do and have no one else to blame.

Re: The Problem with Friendly C

#49
post #28
post #20

Earlier quoted context omitted.

I am almost LOLing at the segmentation fault. Where the fuck does a segmentation fault come from? It's shifting a value . Utterly mystifying.

Maybe it's a hardware trap and not a regular null reference segmentation fault?

Well, shl doesn't trap on x86 for out of range values; it just masks the second operand by 0x1f [1]. But it could be some other kind of trap.

Complete guess in the dark: tcc misencoded an instruction causing an illegal instruction trap, maybe in some sort of misguided attempt to optimize the shift to a lea.

[1]: http://x86.renejeschke.de/html/file_module_x86_id_285.html

Re: The Problem with Friendly C

#50
post #46
post #44

Earlier quoted context omitted.

How about being build-system defined? As in, let me specify a "target behavior profile" as a switch to the compiler, which would be shorthand for a bunch of switches that define specific results for specific undefined behaviors: "-foverflow=checked" (add explicit checks) vs. "-foverflow=wrap" (do what most ISAs already do, but even on target ISAs that don't do that) vs. "-foverflow=clip" (do something weird that no t…

As long as the option -foverflow=undefined is kept around, you have my vote. Now, it's just a simple matter of programming ;)

I think the equivalent for the current practice would basically be:

1. setting "-foverflow=promote" on the compiler's parsing-and-IR-generating stage, thus generating code that acts like all integers occupy infinitely-sized abstract machine registers, and expects this to get shimmed as checked promotion to pointers-to-structs from a bignum library on most architectures;

and then, seperately,

2. setting an optimization flag like "-Onopromote" for the target-recompilation stage, that throws away all the bignum promotion shims that would have been inserted, and pretends that it's adhering to bignum-promotion semantics anyway (thus creating code that will be optimized the way C code currently is.)

That's quite a bit more complicated, but also a lot more explicit about what's going on: you have to specify (or pick a profile that specifies) the actual semantics you want, and then you have to explicitly opt into assuming those semantics rather than enforcing them. There'd probably be a similar pair of {semantic, assumption} flags for each currently-undefined behavior.

Post reply on HN