4 ms·
This is really overkill. When I'm writing C, it's generally for one of two reasons: 1. Interoperability code with some library or the OS. 2. C as the closest
by rbehrends 11y ago
This is really overkill. When I'm writing C, it's generally for one of two reasons:
1. Interoperability code with some library or the OS.
2. C as the closest thing to a portable assembler there is (e.g. to implement something like the Ruby interpreter).
What I need for this isn't perfect safety; it's the ability to reason with some confidence about the code I'm writing (I may want to be able to rely on code review by merely mortal programmers or create my own tools to enforce this). If I wanted a safe, expressive language, I wouldn't use C in the first place.
But right now even something as simple as a malloc implementation is riddled with traps even for reasonably skilled programmers. The Linux kernel uses -fno-strict-overflow and the FreeBSD kernel uses -fwrapv because the price to be paid by enabling the undefined behavior of -fstrict-overflow was too high.
- pcwalton 11y ago> 1. Interoperability code with some library or the OS. If by "interoperability" you mean "binding a more modern language to a C library", no argument there. Binding to C is a legitimate reason to write C. > 2. C as the closest thing to a portable assembler there is (e.g. to implement something like the Ruby interpreter). My question is: why do you want a portable assembler? Why not write Ruby in, for example, Go? It could certainly be done; look at JRuby, for instance. The vast majority of the time, the answer to this question is "performance". Which brings us back to the point of the article. Without compiler optimizations enabled by undefined behavior, you can easily lose 2x performance or more. A 2x performance loss for MRI is unacceptable. Malloc implementations embody this even more. In fact, it's kind of hard to think of any piece of code that's more performance critical than malloc. Many large applications (apps, games) spend 10%-20% of their time in the malloc and free functions. Allocator performance is so important that Facebook and Google have invested a huge number of man-hours into these two routines (jemalloc and tcmalloc respectively). It all comes down to performance again: under these extreme contraints, malloc simply can't afford a 2x performance loss. A Friendly C that produced slower code would have little chance of being adopted by allocator writers.
- rbehrends 11y ago> My question is: why do you want a portable assembler? Why not write Ruby in, for example, Go? It could certainly be done; look at JRuby, for instance. I would in principle like to do this in another language. Go isn't that, because Go's runtime makes some very specific assumptions (about things like stack layout and how it interoperates with syscalls). I may not want to be weighed down by these assumptions. For similar reasons, you may want to eschew JRuby, as it locks you into the JVM ecosystem (which is great if that's what you need, not so great if you don't want to suffer from the poor interop with non-JVM libraries). > The vast majority of the time, the answer to this question is "performance". The answer for me is generally that C is ecosystem-agnostic (what I'd expect from a portable assembly language; practically any other language that isn't called BCPL makes more ambitious assertions about its environment). Note that when I talk about interoperability above, I don't just mean interoperability between one language and C, but also to build bridges between two non-C languages (which is actually fairly important for some of my current work). Performance is a nice additional benefit. > A 2x performance loss for MRI is unacceptable. We have a few points to chew through here. First, I didn't say that undefined behavior is unacceptable. My problem is with undefined behavior that is difficult to reason about or to investigate (as an extreme case, when an infinite loop is turned into a no-op). Second, I don't buy the 2x performance loss. What makes most bytecode interpreters slow is how C compilers optimize the dispatch loop (poorly), not the exploitation of undefined behavior or the lack thereof. For example, -fwrapv has virtually no effect on Ruby's performance (even though clang/gcc unnecessarily disable strength reduction in some cases where they don't have to). Ruby would likely benefit a lot more from having its bytecode dispatcher rewritten in assembler LuaJIT-style (I'm talking about the LuaJIT interpreter, not compiler) than it would lose from not exploiting undefined behavior on a large scale. Finally: code that crashes really fast (or may suddenly start crashing for inexplicable reasons because the compiler was upgraded) is not going to help anyone. This is not a hypothetical concern; you'd be surprised how much C code doesn't survive an encounter with UBSAN.