C Is Not a Low-Level Language: Your Computer Is Not a Fast PDP-11

C Is Not a Low-Level Language (2018)

In the wake of the Meltdown and Spectre vulnerabilities, this article argues that C is not a low-level language. Modern processors and compilers have become incredibly complex to maintain the illusion that C maps directly to hardware. The quest for instruction-level parallelism and speculative execution, driven by C's sequential abstract machine, led to these security flaws. Compilers like Clang require hundreds of thousands of lines of code to optimize C, and language features like structure padding and unspecified values hinder performance. The article contends that C's abstract machine no longer matches modern hardware, making it a high-level language that demands a 'sufficiently smart compiler' to run fast.

The root cause of the Spectre and Meltdown vulnerabilities was that processor architects were trying to build not just fast processors, but fast processors that expose the same abstract machine as a PDP-11.
  1. weitendorf

    This is such a pedantic point IMO. C is low level because it makes it very easy to work with machine language/assembly and do stuff like this (LLM assisted example follows):

    int main() {

    __m512i vecA = _mm512_setr_epi32(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15);

    __m512i vecB = _mm512_setr_epi32(0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75);

    unsigned short mask = 0;

    __asm__ (

    "vp2intersectd %[B], %[A], %%k2"

    : "=@cck2" (mask)

    : [A] "v" (vecA), [B] "v" (vecB)

    : "k3"

    );

    printf("Intersection Mask: 0x%04X\n", mask);

    return 0;

    }

    This is something "low level" programmers use very often to realize the benefits of a high-level language while exercising explicit control over using specific hardware instructions (vp2intersectd being an AVX-512 instruction used in highly optimized search algorithm impls).

    Obviously if you rely on implicit behavior from the compiler to optimize your code you are no longer "low level". But if you can quickly and easily drop into machine-level instructions to provide explicit implementation semantics, and the language indeed makes that relatively simple and easy to do, that sure seems "low level" to me

  2. bee_rider

    “Low level language” is one of those terms like “VLSI” (very large scale integration) where they defined it in the 70’s or something, so the academic definition is out-of-sync with what most people would expect.

    This is fine, it’s a term of art and those don’t need to be immediately obvious.

    I don’t like the title of this article for that reason, though. Really a better title would be something like “a modern x86 processor is not a PDP-11.” The subtitle is perfect basically.

    Edit: also IMO it is not really fair to beat up on C for this, the problem is not really one of low-level-ness. A language that actually exposed the complexity of speculative execution and all that could be pretty high level. It would just be harder to read in a linear text editor, right? We’d be better off drawing the dependency graph or something.

  3. legobmw99

    I’ve been a fan of this article for years, though it does often make me think that there really aren’t any true low level languages for our super scalar modern CPUs. Does anyone know of any?

  4. melodyogonna

    Well, C does give you the control when it matters, even if they feel bolted on for more modern features.

    I actually believe Mojo is the only modern language not designed to pretend every computer is a PDP-11. C has been so successful that many succeeding languages just did C things as a matter of course.

    In Mojo, everything is designed with the complexity of the modern computer in mind, and at every stage the programmer has complete control of outcomes. You decide what gets inlined, what gets passed in registers, what gets unrolled, etc. The language has excellent ... ney, probably the best portable SIMD support there is; all integers are built on top of SIMD, and the scalar integers are just SIMD with length of 1. You get complete control of what gets compiled as well due to powerful compile-time programming that is similar to, but more powerful than Zig's (imo, because you can supply a lot more information). While C and Rust allow inline asm, Mojo goes further by letting you supply inline MLIR and LLVM as well, so in situations that warrant it, you can tell the compiler to compile to a specific LLVM intrinsic. The language also does not assume you're compiling to run on just one machine; every modern computer is heterogeneous by nature and may contain multiple programmable units, so the compilation pipeline is designed to allow compiling certain parts of code for one target and other parts for other targets... as one compilation unit.

  5. glouwbug

    Maybe not then, but we basically have our own poor man's template system now:

    #define array(T, N) struct array##T##N { T value[N]; }

    void copy(array(int, 32)* x, array(int, 32)* y) {

    *x = *y;

    }

    int main() {

    array(int, 32) x;

    array(int, 32) y = { 1, 2, 3, 4 };

    copy(&x, &y);

    }

    With (rumors of) lambdas and defer on the way, C is going the way of classic WoW.

    https://en.wikipedia.org/wiki/C29_(C_standard_revision)

More from this day

2026-09-07