Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I haven’t seen a good explanation of the arguments against this kind of thing. Who doesn’t want to fix the UB weirdness? Are there really many people worried it will completely break optimization? If so I’d love to hear about it.

Even if you don’t agree with this proposal, or with Regehr’s, surely a reasonable response would be “okay, yeah, it would be good to do something along those lines, but not that; how about this?”



> I haven’t seen a good explanation of the arguments against this kind of thing. Who doesn’t want to fix the UB weirdness? Are there really many people worried it will completely break optimization? If so I’d love to hear about it.

If you're not a compiler writer, you probably have a mental model of undefined behavior that works like this:

    if (expr->invokes_undefined_behavior()) {
      // Yay! Silly user!
      shit_on_users_code();
    }
Sure, you probably don't think it's so brazenly stated, but the general sentiment from compiler users tends to be that all you have to do is delete that if statement and everyone's happy.

But that's not how compilers work, not in the slightest. Instead, the undefined behavior effects tend to work like a chain: you dereferenced a pointer, therefore it's now safe to assume on any subsequent path that it will always be safe to dereference that pointer, therefore we can move that later load out of that loop even though we don't know if the loop will be executed in the first place. And the reason that logic works is because we're allowed to optimize under the assumption of undefined behavior in one of those steps.

Now in some cases--signed integer overflow and strict aliasing come most readily to mind--there is a single knob you can twist to disable the undefined behavior (and most compilers let you twist that knob with command-line flags). Arguing whether or not the standard itself should dictate a different position on that knob is one thing, and it's by no means a bad discussion to have. But instead arguing "let's let you keep undefined behavior, but let's try to specify what you can do with undefined behavior" runs into the problem that it's very difficult to actually specify what somewhat-constrained undefined behavior really means. Look up the massive email threads on LLVM that try to tease what poison and undef actually mean.


Actually you should correct that as "If you're not a C compiler writer".

Undefined behavior is one of the tricks that enabled C compilers to catch up with what everyone else was doing in big iron outside AT&T walls.

Fran Allen the Turing awardee on high-performance computing and compiler research, stated this on C:

"Oh, it was quite a while ago. I kind of stopped when C came out. That was a big blow. We were making so much good progress on optimizations and transformations. We were getting rid of just one nice problem after another. When C came out, at one of the SIGPLAN compiler conferences, there was a debate between Steve Johnson from Bell Labs, who was supporting C, and one of our people, Bill Harrison, who was working on a project that I had at that time supporting automatic optimization...The nubbin of the debate was Steve's defense of not having to build optimizers anymore because the programmer would take care of it. That it was really a programmer's issue....

Seibel: Do you think C is a reasonable language if they had restricted its use to operating-system kernels?

Allen: Oh, yeah. That would have been fine. And, in fact, you need to have something like that, something where experts can really fine-tune without big bottlenecks because those are key problems to solve. By 1960, we had a long list of amazing languages: Lisp, APL, Fortran, COBOL, Algol 60. These are higher-level than C. We have seriously regressed, since C developed. C has destroyed our ability to advance the state of the art in automatic optimization, automatic parallelization, automatic mapping of a high-level language to the machine. This is one of the reasons compilers are ... basically not taught much anymore in the colleges and universities."

-- Fran Allen interview, Excerpted from: Peter Seibel. Coders at Work: Reflections on the Craft of Programming


If you're not a compiler writer, you probably have a mental model of undefined behavior that works like this

I find that very patronizing.

Yes, we mere compiler users complain about specific examples; that’s because we keep finding specific examples that break in weird ways!

But at the same time, we realize that it’s a general approach that causes trouble. As I understand it: assuming that undefined behavior cannot happen and optimizing on that basis.

I’m not arguing for specific small tweaks to fix a few known weirdnesses, or even a more general adjustment to the notions of poison and undef (as your mention of “massive email threads” suggests).

I’m arguing, first, that the entire approach seems to be misguided and not working well; as evidence, many real-world examples of code doing extremely unexpected things, and the position of the Linux kernel maintainers (surely an important group of users by any standard).

Second, that the problem seems to be largely ignored by compiler writers; as evidence, no changes or improvements, despite several proposals from outsiders. You can point to massive internal debate on email threads but it amounts to nothing if nothing comes out of it.


Since -fno-delete-null-pointer-checks works in both GCC and clang and, nevertheless, the compilers are able to do significant optimization, you might want to think it out again.


Yes, by removing the undefined behavior. The parent post differentiated this from keeping the behavior undefined but somehow constraining it, which is what your proposal tries to do.


I don't think there are good arguments against it, and I agree it would be great to fix it. But I think it's very important to understand the reasons why Regehr's proposal failed. I also think it's incumbent on new proposals to argue why their effort is likely to succeed where Regehr's did not.

If I were more cynical, I'd say that the C-standards community has simply ceded the ecosystem niche of a low-level language which has common-sense behavior, creating an opportunity for other languages such as Rust, D, Zig, etc. But I don't really believe that. Almost everything we do relies on C at the bottom, and it's pretty much the only viable way to publish an API that can be consumed by multiple languages.


> Almost everything we do relies on C at the bottom, and it's pretty much the only viable way to publish an API that can be consumed by multiple languages.

I think it's true that almost everything we do relies on the C ABI at the bottom. That's different from relying on C, and I think these are actually the same problem: the C ABI is straightforward and has been implicitly stable (whereas e.g. the Rust ABI is explicitly not stable) because the C ABI has very few types and very little information in types (aliasing, ownership, alignment, thread-safety, parameter types of functions, etc.) and the more common-sense lower-level languages are obligated to have meaningful types.

You can certainly publish a C API / ABI from a Rust library and consume it from a D program without either side knowing that the other side isn't actually written in C.

I am curious what is the next meaningful step beyond the C ABI and the C type system: even stabilizing the Rust ABI (which I would love to see) isn't going to help a lot for writing D libraries that implement them or Python programs that consume them, partly because Rust has unique ideas of things like lifetimes that don't immediately translate to other languages. What's the subset of stuff that's in all these languages' type systems that isn't in the C ABI?

I'm reminded of Vala (which I kind of miss), which was GNOME's attempt a couple years ago at a better-than-C language that was very explicitly C-ABI-compatible. It used the GObject protocols for types - is GObject the shape of thing to standardize?


> I am curious what is the next meaningful step beyond the C ABI and the C type system

The C ABI and type system omits a lot of stuff that could be useful. Some points off the top of my head:

* Vector types

* Multiple return values

* Functions with bound environments

* Coroutines

* Dynamically-sized callee-allocated structures (most notably lists)

* Distinguishing between "byte array" and "string" types

* Vtables

* Something that lets GC'd languages cooperate more nicely. I'm not sure how exactly to specify this, but this is the sort of thing we could have if we stopped insisting that everything must be in terms of C ABI.


Hm, are there any cases of two GC'd languages that interop well that were not designed to interop from day one (that is, JVM languages don't count)?

The only one that comes to mind is https://research.mozilla.org/2014/08/26/javascript-servos-on... , which is Rust (pre-1.0) and JavaScript, but that somewhat doesn't count because Rust isn't really a GC'd language. (This article was written about a year after the special syntax for garbage-collected references was removed and Gc<T> became just a thing in the standard library, and about a month before that type was also removed because it wasn't a very good GC.)

It seems to me that if you make your GC a purely refcounting system this is easier: you just need to specify how to incref and decref objects, and put a well-known function in the vtable for destroying and deallocating the object. I'd guess that's how things like GObject or Qt bindings in Python or similar languages work.


COM was an example of using refcounting to drive a richer cross-language object model. And these days, WinRT is another take on the same (and, indeed, is COM under the hood, but with a richer type system and metadata). So you can have e.g. JS interop with C# via WinRT, but only because their shared view of the world is all refcount-based.


If you codify things that some vendors (notably Microsoft) are already doing with their calling conventions, that would give you alignment specifiers, and vector types passed in SIMD registers.

I personally think ownership is a biggie. If I pass in a pointer, is it expected to outlive the call?

And buffer sizes, of course.


Most of those features are available on UWP.


> Almost everything we do relies on C at the bottom, and it's pretty much the only viable way to publish an API that can be consumed by multiple languages.

Not quite true, on Windows it is called COM, WinRT or UWP as of Windows 10. Or MSIL in alternative.

On IBM i, IBM z, Unisys ClearPath, takes a variation of what is known as language environment.

On Android you will have more luck targeting DEX than C.

On browsers better compile to JavaScript in some way.


FWIW, my personal response is "C does not carry enough information with variables to make productive high-performance programming possible; how about switching to a language that does?"

The trouble is that "carry information with variables" means one of two things, runtime checks (which often - but not always! - rule out "high performance") or types.

Rust is my personal preference here, but it's hardly the only option - Go and idiomatic C++14 are also good direct competitors, Java and PyPy can be faster than AOT-compiled code if you don't mind startup time, many applications actually aren't in need of a language that aggressively optimizes (e.g., it's relatively short logic on top of a database that someone else is writing in C), etc.


It's perfectly reasonable to argue that C is not as good as XYZ. You may even be right. But it is not reasonable to conclude from that, that it ok for C implementations to make C programming more hazardous than it needs to be. Go out and make Rust a more appealing alternative to C and good luck with it. But don't tell me C can be turned into mush because you don't much like C.


Don't get me wrong, I like C. I don't want to write production software in C any more, but I do actually like the language a lot. Le cœur a ses raisons.

C is already mush. Maybe it wasn't in the 1980s, but it certainly has been since I first started learning C (around 2000).

I'm simply responding out of rational self-interest to recognizing that C cannot be what I want it to be, and what I want it to be looks an awful lot like Rust (or Go, or Swift, or...).

C doesn't even have sum types with a language construct for exhaustive matching. That, by itself, makes C programming dangerous. For every NULL check unexpectedly deleted by a compiler, there are a thousand NULL checks that weren't written in the first place, because C has no mechanism to require a NULL check before using a pointer.


It is perfectly ok for C implementations to make C programming as "hazardous" as they want to. It's up to you whether you want to use them.

Frankly, I don't understand the UB hate at all. There is a rapidly growing space of programming languages specifically designed to give you safety guarantees and well defined behavior. If that's what you want, by all means use them. There are still those of us out there for whom even `-fwrapv` represents an unacceptable loss in performance for certain programs. There is an important and valuable niche for a language that enables compilers to optimize code without regard for saving programmers from mistakes in their code, and that's always going to be the case. Most people probably shouldn't be using it very much, but that's an entirely different issue from trying to "fix" what's not broken in C.


I'd love to see some examples of how -fwrapv makes real programs see an "unacceptable loss in performance". Are there any? I confess to doubt.


That's easy, I'm working on a computational kernel (for a scientific simulation of fluid dynamics) right now that loses ~20% performance the second you compile it with -fwrapv in GCC (the corresponding factor is slightly smaller in clang, because it's worse at optimizing it in the first place). I haven't invested enough effort in it yet to tell you if I could beat the compiler with manual optimizations in a sane amount of time, but the difference is certainly pronounced.

I haven't stumbled across anything new or unique here, it's just that most people don't write compute-constrained kernels that often these days. The optimization benefits of UB for signed integers are well known, and that's just one useful example of UB in C compilers.

And again, I'm fully on board with the notion that many people would be better off with a more error tolerant subset of what C does - and there's a lot of languages that provide just that. I'm just saying there are real costs to the change and they matter.


I love to see code snippets. Otherwise this makes zero sense. The check for overflow is super cheap on any processor you'd run fluid dynamics code on. You'd have to be running pathologically tiny loops. I have never seen any papers on this "well known" benefit. Perhaps you can provide me a link. In any event, it's interesting that you think that compute intensive kernels are self-evidently more important uses of C than device drivers, embedded systems, middleware, etc. Perhaps you'd be better off using a language specifically designed for optimized numerical codes - FORTRAN for example.


I can't release the whole codebase before publication, but if you're really serious about analyzing it, I can probably sanitize it in an evening and get you an MWE.

Regardless, apart from pathological integer math cases, these things have little to do with the cost of overflow checks. Rather, it's about the followup optimizations the compiler can do after the UB frees it from a restrictive assumption. The trivial common examples (optimizing x*2/2 to x for integers, etc.) don't really capture the impact of these things very well, because you can't demonstrate it in a oneliner, but you can derive representative examples pretty easily if you take tight-ish loops (>16 independent loads, some SIMD-able compute, a few dependent stores at the end) and switch the index types. The vectorizer is often easy to defeat if it can't assume no overflows, the cost of the check itself is irrelevant.

On your last point, I don't think compute intensive kernels are "self-evidently more important" than anything else. I think there are usecases - like scientific computing, device drivers and embedded software, to name a few - that often benefit from prioritizing programmer control over any safety net, and FORTRAN doesn't cut it for many of those uses. There are plenty of languages that give you all the safety you could possibly want. C/C++ have never been about that and I don't see why that should change. Use the right tool for the job, by all means advocate for people who don't need this level of control to use something "safer", but the notion that every tool should be blunted to some common denominator just makes no sense to me.


I'm puzzled how I could have given the impression that I am looking for safety. I just don't want the compiler to defeat programmer intentions. I don't see how your example can depend on overflow semantics. The vectorizer is free to assume there is no overflow and let the programmer suffer if she screwed up. What I don't want the optimizer to do is to prevent the programmer from checking for overflow because it can't happen in theory while it can happen in practice.That is, I am interested in prioritizing programmer control over the safety net.


Well, now I'm confused. When does the compiler defeat programmer intentions? If you specify -fwrapv and insert manual overflow checks wherever you wish, the compiler will never optimize them away and you will have as many runtime checks as you want. Programmer control is not compromised, which is good.


One of the motivating cases for this proposal is that gcc/clang, without fwrapv, will both implement wrapping on most architectures (since that is what the processor implemements) and silently remove manual overflow checks for overflow because it is "impossible" under a stupid interpretation of what UB means. Similarly, those compilers will remove manual checks for null pointers if there is a prior dereference to a pointer on the theory that dereferences of null pointers are "impossible" even when they happen.


Which is a very reasonable assumption for an optimizer. Every major compiler has switches to turn that behavior off. I'd be sympathetic to an argument that perhaps those switches should be on by default, but if you change the language standard, you're essentially saying entire classes of optimizations should be outright prohibited by a conformant compiler, with no programmer control. That's a classic case of throwing out the baby with the bathwater.


I do not see how FALSE is ever a reasonable assumption for an optimizer: " a+1 > a" is not a true statement for code emitted by gcc/clang on most architectures (with or without frwapv) Can you put one of your fwrapv deoptimizations on godbolt or otherwise show it?


Rest assured, it is quite clear to us that until we get rid of UNIX clones, C isn't going anywhere.

So the only alternative is to improve the band-aids and keep sponsoring PhDs on security research related to C.


Check Chandler Carruth's talks about UB, he already has a couple of them.


They exhibit a really badly mistaken idea of the role of the compiler.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: