13 ms·
> There are undeniably power users for whom every last bit of performance translates to very large sums of money, and I don’t claim to know how to satisfy them
by haberman 3y ago
> There are undeniably power users for whom every last bit of performance translates to very large sums of money, and I don’t claim to know how to satisfy them otherwise.
That is the key, right there.
In the 1970s, C may have been considered a general-purpose programming langauge. Today, given the landscape of languages currently available, C and C++ have a much more niche role. They are appropriate for the "power users" described above, who need every last bit of performance, at the cost of more development effort.
When I'm working in C, I'm frequently watching the assembly language output closely, making sure that I'm getting the optimizations I expect. I frequently find missed optimization bugs in compilers. In these scenarios, undefined behavior is a tool that can actually help achieve my goal. The question I'm always asking myself is: what do I have to write in C to get the assembly language output I expect? Here is an example of such a journey: https://blog.reverberate.org/2021/04/21/musttail-efficient-interpreters.html https://blog.reverberate.org/2021/04/21/musttail-efficient-i...
I created the https://github.com/protocolbuffers/upb https://github.com/protocolbuffers/upb project a long time ago. It's written in C, and over the years it has gotten to a state where the speed and code size are pretty compelling. Both speed and code size are very important to the use cases where it is being used. It's a relatively small code base also. I think focused, performance-oriented kernels are the area where C makes the most sense.
- ianlevesque 3y agoThanks for giving an actual example of such optimizations. In my personal experience my C++ (and Rust) code was often outperformed by the JVM's optimizations so I've found it hard to relate to the tradeoffs C++ developers assume are obvious to the rest of us.
- grumpyprole 3y ago+1. Part of the problem is that x86 assembly is hardly programming the metal anymore. The performance characteristics of processors has changed over the years also, compilers can be more up-to-date than humans.
- overgard 3y agox86 assembly doesn't represent the actual opcodes the CPU executes anymore, but it's still the low level "API" we have to the CPU. Even if assembly isn't programming to the metal, it's definitely more to the metal than C, and C is more to the metal than Java, etc. Metalness is a gradient
- grumpyprole 3y agoLol, I like the word "metalness".
- SubjectToChange 3y agoMetalness is a gradient It would be better to say that "Metalness" is a sort of "Feature Set". IMO, most programmers would tend to agree that C++ is far closer to Java than C is, yet C++ is every bit as low level as C is. Indeed, even a managed language like C# supports raw pointers and even inline assembly if one is willing to get their hands dirty.
- kaba0 3y agoThere is expressivity and abstraction-ability which are language features, and there is “levelness” or metalness as is it being referred to. Low and high level languages have two definitions I have seen, one is more objective, but utterly useless: only assemblies are low-level, everything else is high. The other one is more about what can be controlled in a given language’s idiomatic usage, I believe that’s what most people actually refer to intuitively - e.g. C#/Java have ways to manipulate pointers, but you would generally not say that. In that regard, C, C++, Rust are quite close to the metal with the latter two actually being even closer as they have native control over SIMD datatypes as well, while C doesn’t have that. Note, this is only a partial ordering, as e.g. C#/Java also have control over vector instructions, yet I wouldn’t go around claiming them closer to the metal as C. Going back to expressivity, C++ is very expressive, while C is very inexpressive, but that is a completely orthogonal axis.
- lbrandy 3y agoI struggle to resonate with what you are saying, as my experience is the opposite. I'm curious where this discrepancy is rooted. Reckless hypothesis: are you working on majority latency or majority throughput sensitive systems? I have seen so, so, so many examples of systems where latencies, including and especially tail latencies, end up mattering substantially and where java becomes a major liability. In my experience, actually, carefully controlling things like p99 latency is actually the most important reason C++ is preferred rather than the more vaguely specified "performance".
- ianlevesque 3y agoThe specific example that comes to mind was translating a Java application doing similarity search on a large dataset into fairly naive Rust doing the same. Throughput I guess. It may be possible to optimize Rust to get there but it’s also possible to (and in this case did) end up with less understandable code that runs at 30% the speed. Edit: And probably for that specific example it’d be best to go all the way to some optimized library like FAISS, so maybe C++ still wins?
- grumpyprole 3y agoI've seen C++ systems that are considerably slower than equivalent Java systems, despite the lack of stack allocation and boxing in Java. It's mostly throughput, malloc is slow, the C++ smart pointers cause memory barriers and the heap fragments. Memory management for complex applications is hard and the GC often gives better results.
- vvanders 3y agoI've seen so may flat profiles due to shared_ptr. Rust has done a lot of things right but one thing it really did well was putting a decent amount of friction into std::sync::Arc<T>(and offering std::rc::Rc<T> when you don't want atomics!) vs &mut T or even Box<T>. Everyone reaches for shared_ptr when 99% of the time unique_ptr is the correct option.
- memefrog 3y ago
- overgard 3y agoI think the reason to use C/C++ over java has less to do with the various optimizations and more to do with control over memory layout (and thus at least indirect control over cache usage and so on). Plus you remove a lot of "noise" in terms of performance (GC hiccups, JIT translation, VM load time, etc.).
- kllrnohj 3y agoIt would be fascinating if you could give any such comparison examples. The only time I've seen JVM come anywhere close to C++ in normal usage is if the C++ code was written like it was Java - that is, lots of shared_ptr heap allocations for nearly everything. Or perhaps you're one of the rare few that write Java code like it was C instead? You can definitely get something fast like that, but it seems all too rare.
- deterministic 3y agoThere is no universe where well written and optimised C++ code will be out-performed by Java code. You might get similar performance in Java if you create zero objects (except during initialisation) and then manually manage the usage of objects at runtime. However only if the matching C++ code is written naively.
- mcguire 3y agoIn the 1970s, the alternative to C was assembly. In fact, I can remember hearing stories of the fights needed to pry every-day, normal developers---not systems programmers, not power users---off of assembly to a "higher level language". It wasn't until the 90s that it became clear that C was not appropriate for applications work (and this was in the era of 4-16MB machines), although that took a long time to sink in.
- daymanstep 3y agoWhy is C inappropriate for applications work?
- TillE 3y agoTry writing a generic, reusable data structure in C. It's agony.
- bluetomcat 3y agoYou don’t need to write generic and reusable data structures in C. You write a data structure suited for the problem at hand, which often means that it’s going to be simpler and more performant because of the known constraints around it.
- grumpyprole 3y agoIt could be more performant because of the known constraints around it, or it could be an ad-hoc, informally-specified, bug-ridden, slow implementation of half of some data structure. At least with a generic and resuable data structure you have a known reliable building block. Again, performance over safety.
- commonlisp94 3y ago> It could be more performant No, it almost always is. The designers of a generic library can't anticipate the use case, so can't make appropriate tradeoffs. For example, compare `std::unordered_map` to any well written C hash table. The vast majority of hash tables will never have individual items removed from them, but a significant amount of complexity and performance is lost to this feature.
- overgard 3y ago> In the 1970s, C may have been considered a general-purpose programming langauge. Today, given the landscape of languages currently available, C and C++ have a much more niche role. They are appropriate for the "power users" described above, who need every last bit of performance, at the cost of more development effort. I really don't think this is true. I've worked in CAD and video games and embedded software, and in all those you're likely using C++ (not to mention a lot of desktop software that can't afford to embed a chromium instance for electron.) For some reason people here just assume that anything that isn't web development or backend server CRUD stuff is a niche. As much attention as something like Rust or whatnot gets on hacker news, the reality is that if you can't afford garbage collector pauses, and you need performance, you're using C/C++ most of the time.
- kiratp 3y agoRust is not a GC language. It can and does achieve the same perf as any modem C++ with the latest flavor of the C++ safety guardrails. The set of reasons to start a new project in C/C++ are few and the list is shrinking by the day.
- overgard 3y agoI phrased it poorly, I didn't mean to imply Rust is GC'd. I mean that it's still niche. C++ very much is not niche, which is my point. The other non-niche languages generally are GC'd (java, C#, javascript, etc.) > The set of reasons to start a new project in C/C++ are few and the list is shrinking by the day. That'd be true if every project started from scratch with only the language standard library. And yet..... almost any project you start is going to be dependent on a large chunk of code you didn't write, even on greenfield projects.
- haberman 3y ago> That'd be true if every project started from scratch with only the language standard library. And yet..... almost any project you start is going to be dependent on a large chunk of code you didn't write, even on greenfield projects. I think this is true, and I'd refine my original statement accordingly. My original comment was thinking more from first principles, not as much about pragmatic considerations of ecosystem support. If we were to disregard the momentum of existing ecosystems, I think C/C++ would be niche choices today: very important for certain, focused use cases, but not meant for the masses. Taking into account the momentum of existing ecosystems however, they still play a large role in many domains.
- latenightcoding 3y agoRead your article (and cloudfare's) and as someone who uses musttail heavily I don't understand the hype, as you mentioned in your blog: you can get tail call optimizations with (-O2), musttail just gives you that guarantee which is nice, but the article makes it sound as if it unlocks something that was not possible before and interpreters will greatly benefit from it, but it's more reasonable to ask your user to compile with optimizations on than it is to ask them to use a compiler that supports musttail (gcc doesn't). Moreover, musstail has a lot of limitations it would be hard to use in more complex interpreter loops
- mtklein 3y agoOrdinarily you're at the whim of the optimizer whether calls in tail position are made in a way that grows the stack or keeps it constant. musttail guarantees that those calls can and are made in a way that does not let the stack grow unbounded, even without other conventional -On optimization. This makes the threaded interpreter design pattern safe from stack overflow, where it used to be you'd have to make sure you were optimizing and looking carefully at the output assembly. If nothing else musttail aids testing and debugging. Unoptimized code uses a lot more stack, both because it hasn't spent the time to assign values to registers, but people often debug unoptimized code because having values live on the stack makes debugging easier. The combination of unoptimized code and calls in tail position not made in a way that keeps stack size constant means you hit stack overflow super easily. musttail means that problem is at least localized to the maximum stack use of each function, which is typically not a problem for small-step interpreters. Alternatives to musttail generally involve detecting somehow whether or not enough optimization was enabled and switching to a safer, slower interpreter if not positive... but that just means you're debug and optimized builds work totally differently, not at all ideal!
- jjoonathan 3y agoPerf is a niche, here's another: address space lets you talk to hardware. VM and OS abstractions have been so successful that you can go a whole career without talking directly to hardware, but remember, at the bottom of the abstraction pile something has to resolve down to voltages on wires. Function calls, method names, and JSON blobs don't do that. So what does? What abstraction bridges the gap between ASCII spells and physical voltages? Address space. I/O ports exist, but mostly for legacy/standard reasons. Address space is the versatile and important bridge out of a VM and into the wider world. It's no mistake that C and C++ let you touch it, and it's no mistake that other languages lock it away. Those are the correct choices for their respective abstraction levels.
- packetlost 3y agoIdk what you're on about, I mmaped a `/dev/uio` from Python this morning. Yeah, I had to add it in a .dts file and rebuild my image, but even slow as shit high level languages like Python let you bang on registers if you really want to.
- notacoward 3y agoThat worked because you were on an OS that supported it, using a device with simple enough behavior that things like timing or extra/elided writes didn't matter. It's great when that works, but there are very many environments and devices for which that option won't exist.
- packetlost 3y ago> That worked because you were on an OS that supported it I mean... yeah? I'd be surprised if most operating systems didn't have a facility for directly accessing memory mapped devices in some capacity. Even Windows does I think. Have any examples of an OS that doesn't support banging directly on memory? > using a device with simple enough behavior that things like timing or extra/elided writes didn't matter Yeah, but that's less of a function of the language like GP was referring to and more a property of using a non-RTOS that doesn't play nice with timing sensitive things from user-space. The language itself is not really the issue there, which was my point.
- SubjectToChange 3y agoOf course the problem is that the vast corpus of legacy C code was not written with such aggressive compilers in mind.
- jfengel 3y agoWith modern CPUs, that kind of hand-tuned assembly gets rarer and rarer. Pipelines and caching and branch prediction make it hard to know what's actually going to be faster. And even when you do know enough about the CPU, you only know that CPU -- sometimes only one model of a CPU. There's still a niche for it, but it's tiny and it keeps getting tinier.
- khuey 3y ago> When I'm working in C, I'm frequently watching the assembly language output closely, making sure that I'm getting the optimizations I expect. I frequently find missed optimization bugs in compilers. Do you repeat this exercise when you upgrade or change compilers?
- 8n4vidtmkvmk 3y agoIf the assembly is that important I'd find a way to put it into my unit tests. Create a "golden" for the assembly and it should trigger a diff if it changes.
- jacquesm 3y agoThe easy way to achieve that is to freeze the assembly once it is generated and to keep the C inlined around as documentation, as well as a way to regenerate the whole thing should it come to that (and then, indeed you'll need to audit the diff to make sure the code generator didn't just optimize away your carefully unrolled loops).
- oasisaimlessly 3y agoFor hot functions that were carefully optimized, I have test harnesses that run them and measure their cycle counts, which are asserted to be equal to the cycle count after hand-optimization (+/- some small margin if there is non-determinism). This is sensitive to regressions while having a very low false-positive rate due to being (approximately) deterministic.
- kaba0 3y agoI do buy it in case of C++, but C? You can’t even have a proper data structure with it due to it being so inexpressive - and surely data structures are one of the most significant factors of performance. Besides, the industry hands down chose C++ for the truly performance-oriented workloads.
- stjohnswarts 3y agoYeah I don't use c any longer unless I absolutely have to. I mostly do embedded so that happens sometimes. I much prefer c++11 on up.
- dkersten 3y agoI just want to thank you for that blog post! I’m currently writing a little bytecode vm for something and this really improves my code! Luckily I’m currently already only targeting clang, so I’m ok with using a non-portable feature.