148 comments

[ 3.5 ms ] story [ 201 ms ] thread
I can see both sides of this. But looking at this argument:

> There are two ways to evaluate the the C specification's rightness and properness. [...] The second is to ask what is most useful. And there again the C committee have clearly failed.

Just two days on HN we saw this article: https://news.ycombinator.com/item?id=11468603 In which it says:

> no matter the kind of software, performance is almost always worse than our customers would like it to be.

That is why all of this is happening. There is a market demand for performance. Compilers that increasingly exploit UB in C is just a manifestation of that market demand.

  There is a market demand for performance.
You could fulfil that demand without breaking existing code by making the riskier optimisations configurable and turned off by default.
They are usually. Most people don't change the -03 in their build files after a compiler update though.
-O4 exists for 'risky' optimizations.

-O3 is for optimizations that are mostly not risky.

-O2 is for optimizations that don't involve a speed/size tradeoff

-O1 is for optimizations that don't slow your compiler down too much

Obviously these are fuzzy categories, but -O4 is where "undefined behavior" optimizations belong. Alternately, each individual optimization has its own flag, and can be turned on individually.

I've never heard of -O4 for "risky" optimizations, and I can't find any reference to this. Do you have one?
If you search for "gcc O4" you'll find some references (it's tricky because if you search for -O4 Google will search for everything without O4).

Currently, there are no optimizations that fit in that category, but as recently as 2003 I remember seeing it in the man pages. I believe there was -O5 and -O6 too, but I don't trust my memory enough to be certain.

The results for that query all seem to say that "gcc -O4" doesn't exist. There is one proposal from Chris Lattner for adding this to Clang, but it's not clear that this ever happened.
Currently gcc uses -Ofast for 'dangerous' optimizations.

Maybe I remembered the history wrong. There are definitely cases of people using -O4 http://www.larcenists.org/Twobit/KVW/kvwbenchmarks.html .

There are definitely optimization options which are (or have been) available that are not turned on by -O3. For example:

-ffast-math

-funroll-all-loops

-fstrict-aliasing

Are not turned on (for good reasons: unrolling loops often slows a program down).

So there's no reason UB optimizations need to be turned on by default, or with -O3. You could throw them in -Ofast or use -Oub, or use optimization specific flags (they should have optimization specific flags in any case) and that was my main point.

-Ofast turns on optimizations that can break standard-conforming programs!

That is a very different thing that turning on optimizations that might break non-standard-conforming programs!

I think the idea of making it controlled by optimization flags is interesting, but you would need to formalize what the more "safe" variant of the language is if people are actually going to depend on it.

That is a very different thing that turning on optimizations that might break non-standard-conforming programs!

You're talking as though the standard were well-defined. It's not.

One option in GCC I'm aware of is -faggressive-loop-optimizations . Quoting the manual "This option tells the loop optimizer to use language constraints to derive bounds for the number of iterations of a loop. This assumes that loop code does not invoke undefined behavior by for example causing signed integer overflows or out-of-bound array accesses. The bounds for the number of iterations of a loop are used to guide loop unrolling and peeling and loop exit test optimizations. This option is enabled by default. "

https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html

But why do we always want more, more, more? If gcc -O2 without whole-program optimization was good enough to encode MP3 faster than real-time on a 366 MHz Pentium laptop in 1999, then surely it's good enough now, with all the increases in processor performance since then. If that level of compiler optimization isn't good enough now, then maybe that means we programmers are doing something wrong. At some point, better optimization in a compiler probably isn't worth what you give up to get it, in terms of reliability and comprehensibility.

Edit: All the responses are right. Feel free to downvote this ill-considered comment into oblivion, now that I can't take it back.

People in 1999 weren't running huge neural nets and beating the world champion in Go.

There are millions of computers in data centers around the world. They are working on problems that someone thought it was worthwhile enough to pay for. If C compilers emit code that is slower, that means buying and maintaining a lot more computers, using more energy, producing more greenhouse gas, etc.

> People in 1999 weren't running huge neural nets and beating the world champion in Go.

Which was mostly GPU code, so it was either CUDA or OpenCL btw. Not actually C.

People do need to realize that the fastest, massively parallel supercomputers are in fact written in CUDA / OpenCL. And the most powerful supercomputers are mostly being used to display 3d-models of a sword-wielder fighting a guy with guns.

Fair enough. But some very significant fraction of the hardware and energy spent on data centers today is on CPUs.
> If gcc -O2 without whole-program optimization was good enough to encode MP3 faster than real-time on a 366 MHz Pentium laptop in 1999, then surely it's good enough now

This is patently a ludicrous argument. How can the speed of encoding an MP3 be used as a proxy for countless other applications of computers?

Programs that are slower than they need to be waste real energy and other industrial resources.

Yes, you don't need the performance for listening to MP3s. MP3 decoding is nowhere near the outer limit of CPU performance you rely on every day.

Just to stay within the domain of codecs, YouTube would not be a nice experience on a 366 MHz Pentium laptop with primitive compiler optimizations.

I don't think this is an "ill-considered comment" at all.

Daniel J. Bernstein made the same comment in his slides about "the slow death of the optimizing compiler"[ https://cr.yp.to/talks/2015.04.16/slides-djb-20150416-a4.pdf ]. Basically, a lot of programs are I/O bound. Those that aren't are often dominated by a tiny amount of super-hot spots that can be optimized by hand via assembly language. Most code is "freezing cold" -- it's hardly worth optimizing. C compilers are giving up safety and getting very little in return, except shiny new compiler benchmarks.

You don't really understand the UB problem if you model this in your head as some sort of simple set of rules that everybody really should have known all along, and it's not a big deal to fix the UB in your code. You have to make sure you're modeling it as A: a set of rules so arcane and byzantine that virtually nobody understands them, and to a first approximation, no non-trivial well-defined C program has ever been written and B: that this was a de facto change in C's nature... in a very real way, the language changed out from underneath people, without them expecting it.

You can't just shrug this away with "Well, if you want performance...", because people in fact don't want abstract "performance"... they want the language they were truly writing in to perform well, not for what is de facto a different dialect of the language to suddenly appear and replace the language they were using.

because people in fact don't want abstract "performance"

That's true, if performance were the only consideration, we would all be writing assembly.

> You don't really understand the UB problem if you model this in your head as some sort of simple set of rules that everybody really should have known all along

What does this have to do with my comment, at all?

> You can't just shrug this away with

Who is shrugging?

You don't understand a complex problem until you understand both sides. The article only presents one side. I am presenting the other. It doesn't mean I think the problem is simple or easy.

To add to this, a lot of what compilers decide is undefined isn't bizarre corner case code that rarely happens, but common and useful techniques that the compiler will happily demolish without warning. The 'defined' workarounds, however, are usually difficult, less safe, harder to understand, and often slower. This is without getting into the nightmare that is LTO finding 'undefined' behavior across program boundaries.
> The 'defined' workarounds, however, are usually difficult, less safe, harder to understand, and often slower.

That has not been my experience. Do you have an example of this?

There were examples in the linked message board post [1]. Note how there is now a paragraph describing why the code is written with 4 casts instead of comparing pointers naively. Multiply this by hundreds of instances of similar code in a large code base.

[1] http://xenbits.xen.org/gitweb/?p=xen.git;a=blob;f=tools/libx...

Why not just write a function is_ptr_less that does all the casts.

P.S: In C++ std::less is guaranteed to do the right thing(tm).

Sure - all sorts of stuff with type-punning in unions because much harder to understand when it's transformed into a whole series of casts to/from void/char , with the loss of checking that one gets from a union. Even without unions, aliasing rules can screw you over in surprising ways. Look at the free list example linked in the post for another one.

Slower - the various methods of checking for integer overflow are slower than x = a + b; if (a < x) {/overflow*/}. It can also be easier to just use a slower, or more memory hungry way of doing things than a better but needlessly complicated method.

At my current job we've sacrificed memory (and in this case, performance as a consequence) because the complexity cost of keeping some critical code in the defined behavior realm was too risky.

Type punning in unions is best done using memcpy. Compilers know what it does and optimize it: http://blog.regehr.org/archives/959

As for integer overflow at least gcc and clang have built-in functions that generate optimal code by checking the CPU's overflow flag: http://clang.llvm.org/docs/LanguageExtensions.html#checked-a... and https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins...

C99 Techincal Corrigendum 3 clarifies that union-based type punning is valid by amending 6.5.2.3 with this footnote:

If the member used to access the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called “type punning”). This might be a trap representation.

(copied from a comment on blog.regehr.org)

This is actually a really common area of misunderstanding. But ultimately, one of the main uses of unions is to provide multiple ways to access the same data.

In C, yes, type-punning with union is valid. However, in C++, it is considered undefined behavior and may get removed by the compiler if it sees fit.
Thanks for the clarification. Do the new C++ standards explicitly discuss this anywhere?
There's this stackoverflow qanswer: http://stackoverflow.com/questions/11373203/accessing-inacti...

where answers try to distill the standard into something legible by humans. Unsurprisingly, they come to different conclusions.

The overall interpretation in some situations, if you look at things just right, you can do type punning in C++. But it's pretty risky since you're a LOC away from the compiler deleting your function.

Or, you can't really do it at all safely. Which I would conclude from the above anyways.

> You can't just shrug this away with "Well, if you want performance...", because people in fact don't want abstract "performance"... they want the language they were truly writing in to perform well, not for what is de facto a different dialect of the language to suddenly appear and replace the language they were using.

Is wanting a for loop setting an array to zero to optimize into memset optimizing the language they were truly writing in? I think it is. But that optimization frequently depends on undefined behavior.

UB exploitation usually exists because people filed bugs on compilers complaining that they didn't optimize some case they expected to optimize.

Is wanting a for loop setting an array to zero to optimize into memset optimizing the language they were truly writing in? I think it is. But that optimization frequently depends on undefined behavior.

Yes, this is a good optimization, since it efficiently does what the programmer intended. The bad optimization is removing the security-essential loop altogether when the compiler notices that result appears unused, and sensitive information is left susceptible to later attack.

UB exploitation usually exists because people filed bugs on compilers complaining that they didn't optimize some case they expected to optimize.

I doubt that anyone has ever filed a bug saying "I explicitly wrote a loop to zero memory, but the compiler failed to optimize it out." If you know of one, please point to it. I think you are throwing out the baby (intentional C) with the bathwater (autogenerated C++).

> I doubt that anyone has ever filed a bug saying "I explicitly wrote a loop to zero memory, but the compiler failed to optimize it out."

That optimization is a natural consequence of SROA and DCE. If you claim you don't want those optimizations, I don't know what to tell you. Those optimizations are some of the most basic, critical optimizations any modern C/C++ compiler does and throwing them away can easily result in at least a 2x performance loss.

The optimization actually has very little to do with SROA. In LLVM there is a special loop idiom recognition pass designed to detect memset / memcpy loops that works after ordinary SSA conversion.
> I doubt that anyone has ever filed a bug saying "I explicitly wrote a loop to zero memory, but the compiler failed to optimize it out."

I suspect it's less that they asked for that than they asked for some generic optimization on loops, and the implementation works great 99% of the time, and 100% of the test cases, but because it was approached from a performance perspective the idea that sometimes we want to change the state of the memory, even though it has no affect on a correctly functioning program, isn't considered. Zeroing memory in this way has nothing to do with correctly running programs, the only use is to defend against your program malfunctioning and exposing the data, or some external actor looking at memory. Neither of those have to do with the normal functionality of the code the compiler is running, but is is important. Unfortunately since it deals with things that have nothing to do with the actual instructions needed to make the program function, it's easy to overlook.

> because it was approached from a performance perspective the idea that sometimes we want to change the state of the memory, even though it has no affect on a correctly functioning program, isn't considered.

A lot of dead stores happen because of post-inlined, post-constant-propagated (etc.) code. Nobody writes that code by hand; it occurs due to inlining and other aggressive types of optimizations. You need those optimizations for performance; IPO is a huge win.

It's like the old joke: "Who writes that kind of code? Macros do."

I understand, I'm not really taking a stance. The question, really, is should the compiler be responsible for making sure your unused bits and ends of memory that are provably irrelevant to the functioning of the program that is being compiled stay exactly as you left them? I think it's interesting because the situation is a bit untenable on both ends of the spectrum.

If we disallow all those optimizations, we end up with hugely wasteful programs in the current C ecosystem. Enforcing this is useless, the market will route around your best intentions, because no company that's making money will be willing to cede a huge performance gap to their competitors.

If we require code be annotated to bypass optimizations, we run afoul of the fact that it's impossible to know what optimizations might be developed in the future that will affect portions of our code that might be security sensitive.

The problem is insidious and deep. Imagine an architecture that allows specialized instructions to speed up zeroing chunks of memory, but does so by hardware mapping different pages around behind the scenes (to swap in a pre-zeroed page for a filled page). Correctly detecting that the architecture has an efficient method for zeroing large chunks of space could be detected and used by the compiler, but the hardware is working against us because the original memory may still be accessible to an external actor. C is ensuring the memory addresses you requested are zeroed, it's just not ensuring that the data that existed within them is no longer anywhere on the system.

The C standard already allows the compiler to remove calls to memset if the memory isn't used afterwards by the program (essentially dead-store elimination). C11 provides memset_s to clear memory for security reasons.
You can easily write a for loop that optimizes to memset without relying on undefined behavior. The only potential problem is that the loop bound evaluation could require a memory access, but this is simple to work around.
Well, not if the compiler can't prove that the stores don't alias the induction variable. TBAA is the easiest and most reliable way to prove that, which is UB exploitation.
No, that is not the most reliable way to prove that property. The most reliable way is to ensure that the loop bound and induction variable are local variables that have not had their address taken.
Well, yes, in the trivial case, but not if the induction variable and/or loop bound come in as references (as in templated code, etc).

There's also the issue of the store changing the array base, as in Chris Lattner's example on "What Every C Programmer Should Know".

You can always make the induction variable and loop bound local variables, and the induction variable is almost always a local variable anyways. It was the WebKit coding style for years to use local variables for loop bounds for this reason.

This is the example you're talking about in "What Every C Programmer Should Know":

  float *P;
  void zero_array() {
    int i;
    for (i = 0; i < 10000; ++i)
      P[i] = 0.0f;
  }
If you make the global variable static, then it gets converted to a memset with -fno-strict-aliasing, because the 'P' variable never has its address taken. If you used LTO with sufficient linkage information, the same thing would happen. Similarly, if you pass the array as an argument then the loop gets converted to a bzero call just fine without -fno-strict-aliasing:

  float *P;
  void zero_array_impl(float* a) {
    int i;
    for (i = 0; i < 10000; ++i)
      a[i] = 0.0f;
  }
  void zero_array() {
    zero_array_impl(P);
  }
I don't think this is a very good example for these reasons.
To a large degree the language didn't change (some of the undefined behaviours we are discussing were in C89), rather the compiler vendors started to enforce parts of it.
You have to work pretty hard to get code to a state such that compiler optimizations will save you.

I've had this same argument with cache management specialists. If you have to consider how the cache works for your product to work, you're doing it wrong.

> You have to work pretty hard to get code to a state such that compiler optimizations will save you

At least in c++, this is absolutely 100% not the case, there's a huge difference between -O0 and -O2.

I need to check that out for myself.
I've dealt with bugs that existed at -O0, -O2 was fine. But the bug was in the compiler itself.
(comment deleted)
UB being the various "undefined behavior" bits of C?
Yes. Compiler developers like it; everyone hates it. :)
You like it when aliasing rules allow your compiler to copy the target of a pointer into a register, rather than read it from memory over and over in case an intervening write through another pointer could have overwritten it.
I have to say, strictly as an uninformed outsider, this bit of nastiness is just pushing Go, Rust and even Swift higher on my priorities list.

I don't think I'm the only one.

Well then, don't look too deeply at the nastiness in Go, Rust, and Swift.
Can you elaborate? Hopefully they don't have straight UB do they?
Well, Rust for example does not have a formal well-defined behavior to start with, so its kinda fuzzy about UB too. And of course `unsafe` is another story altogether.
Can you elaborate? The rust docs [1] seem to disagree:

    Unlike C, Undefined Behavior is pretty limited in scope in Rust. All the core language cares about is preventing the following things:

    - Dereferencing null or dangling pointers
    - Reading uninitialized memory
    - Breaking the pointer aliasing rules
    - Producing invalid primitive values:
    - dangling/null references
    - a bool that isn't 0 or 1
    - an undefined enum discriminant
    - a char outside the ranges [0x0, 0xD7FF] and [0xE000, 0x10FFFF]
    - A non-utf8 str
    - Unwinding into another language
    - Causing a data race
And all of those things require `unsafe`, so safe rust cannot do any of them (barring compiler or unsafe library or OS bugs).

Edit: And I must admit I don't know much about language theory or formal definition, but there is also a self-described formal grammar [2].

[1]: https://doc.rust-lang.org/nomicon/races.html [2]: https://doc.rust-lang.org/grammar.html

I think the best way to approach this question is the see CAR Tony Hoare's "Billion Dollar Mistake" talk.

A fellow stands up at the end and says, in detail, something that can be summarized as "it's all golden until you invoke the I/O monad."

What do those languages return when you calculate MAX_INT + 1?
In Rust:

  let x = std::u32::MAX + 1;
This line will generate a warning upon build, and in a debug build, will panic. In a release build, it will overflow.
In Swift:

    let a = Int.max + 1
Produces a compile error as the overflow is within Swift's conception of a "literal expression".

If we step out to a case where the overflow is not trivially a literal expression:

    func foo(a: Int) -> Int {
        return Int.max + a
    }

    foo(1)
This causes a runtime assertion failure in optimized or unoptimized builds.

There is a (rarely-used) "Ounchecked" optimization level for which this program produces UB.

I'd say that staying an uninformed outsider is an excellent choice.
The list of things I wanted to learn in my life is halfway to the horizon now. I'm good.
You might want to check out Ada, D, and OCaml too.

Ada is as safe as Rust and lower-level but very fast and very mature. It gets a bit of a bad rap for being ugly, but after a couple hours the Pascal-ish syntax is not any uglier than C. The type system is much less expressive than Rust's, however, which makes a lot of things more awkward. What you lose in conciseness you make up for in performance and readability. Ada does require you to be extremely explicit about everything, which many people do not like. Still, if you're looking for a language around the abstraction level of C but safe and easily auditable, Ada is a quite well-designed language.

D is basically C++25. There's still some undefined behavior, but you hit it much less often. Also it has excellent metaprogramming support. It is not as safe as Rust, but a little safer than C++. Overall it's a very pleasant language if you're a C programmer and want something higher-level.

OCaml also has a syntax that many people find unpleasant at first. Personally I got over that pretty fast and now fine ML syntaxes beautiful. OCaml is essentially garbage-collected Rust with prettier syntax. It has predictable performance (in the sense of being fairly easy to predict what the compiler will generate—the GC will make actual runtime performance a little unpredictable, but isn't bad; probably because OCaml programs tend to involve much less GC pressure than Java or C#).

I don't think Go should be lumped in with those languages though. It is not very safe (it has a terrible error-handling mechanism and doesn't isolate unsafe code particularly well), it has a large runtime and slow generated code, it offers piss-poor abstractions, etc. If Go becomes the next popular systems language, language design will be set back another 30 years.

How is Ada lower-level than Rust? I don't really know Ada. I'm also interested in the performance comparison, we're usually neck and neck with C and C++ already.
I am not as familiar with Rust as I would like, but it seems like a relatively high-level language, akin to OCaml without garbage collection.

When programming Ada my thought process is relatively close to C (but with proper modules and generics). From what little Rust I've used, it was more like an ML—specifically more usage of higher-order functions and discriminated unions (which Ada supports, but is 100x less convenient than ML, but still 10x better than C's struct { enum { } what; union { } val; }; pattern).

In general, memory management is more manual in Ada than in Rust as well. Actually, Ada is technically less safe than Rust since you can use Ada.Unchecked_Deallocate to circumvent the accessibility checker and Ada doesn't have an equivalent of the unsafe { } block. But you almost always use RAII and memory pools and Unchecked_Deallocate for C types gets hidden in a RAII wrapper. Additionally, fat pointers are opt-in (though roughly as inconvenient as thin pointers), which can be useful for very low-level code. I believe Rust's pointers are always fat?

Also I went ahead and checked Rust performance again. I didn't realize you'd gotten so fast. Ada, like FORTRAN, does not allow pointer aliasing by default (there's an ‘aliased’ type qualifier though), so theoretically it can generate faster-than-C. Embarrassingly I don't actually know how alias analysis works in Rust, so maybe you have this advantage too.

It's interesting, there's a weird dual-character to it. Some people see it like you do, some people see it as a low-level language with some extra features. :)

One thing that you may not realize, talking about HOF and such: LLVM is very good at compiling them down. If you use a closure that doesn't actually close over anything, it will get turned into a regular old function, for example. Which means it can be inlined...

Rust pointers are not always fat. Slices are, but &T is a regular old pointer, as far as the assembly goes.

&mut T automatically gets `restrict` applied to it, basically, we do a lot of aliasing stuff as well.

Thanks for elaborating :) I should spend some time with Ada.

You may want to check out hypertalk, algol, prolog, and simula 67 too.
The obvious problem with the "The second is to ask what is most useful" approach is that if you ask 1000 C programmers, 990 are not clueful enough to give any sane answer and will pollute your answer database with noise, and of the remaining 10, 5 are language lawyers and favor approach #1 anyway, and the other 5 will give 20 different answers (Note: above numbers completely made up).

So then the compiler developers give up and implement stuff 1) according to the standard document 2) such that performance on SPEC cpu is maximized.

Indeed. The only realistic way out of this conundrum AFAICS is some kind of consensus around "friendly C"/"boring C" or such that compilers then can implement, whether it's an annex to the official C standard or some other kind of spec.

That, or then everybody switches to Rust. :) (hey, I can daydream can I?).

And everyone will have to accept the performance loss--and there will be performance loss. This is acceptable for many applications, don't get me wrong. But it will be a tough sell.

If you want to get people using a Friendly C, you need to start convincing the biggest users of C and C++ that they should give up performance for a simpler dialect of the language. Like politicians responding to voters, compiler authors respond to what their customers demand. Up to now, their customers have demanded performance. It's not their fault for listening to them.

Performance of real code, not micro-benchmarks, won't change.

And this can work both ways. Friendly C would almost certainly forbid non-compatible type pointer castings, which would actually enable additional optimizations.

> Performance of real code, not micro-benchmarks, won't change.

I doubt that. Autovectorization certainly does help performance of real code, for example.

> And this can work both ways. Friendly C would almost certainly forbid non-compatible type pointer castings, which would actually enable additional optimizations.

I'm not sure what that means; could you elaborate? That sounds like strict aliasing, which is one of the things "Friendly C" is opposed to…

I believe you have been using a definition of Friendly C, as seen here: http://blog.regehr.org/archives/1180.

I wasn't aware of that and my idea of it is different.

Aliasing rules help the compiler decide what values have to be reloaded. The stricter those rules less chance there is for pointers to alias the same memory.

No argument there. I'm a fan of strict-aliasing optimizations. :)
For possible clarity, despite arguing against "unwanted" optimizations, I'll mention that I'm also in favor of strict aliasing. That's the sort of optimization I like, especially if it's "opt-in" by specifying -std=c99 or -std=c11. It's unlikely to happen and probably not wise, but my personal preference for future standards would be to invert the "restrict" paradigm, and assume that even same-type writes never alias unless "may_alias" or some-such is specified.
A lot of real-world projects turn off strict aliasing, such as the Linux kernel. It is just too difficult to think about every possible issue that could happen when doing a typecast. Autovectorization doesn't tend to work well anyway. In most cases where it truly matters, people use inline assembly... for example, in CRC calculations code, tight inner loops in math libraries, etc. If you really want autovectorization, "restrict" is a better solution which doesn't require breaking the world to implement.
If you want to get people using a Friendly C, you need to start convincing the biggest users of C and C++ that they should give up performance for a simpler dialect of the language.

Why would would C++ programmers have to give up anything? You may notice that the frequent complaint (and title of the article) is "undefined behavior in C", and the hypothetical replacement language is "Friendly C", not "Friendly C++". From the point of view of most who are troubled, rightly or wrongly, C++ isn't considered relevant to the problem or the solution.

I think part of the "divide" is that compiler writers (and probably C++ programmers) are more likely to lump C and C++ together. This makes sense, as C++ is mostly a superset of C, and since many of the optimizations being questioned operate at the level of internal intermediate language that's the same for both. But many C programmers don't view them as being the same language at all, and have no opinion on how C++ compilers should operate.

Perhaps what needs to be questioned is assumption that the needs of C and C++ programmers can effectively be served by the same compiler?

(I realize I've responded strongly to two of your comments in a row. I care strongly about the issue, but don't intend this to be an attack. Rather trying to convince you that you're wrong, my goal is just to understand what produces the gap between our viewpoints.)

Do you have any UB-sensitive optimizations in mind that affect only C but not C++, or vice versa? I can't really think of any.
Do you have any UB-sensitive optimizations in mind that affect only C but not C++, or vice versa?

I presume there are some, but they aren't the sort of thing I have in mind. I'm talking more about the expectations that the users of each language have. For whatever reason, C programmers are much more likely to complain about "broken" optimizations than are C++ programmers.

Of course this isn't absolute, but I think it's undeniably a pattern. I'd guess this is because C has a heritage of being "portable assembly", and thus many programmers have an expectation of a 1:1 correspondence between the code they write and the finished product, and are startled when it doesn't.

In the case of explicit null checks being removed and loops being removed resulting in memory not being zeroed, I think they have a point. Perhaps there is some way to apply different levels of optimization to the code that the programmer writes versus code "generated" code?

I don't think it's realistic for friendly C to replace the current C outright. At best I guess one would get -std=friendly or -Ofriendly or such. And HPC users could continue using tbaa, loose fp semantics etc. as before.
It is already common for serious C programs to use -fno-strict-aliasing and less commonly -fwrapv. The programmers are already convinced.
> Up to now, their customers have demanded performance. It's not their fault for listening to them.

I disagree - it is almost entirely their fault for listening to them. Customers usually don't know what they really want. "Faster horses". The whole point of having an oversight committee is to steer the language, and they failed at the task.

Rather than steering towards sanity - which C/C++ badly need - it was steered towards performance at pretty much all costs. The result is a broken language: it is extraordinarily difficult to write correct programs, and assure that a program will continue to compile as intended many years in the future. Sanity, correctness and fragility are exactly the kind of things which oversight should have tackled, but instead we got the opposite.

I say this as a decades-long user and fan of C/C++. But I've come to realize recently that it's a lost cause, because those in the position to fix the language are powerless against the wishes of their customers.

Won't -O0 pretty much solve this for anyone willing to trade performance for a higher tolerance for UB, though if you are in that group there are probably better language choices than C.
I'm in way over my head and skimming comments to see what I can learn, so my question is a bit tangential: why can't everybody switch to Rust? Are you commenting on the difficulty of convincing everybody to rewrite their entire code-base or on some technical aspect of the language?

Thanks!

With the largish disclaimer that so far I'm quite a Rust n00b, I'm loving it so far! So per se I don't think there are any technical deficiencies in the language itself compared to C.
Okay, cool, that answers my question :)

What resources are you using to get into Rust? I've been curious for a while, but I'm not sure where to start.

Nothing special I guess. Mostly the rust book and reference manual + duckduckgo search which bring up the odd blog post or SO question.

But I think it depends a lot on your previous experience. If you have experience of other languages, particularly c/c++ and some functional style thing like Haskell there isn't that much of a mental barrier IMHO.

This narrative of `fault' has two very serious problems.

That can be said almost any time people are trying to find fault. Better to just look for a solution, and not worry about whose fault it is.

What are the particular compiler behaviors or optimizations they would disable?
A quick look at the first example provided suggests the restriction on comparing pointers to internal members of distinct structures.
How about allowing pointers to be compared against NULL after they have been passed to free(): "free(x); if( x!=NULL ){...}" That was completely harmless (and a useful idiom) for 45 years, and now suddenly it is flagged as UB and has to be changed. Why?

Wouldn't it be great if "memset(0,p,0)" was a harmless no-op? It was for time out of mind. But no more.

For bonus points: Can we have a #pragma that tells the compiler to abort with an error if the target machine uses any representation for signed integers other than twos-complement?

Why? Because programmers learned that it is correct, even though it wasn't.(Or at least it stopped being, in C99 an on.)

Here is a car analogy. It is like learning to drive and thinking that you are allowed to drive over a yellow-turning-to-red light. However the rules say, you should stop if you are physically able to. In reality almost everyone tries to get over than yellow. On a rare occasion they get spotted and pay the fine.

C strives for maximum performance, it will not check things for you if you don't ask it to. Having a library function that performs those checks for everyone, would go against that rule. Why would someone else have to pay the performance penalty for you? Write a wrapper or a macro that performs the check, couple of lines, it is that easy.

If you strive to write portable C code it should work regardless of signed representation. C defines all the range macros for types, and gives you types that guarantee certain ranges. Assuming you use those types and macros, I'm really curious what incantations, that couldn't been solved in a portable manner, require you to know the signed representation.

>Can we have a #pragma that tells the compiler to abort with an error if the target machine uses any representation for signed integers other than twos-complement?

This should do it (off the cuff):

  typedef int NOT_TWOS_COMPLEMENT[(unsigned int) -1 == UINT_MAX ? 1 : -1];
In C++ use static_assert.
I don't see how that would work. Casting -1 to an unsigned type is independent of representation and will always give the max unsigned value.

C has _Static_assert.

One area that worries me that I never see anyone talk about is LTO between C and C++. How do you even reason about that?

I asked a C++ and compiler expert about this once and he told me: "I believe both GCC's and LLVM's LTO will happily cross this barrier, so it doesn't offer you any real protection from their optimizers."

There is no single standard that defines what UB is for a mixture of C and C++. There are clearly some parts of C++ that are trying to improve interoperability with C, like "standard layout" classes. The best you can do is try to simultaneously follow the rules for both languages when you mix the two.

Why not flag each function with "C" or "C++" depending on where they originate from? The linker can then perform only optimizations that are valid for the respective language.
Personally, I think this is blown out of proportion a bit - There are things to complain about, but LTO problems are really only caused by other issues. But with that said, what you described isn't really a worry:

LTO is done on the compilers internal representation of the code (IE. GIMPLE or LLVM IR). This representation is generated based on the rules of each language, and optimizations are performed on this representation instead of the original source representation. Both C and C++ (and anything else) are converted into these representations. LTO simply keeps the GIMPLE or LLVM IR around until link time, and then when the program is linked optimizations are performed over the entire representation of the program. Crossing the language barrier shouldn't be a problem, because the GIMPLE has it's own rules to follow to make sure everything still functions the same. Once you reach this point both languages are already compiled in the practical sense, they're just not actual machine code yet. I would expect however, that because of the differences there are far less optimization opportunities to be taken advantage of.

Ok let's look at a specific example then.

    // foo.c
    #include <stdlib.h>

    typedef struct { int x; } s;

    s *make_s() {
      s *ret = malloc(sizeof(*ret));
      ret->x = 5;
      return ret;
    }

    void free_s(s *val) {
      free(val);
    }


    // bar.cc

    // This class is standard-layout and matches "s" from C.
    class C {
      int getX() { return x_; }
     private:
      int x_;
    }

    extern C* make_s();
    extern void free_s(C* c);

    int main(void) {
      C* c = make_s();
      int ret = c->getX();
      free_s(c);
      return ret;
    }
C++ says you can only call a method on an object whose lifetime has begun. Can C begin the lifetime of a C++ object?
> Can C begin the lifetime of a C++ object?

Yes. C++14 Standard §3.8p1:

...An object is said to have non-vacuous initialization if it is of a class or aggregate type and it or one of its members is initialized by a constructor other than a trivial default constructor... The lifetime of an object of type T begins when:

— storage with the proper alignment and size for type T is obtained, and

— if the object has non-vacuous initialization, its initialization is complete.

Since class "C" is standard-layout and has a trivial default constructor (in this case none at all), assuming that malloc allocates memory that is suitably aligned for "C", the function make_s returns a pointer to an instance of C++ class "C" whose lifetime has begun.

Edit: Thanks for asking btw. I use similar code in a project of mine but never actually checked whether it is actually standard-conforming.

(comment deleted)
"But sadly it seems that the notion that our most basic and widely-used programming language should be one that's fit for programming in is not yet fully accepted."

Ouch. I usually hear that about C++, not C. =[

My guess is that many of the optimizations causing increasing trouble were introduced to improve C++ code performance in pursuit of the "zero-cost abstraction" goal. But many are just as applicable to C (and may even be below the level that a compiler like gcc or clang knows the difference).
The whole "zero-cost abstraction" thing puzzles me. Abstraction is a means to an end, not an end unto itself.
We want abstractions to make things easier to do, but we don't want to pay for them with CPU cycles... that's what a zero-cost abstraction means. It's not about adding in useless abstractions. The ideal is software that is faster to write and easier to understand and maintain, while offering uncompromised execution performance.
C is designed to be capable of running of vastly different architectures, allowing the programmer to use that flexibility when it actually isn't needed, or is harmful.

I think the C coding mindset should be this: if an approach requires code that isn't unambiguously defined, change the approach. If this means more boring coding, to circumvent cute tricks, so be it. If you need that extra 5% speed, use assembly instead of bending C.

The problem is that people often don't even know what is UB and what is not. People don't even always know what is portable and what isn't, to begin with. Some even seem to believe that "it's written in C, so it's portable".
I strongly agree. There is a serious lack of respect of the C rules even among those who teach and this is passed onto their students. It is really hard to convince them to change ways after that.

A couple months ago, as a curiosity, I watched a few videos in a series on programming in C in Windows environment. The teacher was a serious programmer, but the first thing that went out the window was strict aliasing. After that assumptions of integer sizes and range started creeping in. It came apparent that the teacher knew C, but only superficially, signedness, integer promotions and usual arithmetic conversions were treated like a nuisance. If the code compiled and ran, it was good. Those videos were the first C programming experience for at least several hundred people.

Amazing that in 2016, the best programmers in the world have not chosen to solve the "display mailing list archives on a webpage without terrible line wrapping" problem yet.
It was hard to think about, it should be hard to read
The issue of creeping optimizations causing problems far in the future is one of the reasons that I suggested using unsigned intergers in C as much as possible:

"Compiler authors will likely support this as long as possible, but the peer pressure of needing things to go faster and faster will likely push them to exploit more and more undefined behaviour to their advantage in the future. Their argument will be: 'After all, who has sympathy for those who don't follow the standard?'."

From: http://blog.robertelder.org/signed-or-unsigned-part-2/

Lots of people disagree with me, but its nice to have less UB issues to worry about far in the future.

I don't know if it's because I convinced myself a long time ago that I'll never be smart enough to write correct C/C++ code (though that hasn't (yet) stopped it from being my primary development language) or if it's because I just because I'm a secret masochist, but I disagree with this proposal/suggestion.

Regardless of who is right and who is wrong in this matter, I think if everyone took a step back we could at least agree that it makes absolutely no sense to fix this on a (single Linux) distribution level. For Debian to configure/patch compilers on their platform to "narrow" undefined behavior is insane and ineffectual. Software isn't "validated" on/against a particular OS, it's validated on a compiler basis.

Breaking this assumption introduces a massive schism. While Debian is an amazing distro with plenty of clout (I'm a FreeBSD guy, but Debian comes second), it's terrifying to imagine a new generation of "cross-platform" C/C++ software that can only be verified working on Debian (or with Debian's fork/re-configured compiler). We've come so close to making truly cross-platfrom C++ code a reality (even bringing Windows, I repeat WINDOWS, into the fold) with C++11 (and the subsequent releases) and it's, in my humble opinion, utter folly to try and change the way code will fundamentally compile depending on the distribution you run.

If Debian cares, make a proposal to the C++ committee, bribe^H convince members to see their way (or threaten^H blackmail^H show them the dangers of continuing down the road they're on). Heck, fork C/C++ and call it E or C+++ or c-safe or something - or more reasonably - write a tool to convert C to rust or D-without-the-standard-library and announce only tools in that/those languages will be allowed in the standard distribution. But for Heaven's sake, please don't try to redefine C.

Or they'll force the compiler writers' hand. See: EGCS.
If you read the thread more thoroughly, Ian Jackson is actually more on the side of Debian leaving things the way they are upstream, while others are proposing that Debian enable Link Time Optimization for the sake of performance.
Isn't that what Ian said in TFA?
"even bringing Windows, I repeat WINDOWS, into the fold"

I am not a C++ expert but have tinkered with C++11. How will portable C++ work with the Windows UTF-16 (wchar_t) and other systems using UTF-8 (char_t)?

Is there a way to have standard C++ portable across Windows' UTF-16 and !Windows UTF-8 without #ifdef'ing a char wrapper?

edit: UTF-16 not UCS-16

Modern versions of Visual Studio allow you to configure your project as one of ASCII/UNICODE/MBCS. MBCS (multi-byte character set) is UTF8 compatible. TCHAR becomes char (instead of the usual wchar_t) and most APIs you call via their once-ANSI names (e.g. CreateFile being #ifdef'd to CreateFileA) will actually accept UTF8 instead. Unfortunately, APIs that were created when ASCII was declared dead and without an xxxxA counterpart still need manual mapping to the xxxxW functions instead (which is just plain stupid on MS' behalf).
#ifdefing a leaky abstraction that can be either UTF-8 or UTF-16 would be pretty horrible to work with.

For a more workable approach read this article, especially the section "How to do text on Windows".

http://utf8everywhere.org/

Glossary: UB = Undefined behavior LTO = Link time optimization
And ASAN (the Address Sanitizer). I always have them turned on for development builds (slowdown of about 100%). Only release builds have them switched off.

Recently: Use-after-free of a memory block. ASAN told me:

1. Where and in which thread the use-after-free occurred (including stacktrace).

2. Where and in which thread the memory was deallocated (including stacktrace).

3. Where and in which thread the memory had originally been allocated (including stacktrace).

Fixing the bug took about 30s with that info.

I agree with this wholeheartedly.

I can see the C language being forked into the one we are getting and the one users actually want.

I'm getting bit annoyed about these sorts whiny posts that float up occasionally. Here is a simple three step program if you don't like what C language is today:

1) Write your own damn spec with no "problematic" UB. Hookers and blackjack are optional

2) Write a compiler for your shiny new language.

3) Start writing/porting code to your new language

There you have it, UB problem solved once and for all.

I'm not sold over on Regehrs work, but at least he is doing something with his Friendly-C proposal. This has the added benefit that we get something more concrete to discuss and debate about instead of vague "optimizations breaking my code are bad".

It is completely unfeasible to try to turn back time on C and somehow magically make compilers deduce programmer intent from some random crap that you throw at them. Like it or not, computers are based on rules, and standards are the best way we have to establish those rules among large number of parties.

Designing your own language isn't even necessary. There's Rust, Ada, Modula, Pascal... if you're feeling masochistic, use COBOL ;-)
The Wikipedia page for COBOL list several places where programming constructs lead to undefined behavior.
The problem is not just with "what C language is today", but with the fact that the use of undefined behavior allows future optimizations to possibly change how your code that was written today, tomorrow, or years ago, compiles and runs in the future.
There has been very little new undefined behavior added to C recently. The only one I can think of is INT_MIN % -1, but most people seem to think that was a good idea. All compilers were already treating it as UB, even though it wasn't.

http://blog.regehr.org/archives/175

I'm a digital hardware (chips) guy. We use compilers (synthesis tools) and normally won't get a second chance to recompile if the logic gates in our silicon chip are wrong as a result of compiler bugs or misinterpretation of source.

We automatically distrust the compiler (synthesis tool) to do the right thing. You formally prove the 'compiled' output (logic gates) that will be manufactured matches with the source code of the design (verilog/vhdl) using tools written independently to the compiler.

This isn't easy, and I know the problem space is larger, but does anyone ever do this for software?

SQLite is usually (and correctly) held as an example of thorough software testing: https://www.sqlite.org/testing.html

Perhaps you could offer a sense of how this compares to the hardware testing practices you use?

Typically in hardware design you'd use combinational equivalence checking (CEC) to formally prove that a synthesized design is correct. See https://en.wikipedia.org/wiki/Formal_equivalence_checking. In CEC you use a SAT solver to prove that a new (synthesized and optimized) circuit is equivalent to some reference model (aka golden model) which you know is correct. So instead of relying on just testing (which is incomplete) you have a formal proof that your design is correct. Of course you still have to trust that the SAT solver works correctly... :-)
In hardware design you typically test functionality through logic simulation or emulation (effectively running the code in a computer simulation or fpga), use test harnesses, look for code coverage, run unit tests, random code fuzzing, code assertions etc. You might also do formal checks for some assertions to e.g. avoid deadlocks.

A secondary check is that the source that you functionally tested is logically equivalent to what you manufacture. This is where you are not checking your code, but the issue is trust of compiler/ compiler optimisations in synthesis. It needs to be redone if you recompile - that's the step I don't really ever see in software development - if I use a different compiler option or underlying instruction set architecture to the SQLite Dev team, do I still trust my binary?

Of course the level of paranoia is far higher in hardware where it costs multiple millions of dollars to crank out a new spin of a chip!

> if I use a different compiler option or underlying instruction set architecture to the SQLite Dev team, do I still trust my binary?

If that is critical, you can join the SQLite Consortium Membership for $75K/year and access to the test suite. There's also an option to pay SQLite developers to "run TH3 on specialized hardware and/or using specialized compile-time options, according to customer specification, either remotely or on customer premises." The TH3 test harness is an aviation-grade test suite.

The level of paranoia for aviation software is also rather high.

I've seen that "no second chance" thing personally and it's hilarious to me. WTF? I would hum "Mars The Bringer of War" to FPGA guys at times because of this ( it's the theme used in "The Right Stuff" at Those Times in the movie ).

In the general case, no. The economics of software don't favor it. There are tools that inspect code and make helpful suggestions.

This is why you can't trust your synthesis tools, BTW.

But much worse in software is the cultural ... "zero" ( as in a zero in a filter) about the axis of provability in general. It's a point of despair.

I can tell you that in multiple cases, I was able to represent the "core" logic of systems much that I could build a test rig around it and do exhaustive testing ( with the caveat that it's only as good as the test framework ). Permutaitons are reasonably cheap these days. But to do this, you must nearly eschew the us of third party code and in cases, even large parts of standard libraries.

But the standard answer is to despair and moan "it's impossible." The "prove it correct" people and the "git 'er done" people are two different tribes.

> This isn't easy, and I know the problem space is larger, but does anyone ever do this for software?

Typically the state space is simply too large to be practical. It is, admittedly, how a lot of verified software is done (because there are so few verified compilers), though often then the source-code to assembly correspondence is verified manually, which limits possible optimisations.

The problem you describe is one of unreliable compilers, here the problem is that the C standard allows a few undefined things to cause an avalanche of undefined behaviour. What the compilers do is totally correct according to the standard, but not at all what programmers expect.
The correctness proof of the seL4 microkernel supposedly makes no assumptions of the compiler and verifies the binary output. I don't know the details.
There's SPARK (http://spark-2014.org/) that's an Ada dialect with verification features, and there's frama-c, doing something similar for C (http://frama-c.com/).

In both cases the compiler is pretty much part of the trust base (which is a problem because they're annoyingly complex), but the issues discussed here are declared invalid by the subset (ie. you mustn't use statements that may lead to undefined behavior).

For seL4, mentioned in another comment, there was proof that interesting properties of the high-level code and the low-level binary were equivalent. That only works with a relatively static compiler version and for some optimization levels (anything that optimizes too globally will seriously mess up such attempts of showing equivalence), but it takes the compiler out of the trust base.

So the real argument is whether LTO is on by default? Because if it's the source of trouble and it's off by default, then there's only trouble when you choose to use it.
(comment deleted)
For coreboot, I started making gcc (we use our own toolchain for various reasons already) emit some link-time error inducing symbol, because their current approach (__builtin_trap()) is nice for undefined behavior in userspace programs that can segfault, but not for firmware that silently hangs.

https://review.coreboot.org/14364

On the contrary, I'm currently implementing LTO in our project because it allows to uncover and fix the whole slew of latent problems related to ODR violations. At the same time I'm enabling UB sanitizers, so I'm confident that LTO will actually make our code more robust and reliable. The usefulness of LTO goes beyond better optimization, and it should not be ignored.