I am a C programmer. Not a C++ programmer; I’ve bounced off the language several times, having understood the basics just fine but baulked at being presented with half a screenful of boilerplate every time I try to understand real code. I use Python and the occasional shell script for my tools; there was a period when I used Perl a great deal, but I’ve forgotten almost all that I knew about it now. I haven’t used Rust in anger, though there’s no particular reason for that other than habit, and I use homeopathic amounts of Lisp when tweaking my emacs setup. Nothing else. Other high-level languages may well have their places (though I doubt it in some cases), but they don’t fit my environment. My colleagues differ from me in their language preferences and environments, of course.
See, I’m primarily a low-level embedded systems programmer. And by embedded systems here I don’t mean Linux-on-a-chip, I mean directly controlling hardware by poking values into registers. Most languages that are more “high level” than C introduce background systems and superstructure that I really don’t want in the way, particularly when my only means of finding out about them is to read the assembly code the compiler generates. (That’s a good habit to get into anyway, as you will better understand what the code you wrote does, but it can be tedious. In these days of AI coding I suppose the same applies; you will learn from what your AI writes for you, but that can be a lot to read.) When my code writes a value to a memory location, waits two microseconds and writes a different value to a different memory location, I want that exact sequence of operations in that exact order, because it matters to the hardware. In effect, I’m using the C compiler as a glorified macro assembler.
Sometimes even C suffers from “knowing better” (honestly I regard strict aliasing as a bug in the standard), particularly if I thoughtlessly turn on optimisation. Optimisers by their very nature re-order operations for convenience when it doesn’t affect the program logic, and poking hardware registers doesn’t obviously affect program logic to a compiler, at least not without throwing “volatile” qualifiers around in ways that aren’t always helpful. These days I am well enough versed in the ways of compilers to usually coerce them into doing what I want, but sometimes it is just simpler to write the assembler myself. It’s often more readable, surprisingly enough.
I’ve had a healthy disrespect for optimisers for a very long time. Back in the day I wrote low-level software for Acorn computers, so I knew my ARM code backwards. Acorn’s Norcroft compiler produced good ARM code, but it was usually possible to do better by hand, so we did. There were all sorts of cunning tricks you could pull, such as when to use instructions that set the condition flags so that you got a test as a side-effect of another operation, and even the best compiler in the world wasn’t going to spot those reliably. Then Acorn went bust, ARM licensed processor cores and suddenly I had to use GCC on a PC to program all these wonderful ARM-based chips.
GCC was… poor. It could produce decent basic code, but it was structured around programming x86 processors and it didn’t really understand the flexibility of ARM instruction condition codes. I am told that the optimisation phases didn’t have access to the information they would need to take advantage of that flexibility. Whatever the reason, I could always write significantly faster and more compact code in ARM assembler than it ever managed. ARM poured a lot of resources into improving GCC for their chips, since it was the free compiler everyone used, and the result was the Thumb instruction set. Something that had 16-bit instructions rather than 32-bit, effectively ditched the beautiful symmetry of the ARM instruction set, and lost all that lovely flexibility in condition codes. GCC is a lot better at optimising it, but I still think it’s a near thing between GCC-optimised Thumb code and hand-optimised ARM code.
I’m being a little unfair to compilers in general here, making it sound like optimisers are worse than they really are. Some produce amazing code. TI’s compiler for their C6xxx DSP cores produces loops that are tightly written things of beauty, though its “straight line” optimisations weren’t nearly so good last time I used it. Clang is apparently better at dealing with ARM processors, probably because it was written at a time when more processors were out there. For most people it won’t even matter that much, but for embedded systems it can matter a lot.
These days of course a lot of factors change what makes code fast, not the least of which is instruction and data caching. The days when I could just look at instructions and not worry about where they or the data were coming from (much) are long past. That makes it all the more important that compilers Do What I Say rather than Do What They Think I Meant. Because homo sapiens appears to be incapable of leaving well alone, that is very slowly becoming less true. The C standard appears to try quite hard not to change the underlying simplicity of the language, a thing conspicuously untrue of C++ standards, but it does happen occasionally. I may be unusual living in a world where the number of bytes of program or data space available sometimes matters and microsecond timing (or sometimes nanosecond timing) frequently matters, but I’m not unique and we are gradually being less well catered for even by the simple high level languages.
So I’m a C programmer in the sense that I’m a lazy assembler programmer, and that’s most of the reason I don’t use other languages in my end product code. Everything else, even C++, introduces subsystems and abstractions that I neither need nor want, and often can’t afford.
