Join the discussion

Write your take first — we'll ask for email only when you're ready to publish.

  • Hacker News
  • C is not a low level language. It's a macro assembler
  • Okay. I like this article and I've thought about it regularly since it last made the rounds here. 1) C is a low level language for a PDP11, or for a single core on a GPU. 2) But what would a low level language look like for an FPGA? Probably verilog. 3) The point worth pursuing however is whether there might be a Hardware agnostic "low level language". 4) yep Haskel by the looks of things. If only I could find the reference.. :-/ There's a set of slides from a crew in Edinburgh doing the history of functional languages. Does any one remember something similar?
  • > what would a low level language look like for an FPGA? Probably verilog

    Verilog is not a programming language (because FPGAs are not programmed) it's a hardware description language. It's also very lossy (every vendor has reams of coding guides for using it just right with their synthetizer).

  • I've never done verilog professionally but I did "Digital Design and Computer Architecture, RISC-V Edition: RISC-V Edition" as an exercise 2 years ago.

    I would say Verilog is very much NOT a low level language.

    Metaphorically, it feels closer to SQL to me. I mean this in that you theoretically tell the system what it should do, and it builds it into the messy real world. However, the reality is that the planner (sql) or linker/placer/router/whatever (verilog) are very good, but you often end up needing to actually fully understand the problem anyway when things don't work in the abstract.

    I know there is https://clash-lang.org/ for Verilog design, which sounds a little like what you are talking about (never really looked at it myself).

  • C is a low-level language for the current ISAs we have, though not for Itanium.

    So the question is if we really want lower level ISAs. Probably not?

    There are many ways in which our current ISAs are actually thoughtfully optimized for superscalar out-of-order processors. Just look at all of the big differences from 32 bit arm to 64 bit arm, which all exist to make execution faster on superscalar processors.

    And yet they are still perfectly implementable in cheap microcontrollers. The Cortex-A53, available in boards for a little over $15, is a simple 2-wide perfectly in-order design, without a physical register page beyond the ISA register. Basically, it is a simple Pentium-type chip.

    The Apple M chips are some of the most impressive feats of out-of-order superscalar micro-engineering ever. And yet both of these can run the same software with the same ISA. This is enormously valuable.

    I fail to see how any sort of much lower level access to the machine would be portable across price ranges and microarchitecture generations. I also fail to see how it would provide a non-trivial speedup over C code pattern recommendations and targeted extensions (eg. vector extensions).

  • > I fail to see how any sort of much lower level access to the machine would be portable across price ranges and microarchitecture generations

    That's the assumption that can be removed. GPUs don't have stable ISAs, and their assembly-like code gets recompiled for each microarchitecture.

    In the Intel's world of prebaked machine code adoption of a wider set of SIMD instructions takes a decade+. In GPUs it's just a driver update.

  • I’d say assembly is the lowest level programming language we have. You have to balance the abstraction of hardware instructions with being human readable to also qualify as a programming language

    I don’t think byte code qualifies as human readable but it is closer to the metal obviously

  • Modern CPUs hide so much logic that the assembly language is an abstraction itself. CPUs have many times more registers than their assembly language, execute instructions speculatively and out of order. Multiple layers of caches are synchronized in complex ways. There's an invisible complex work scheduling algorithm that can even make one CPU core interleave work of two (hyperthreading).

    GPUs can expose more of their internals thanks to shader compilation. They don't have to emulate previous-gen chip, and instead every chip can expose exactly what it supports and rely on software being recompiled for it.

  • I enjoy this article showing up here once in a while. It makes me think about the stack of abstractions we actually live in… CPU -> microcode -> ISA -> firmware/BIOS -> OS + drivers -> C abstract machine -> your app. ( I left our virtualization purposely thinking of a bare metal stack )

    Current ISAs have so much machinery underneath that it’s hard to tell when you’re talking to the iron and when you’re talking to the microcode

    You can still argue that Forth on a Forth CPU is a genuinely low-level language. :)

  • From the preface of first edition The C Programming Language. Just interesting to note the authors never claimed it was low level, just not "very high level."

    ---

    C is a general-purpose programming language with features economy of expression, modern flow control and data structures, and a rich set of operators. C is not a "very high level" language, nor a "big" one, and is not specialized to any particular area of application.

    But its absence of restrictions and its generality make it more convenient and effective for many tasks than supposedly more powerful languages.

  • This is one of my favorite papers; it stole about a year and a half of my time. I still pine for Lisp processors although array languages can now self-host on GPUs, which, APL-pilled, I now feel is better. It'd be so cool (...for compiler writers) to be able to control precisely which kernels stay in which cache levels etc.
    by veqq
  • What do you mean it stole your time?
  • 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)

  • I remember `defer` being up for consideration for the last consortium but it got yanked. Lambdas would definitely be nice to have.
  • >C is going the way of classic WoW

    What does this mean?

  • 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.

  • 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?
  • In what way would exposing the true microcoded out-of-order etc nature of the beast benefit certain tasks?
  • > really aren’t any true low level languages for our super scalar modern CPUs

    do you mean low level but higher level than assembly language for those processors (like MIPS assembly for an R10k, for example) ?

  • Wondering can we write microcode? That's definitely closer to the metal.
  • Probably Mojo, it doesnt just talk to your CPU it also will talk to your GPU bypassing the need for CUDA. Its early days, but I see strong potential in Mojo. Currently its primary focus is GPUs for AI inference, but give it a year or two and it will be really interesting for more than just that.
  • The thing is, you can only program what is programmable. If a CPU has some capability that isn't programmable, I don't know what C or any other language is expected to do.
  • The article does make an example quite early;

    > GPUs achieve very high performance without any of this logic, at the expense of requiring explicitly parallel programs.

    GPU cores are in some ways closer to "PDP-11", they're either acting as thousands of parallel simple processors, or expose pretty raw instructions for very parallel use-cases.

  • “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.

  • 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

  • Isn't the point that x86 instructions are themselves no longer a good mental model for what the processor is actually doing under the hood, and thus C which was long billed as a thin layer over top of assembly is itself a higher abstraction?

    There is AFAIK no way to express or interact with speculative execution/branch prediction, for example.

  • Common Lisp allows you to define VOPs (compiler instructions) in user code. Smalltalk allows you to write inline assembly. Are those also low level languages?
  • Yeah, and also not every idiomatic C/C++ code is portable. Hardcoded structure sizes, assumptions about the byte order in integers or assumptions about alignments in certain data structures might cause headaches with porting the code to another CPU. Truly high level languages hide these details.
  • I think a reasonable definition of 'lower-level' is getting the programmer to take over some tasks from the compiler. Mechanically expanding a block of code into intrinsics isn't really that.

    What makes 'C' not really low level by a reasonable definition, is that the register allocation decisions are not yet made. Which, depending on how it works out, can effect ordering, inlining, unrolling etc, so the compiler can pretty much go to town on your code and create something unrecognizable.

    Since registers aren't really allocated here, this is basically on the level of C code, and all that stuff can happen here, so this really isn't much lower level than C.

    Not being elitist, it's just worth knowing what's going on under the hood of compilers, and the nature of the contract they uphold.

  • There are many reasons why C is not a low level language. Take the example: while `asm()` blocks are a common extension to C compilers, anything within the block is (a) compiler dependent and (b) architecture dependent. To choose an extreme counter example: you may as well claim that versions of BASIC with the POKE keyword are low level languages simply because you can POKE machine code directly into memory.

    Yet one of the more interesting reasons, in my mind, is that C adds a tonne of abstractions. The roster of data types is one of those abstractions. Processors have a very weak notion of data types, and memory has absolutely no notion of memory types at all. For example: casting a `float` to an `int` has a very specific definition in C, and that definition involves altering the pattern of bits. While you can create a float and force the C compiler to regard that memory location as an int (via casting pointers), it isn't how the language is meant to be used (outside of rare cases).

    If I recall correctly, some of the direct predecessors of C were typeless, which is closer to how the CPU and RAM treat data.