10 ms·
I think it’s worth emphasizing that the C spec’s love of undefined behavior—if you do X by accident, anything can happen—and the apparently massive amount of me
by dgreensp 3y ago
I think it’s worth emphasizing that the C spec’s love of undefined behavior—if you do X by accident, anything can happen—and the apparently massive amount of memory-unsafe software that has been written that will just allocate 16 bytes on the stack and then read from a file descriptor until it encounters a null byte… are examples of things that aren’t considered remotely sane or reasonable to a modern programmer or language designer. Any vague notion of “unsafe” in the context of a modern language—like saying well, if there’s a syscall, maybe something unexpected could happen—doesn’t compare to the bad decisions made by C and C programmers/culture that affect us today because we still use C code in our things.
The deep, idiosyncratic flaws of C trace back to “worse is better.” Few people remember or look up what “worse is better” actually meant. Wikipedia wrongly says it’s a “less is more” sort of thing, and some people think it’s about not being a perfectionist.
But actually… actually actually, if you read the essay, it says “worse is better” means (paraphrasing) it’s more important that C compilers be easy to implement than easy to use. Also, it is more important that the implementation—or the design of the implementation—of a piece of software be simple than that it be correct. It is more important that it be simple than that it be consistent. It is more important that it be simple than that it be “complete” (for example, handle edge cases; it just needs to work in “most situations”). This is not just anti-perfectionism, it is an objectively terrible set of engineering values. But there were so many different kinds of computers and operating systems back then—you didn’t even know if a byte was 8 bits—that it helped a lot that C compilers could do whatever they wanted in many situations. And making it easy for C compiler implementers enabled C to spread far and wide. It was a very different world, and just being able to write in a higher-level language than assembly on a particular computer was a big deal.
- nullc 3y agouh. No. Rust unsafe gives rust behavior a lot like C. If you at all break the rather subtle rules, then essentially anything can and will happen. So for example, there was recently a thread where someone had code that checked if a value was in range to safely coerce it directly to an enum then did so. But because of eager evaluation of an argument the unsafe cast happened first. From this the compiler reasoned that the variable was preconditionally range constrained to always be in range and it optimized out the in-range test (which itself was not unsafe code). This is a classic C bug where someone implements an overflow check that itself can overflow, causing the branch for overflow to get optimized out. But at least in C the simpler syntax at least made it clear that the triggering code got executed first. The more complex rust syntax obscured that. Rust has improved the situation by narrowing the cases where you can get into this trouble, but on the other hand it adds a lot of other complexity that contributes to faulty code (and a nearly mandatory packaging ecosystem which is a security nightmare-- it's the norm for even simple rust utilities to pull in a million lines of unauditable (just by bulk) third party code, including multiple HTTPS libraries). As a result, I don't think it can be taken for granted that rust as a whole is an advancement in software integrity-- it may be, but it's something that ought to be formally studied. In some cases rust might be replacing memory safety bugs with an even greater number of other defects which, depending on the application, may be worse. (not everything is an internet exposed service where hacks are the only failure of consequence and where input really should be assumed to be intelligently adversarial.) In any case, "break the rules and all bets are off" is an issue that likely will continue to exist in any performant language. Automatic code generation will generate stuff with awful performance unless an optimizer goes through and eliminates 'impossible cases', but optimization isn't possible unless the compiler can assume the rules are followed.
- Ar-Curunir 3y ago> As a result, I don't think it can be taken for granted that rust as a whole is an advancement in software integrity-- it may be, but it's something that ought to be formally studied. In some cases rust might be replacing memory safety bugs with an even greater number of other defects which, depending on the application, may be worse. I’m sorry, but without any supporting evidence for this claim, this is just FUD. Everything that we’ve seen in case studies of people reimplementing stuff in Rust indicates that memory safety and logic bugs are derived compared to something like C.
- nullc 3y agoCitation welcome to those case studies, because I've not seen them. It's on the advocates of rust to establish that it makes things better because it absolutely isn't unambiguous. We really seem to be in the stone age in terms of what practices lead to higher quality software. We still have people who chant "goto harmful" against one simply forward jumps to on-error-cleanup code, yet still litter their C++ and java with exceptions which are a less safe and less clear version of the same thing. I've personally found the rate of embarrassing errors in simple rust software is increased over comparable C code, but I freely admit that this experience is far from a formal study and may well be due to the lack of problem-domain competence or general haste in people participating in culty "re-implement in rust" exercises (and where the rust code is far more often someone's "learn rust" project). And at least where security w/ untrusted input is a concern the nature of the rust bugs is preferable to the bugs in comparable C code, but as mentioned that doesn't apply to a lot of software. Another data-point is that the vast majority of firefox crashes I experience now are rust panics, even though the amount of rust code is small compared to C++ code. It's hard to reason from that however, since it can be said that the rust code is more complex and more heavily used than the bulk of the rest.
- klabb3 3y agoI appreciate your comments in this thread. They are a healthy skeptical technology-neutral take unaffected by novel-paradigm-dogma – in this case about Rust, but it really applies to any software practice. I have nothing against Rust and I believe it has advanced the space of mainstream imperative languages significantly, particular in high performance low-level coding. But you have to always keep an eye on the horizon. > We really seem to be in the stone age in terms of what practices lead to higher quality software. I agree. What I always come back to is simplicity, or reducing total cognitive complexity. The only way I know of that actually consistently works, is modularization (in the most liberal sense of the word), in order for our human brains to work in a reduced problem domain at any given point in time. So whatever languages, protocols, tooling and design patterns help with that, I lean into, although this is (currently) a subjective measure. On some problems, it’s clear Rust is amazing in this respect, taking away certain worries so that you can focus on “what matters”. But this is not a truism in all domains, because the issues that Rust addresses are merely a good list, not an exhaustive one.
- zozbot234 3y agoIt's not like PL/I didn't allow for similar flaws, so "Worse is better" is neither here nor there. At least C was simple enough to learn for most programmers. It's still generally the case that a simple approach is more likely to be correct than an overly complicated one for which it's not even clear what "correct" means.
- musicale 3y ago> It's not like PL/I didn't allow for similar flaws, so "Worse is better" is neither here nor there. PL/I is one of the reasons that Multics was not plagued by buffer overflows: "The net result is that a PL/I programmer would have to work very hard to program a buffer overflow error, while a C programmer has to work very hard to avoid programming a buffer overflow error." [1] [1] P. Karger, R. Schell, "Thirty Years Later: Lessons from the Multics Security Evaluation"
- quelsolaar 3y agoC gives you a level of control and responsibility not found in other languages. That's a choice, not something that is inherently worse. It may be worse for what you are doing. Most people don't value the level of control that C gives you and would rather chose another language and that is fine. But having a language available with this level of control is valuable, even if few people chose to use it. Most UB in the C standard is there for a very good reason.
- kaba0 3y agoC is not a particularly low-level. It has no (standard) way to control vectorization, stack usage, calling conventions, etc. It just had an insane about of money spent on making its compilers optimize better.
- quelsolaar 3y agoYou are right, but no language has tried to claim the space of giving the user more control, so C remains the lowest level language we have that is portable. I think there are lots of opportunities in this space, but i don't know of anyone working on it.
- kaba0 3y agoC++, Rust, Zig all are lower level, due to having control over vectors.
- pjmlp 3y agoWhatever people it is C for low level coding, in reality are compiler specific language extensions, not available on ISO C. All languages can have such specific extensions and many do.
- armchairhacker 3y ago> If you read the essay, it says “worse is better” means (paraphrasing) it’s more important that C compilers be easy to implement than easy to use. Also, it is more important that the implementation—or the design of the implementation—of a piece of software be simple than that it be correct. It is more important that it be simple than that it be consistent. It is more important that it be simple than that it be “complete” (for example, handle edge cases; it just needs to work in “most situations”). This is really ironic considering C++, whose compilers may literally be the most complicated compilers to ever exist, and none of them implement the full (C++23) language spec. What kind of maxim does C++ go by?
- eschaton 3y ago1. If you don’t use a language feature in your code, your code shouldn’t pay the cost—no matter what else might be helped, enabled or prevented by that. To this day compilers let entire features like exceptions and RTTI be disabled entirely rather than accept that there might possibly be some costs worth imposing. 2. Binary compatibility and dynamic linking don’t matter at all, and shouldn’t ever be considered in design or revision of anything language-related. 3. All areas of computer science and engineering are open to being a part of the standard library and existing language and standard library authors are the best judges of what should be in the standard and how it should be implemented than anyone else, including platform vendors or subject matter experts.
- fulafel 3y agoZero cost abstractions & backward compatibility over implementation complexity, user friendliness or safety.
- wavemode 3y agoSoftware was significantly simpler in those days. I don't find it strange that they took a simplified view of software engineering. Specifically because those exact same simplified views still exist today - talk to people who've never worked on large complex systems, and you will usually encounter similar "anti-perfectionism". People adopt simplified engineering practices when working on simple software by themselves, compared to when working on complex software within a large team.
- trimistermota 3y agoOne of the reasons C became popular because the compiler was actually reasonable to use on the hardware of the time.
- stjohnswarts 3y agoYeah it's hard for a lot of programmers to context switch back to when a megabyte a HUGE amount of memory (or even 640kB). I think everyone should play around on arduino uno or even smaller memory cpus to get some perspective. You didn't have gigabytes of rams to analyze all corner cases, data races, etc. during compilations.
- moody__ 3y ago>it is more important that the implementation—or the design of the implementation—of a piece of software be simple than that it be correct. This is true and when your compiler actually abides by these values it is shocking how many issues just go away. The problem is that gcc and clang are nowhere near a simple implementation, asking the question of how exactly gcc or clang arrived at some given assembly for some given input is a really complicated answer. So much so that it makes me say that the relationship between the programmer and the compiler becomes adversarial. It would be one thing to just have very complicated optimizations but the whole "undefined behavior lets us to anything" approach is what makes it unbearable. People can (and do) point at the C spec for fault of this and it is true that if the C spec was more strict then these compilers would not have the free pass to do these crazy miscompilations. However there is nothing stopping these compilers from just not doing that, there is nothing stopping them from just defining their own sane behavior for what the C spec defines as undefined behavior. It is no longer the case where we have a dozen or so C compilers that people need to target with their programs, the spec is not the bottom line anymore. The Plan 9 compilers are the perfect example of getting this right. They define sane behavior that a reasonable programmer would expect for what the C spec calls undefined behavior. They are not gargantuan, their optimizations are not crazy. It is generally easy to understand how the compiler ended up with the assembly you see in the binary. Yet they are competent enough to selfhost an entire OS. The insane complexity of these other C compilers is simply not mandatory. They are not perfect of course it is still possible to write bad code but the result is no longer pathological, which is a giant help when you're actually trying to figure out what is going wrong.
- spease 3y agoHow do the Plan 9 compilers compare to gcc/clang when it comes to performance or portability?
- kazinator 3y ago> piece of software be simple than that it be correct Here is the thing: which of these is more plausible or at least less far-fetched? A. Write a program that is not correct now, but will eventually be. B. Write a program that is complicated now, but will eventually be simpler. :) Simplicity isn't something you can leave out now and add later, yet correctness can often be treated that way. Even a shop that values correctness above all else still does debugging. (If not code, then debugging their proofs, and debugging whether their formal specification actually implement the functional requirements).
- jules 3y agoUndefined behavior is critical for performance. Without undefined behavior, C compilers would not be able to optimize at all. You'd be running everything at -O0 or worse.
- jiggawatts 3y agoUndefined behaviour enables only a fairly small set of optimisations. There's a large set of optimisations that can be implemented completely safely without having to make such dangerous assumptions. Other programming languages do this all the time, it's not just C/C++ that have optimisers!
- jules 3y agoThis is simply not true. Almost all optimisations rely on undefined behavior, see https://news.ycombinator.com/item?id=38760475 https://news.ycombinator.com/item?id=38760475 Note that I am not talking about UB like signed integer overflow. Removing that would slow down programs by a couple of percent. The important type of UB is pointer provenance. This ensures that e.g., writing to a random memory address is UB.
- jiggawatts 3y agoAlmost all optimisations in C/C++ compilers depend on undefined behaviour, because practically no behaviour is defined! The trick is to define behaviour, which is what other programming languages do. E.g.: in both C# and Rust, integers have fixed sizes. A C# Int32 is equivalent to a Rust i32. Only God knows what a C/C++ "int" is. It could have 17 bits and use ternary.
- jules 3y agoAs I said, this isn't about ints, it's about pointers. What behavior will you define in C for writing to a random memory address?
- renox 3y agoWhere's your proof? Rust has far fewer UB than C yet its performance is comparable to C.
- fulafel 3y ago"Worse is better" is much older then "undefined behaviour". Undefined behaviour was invented for C standardization, when C was already mature and had been out of the Unix childhood home for a long time. For example, many UB semantics in the standard come from allowing for C ports for strange non-unix hardware, and making bold decisions when developments in compiler optimization state of the art ran into underspecified corners of C semantics.
- jholman 3y ago"Worse Is Better", as a description of a set of engineering values embodied by C, is from 1989. I'm taking your word for it that UB was invented for C standardization (I have no knowledge of that history, and your claim seems plausible), and I'm going to say that probably they didn't invent it in the last year of the seven-year standardization effort, so probably UB is older than Worse Is Better.
- mattpallissard 3y ago> It was a very different world, and just being able to write in a higher-level language than assembly on a particular computer was a big deal. It was different then, but not by too much. Our tooling, languages, etc, has improved yes, but the underlying hardware still exists and occasionally the general solution provided by our improved tooling is sub optimal for our use case. I actually _like_ where C has wound up. I have way better static analysis tooling at my disposal than I had 15 years ago and I can use a "memory safe" language like python or OCaml and say "hey, I know better here", think carefully, and push the memory safe compiler out of my way.
- trimistermota 3y agoOne of the reasons C became popular because the compiler was actually reasonable to use on the hardware of the time.