Something about this constantly appearing trope bugs me.
I began programming C and assembler on the VAX and the original PC. At that time, C was a reasonable approximation of the assembly code level. We didn't get into expanding C to assembly that much but the translation was reasonably clear.
As far as I know, what's changed that mid-80s world and now is that a number of levels below ordinary assembler have been added. These naturally are somewhat confusing but they aim to emulate the C/assembler model that existed way back then. These levels involve memory protection, task switching, caches and all things involved with having the current zillion-element Intel CPU behave approximately like the 16-register CPU of yore but much-much faster.
I get the "there's more on heaven and earth than your flat memory model, Horatio" (apologies to Shakespeare).
BUT, I still don't see any of that making these "Your Ceeee ain't low-level no more sucker" headlines enlightening. A clearer way to say it would "now the plumbing is much more complicated and even c programmers have to think about it".
Because... adding levels below C and conventional assembler still leaves C exactly as many levels below "high level" language as it was before and if there's a "true low level language" for today I'd like to hear about it. And the same sorts of programmers use C as when it was a low level language and the declaration doesn't even give any context, doesn't even bother to say "anymore" and yeah, I'm sick of it.
Edit: plus this particular actual article is primarily a rant about processor design with C just pulled into the fight as a stand-in for how people normally program and modern processors treat that.
> Because... adding levels below C and conventional assembler still leaves C exactly as many levels below "high level" language as it was before and if there's a "true low level language" for today I'd like to hear about it. And the same sorts of programmers use C as when it was a low level language and the declaration doesn't even give any context, doesn't even bother to say "anymore" and yeah, I'm sick of it.
Not really. For many purposes, C is not any more low-level than a supposedly "higher level" language. 20 years ago one could argue that it made sense to choose C over Java for high-performance code because C exposed the low-level performance characteristics that you cared about. More concretely, you could be confident that a small change to C code would not result in a program with radically different performance characteristics, in a way that you couldn't be for Java. Today that's not true: when writing high-performance C code you have to be very aware of, say, cache line aliasing, or whether a given piece of code is vectorisable, even though these things are completely invisible in your code and a seemingly insignificant change can make all the difference. So to a large extent writing high-performance C code today is the same kind of programming experience (heavily dependent on empirical profiling, actively counterintuitive in a lot of areas) as writing high-performance Java, and choosing to write a program with extreme performance requirements in C rather than Java because it's easier to control performance in C is likely to be the wrong tradeoff.
C has aged better than Java though. While Java still pretty much expects that a memory access is cheap relative to CPU performance like in the mid-90's, at least C gives you enough control over memory layout to tweak the program for the growing CPU/memory performance gap with some minor changes.
In Java and other high-level languages which hide memory management, you're almost entirely at the mercy of the VM.
IMHO "complete control over memory layout and allocation" is what separates a low-level language from a high-level language now, not how close the language-level instructions are to the machine-level instructions.
What popular "high level" languages might we consider? Scanning various lists, you have bytecode based languages (Java, .NET-languages, etc), you have the various scripting languages (Python, Ruby, Perl, etc), you have languages compiled the JVM (Scala, Elixar, etc), you extension to c (c++, objective-c). It seems all those are either built on the c memory model with extensions or use an
provide the same control over memory allocation as C does.
But the argument in thread is about something or other eventually being lower-level than C, right? C++, objective-C, D and friends "high-low", provide higher-level structure on the basic C model. Which in most conceptions puts higher than C but we can put them at the same level if we want, hence the "high low" usage, which is common, I didn't invent it.
Basically, the flat memory model that C assumes is what optimization facilities in these other languages might grant you. Modern CPUs emulate this and deviate from it in combination of some memory access taking longer than others and through bugs in the hardware. But neither of these things is a reason for programmer not to normally use this model, it's a reason to be aware, add "hints", choose modes, etc (though it's better if the OS does that).
And maybe different hardware could use a different sort of overt memory. BUT, the C programming language is actually not a bad way to manipulate mix-memory so multiple memory types wouldn't particularly imply "ha, no more c now". But a lot of this is cache and programmers manipulating cache directly seems like a mistake most of the time. But GPUs? Nothing about GPUs implies no more C (see Cuda, OpenGL - C++? fine).
.NET based languages include C++ as well, and .NET has have AOT compilation to native code in multiple forms since ages.
Latest versions of C# and F# also do make use of the MSIL capabilities used by C++ on .NET.
Then if we move beyond those into AOT compiled languages with systems programming capabilties still in use in some form, D, Swift, FreePascal, RemObjects Pascal, Delphi, Ada, Modula-3, Active Oberon, ATS, Rust, Common Lisp, NEWP, PL/S, Structured Basic dialects, more could be provided if going into more obscure languages.
C isn't neither the genesis of systems programing, nor did it provide anything that wasn't already available elsewhere, other than easier ways to shoot yourself.
It is literally impossible to write any reasonable high performance software in Java. (Yes, I've worked with Java devs who thought they had written high performance software, but they had no point of reference). This is mostly due, among other things, to the way modern CPUs implement cache, and the way Java completely disregards this by requiring objects to be object graphs of randomly allocated blobs of memory. A language that allows for locality of access can easily be an order of magnitude faster, and with some care, two orders of magnitude.
Its "literally possible" to do this with Unsafe, and has been for a long time. You get a block of memory, the base address, then you put things in it.
Just because its not the "idiomatic java style" doesn't mean its not Java. You might do this because you can use this for the parts that really need hand-tuned performance, then rely on the JVM/ecosystem for the parts that don't need it.
Java forces you to use profiling, at least with C you can see the exact instructions your compiler outputs. Missing the fancy vector instructions? Modify your code til you can guarantee it's vectorized. With Java you are at the mercy of the JVM to do the right thing at runtime.
Not that I disagree with what you're saying, but I thought you'd find it interesting: you can dump the JIT assembly from Hotspot JVM pretty readily to make sure things like inlining are happening as you'd expect.
You can also view the entire compiler internals in a visual way using the igv tool. You can actually get much better insight into how your code is getting compiled on a JVM like the GraalVM than with a C compiler.
However, I will admit that this is very obscure knowledge.
The instructions no longer tell the whole story though. Maybe you can tell whether your code is vectorised, but you can't tell whether your data is coming out of main memory or L1 cache, and to a first approximation that's the only thing that matters for your program's performance.
I don't see how these get past the limitations of assembly (and C) language mentioned in the post above. None of those links seem to indicate anything about exposing the cache hierarchy or out of order execution to developers.
The authors point was that it’s hard to separate discussion of modern CPU design from the constraints of C. Not from a technical perspective but from a pragmatic/commercial one.
The take away for me was that while C is obviously a higher level abstraction than CPUs, it’s a mistake to think that C has been designed for that hardware, when nowadays it’s the other way around.
But even if the hardware is designed for C instead of the other way around, getting away from C is going to be hard, indeed, if the hardware is designed for C, it kind of makes C the lowest level you can count on.
I'm just saying that the authors group together a variety of claims under "C is not low level" but while the claims themselves might be reasonable, they don't support the base point.
The article blames C for the processor designs that emulate older (non-parallel) processors. I think it can be summarised as C having a relatively straightforward translation into hardware-friendly assembly in the past, but these days both major CPUs and compilers are working hard to preserve the same model, so that neither the assembly is hardware-friendly nor efficient/optimizing C compilers are straightforward.
But "C has a bad effect" is a lot different from "C is not low level". Maybe C had a bad effect because it's low level and they should have been emulating the lisp model instead - for all I know.
Edit: I seems like it's really the flat memory model with single processing that CPUs and compiler-designers have been trying to preserve and that's not C-specific. Indeed, I think higher level languages are effectively more wedded to that.
I mention in another reply - Nvidia GPUs, complex memory model, programmed with Cuda, a C++ system. It really seems like C as such only enters here as something to beat-up on to make other points (whatever the validity of the other points).
There are different definitions of "low level language", but I think a charitable interpretation of the one in this article is that they view processors as providing a virtual machine, and count "levels" from what's more efficient for hardware (that is, the bits below that virtual machine). Though maybe a somewhat strange/confusing title was picked to attract attention.
OK, following that, in the case of the GPU, C++ code is translated into SPX "assembler" but SPX is very much a macro system and the programmer doesn't get access to the true low level.
And I don't think we'll escape the situation that there will low-level emulation code that programmers can't and should not access. It's good to know but that doesn't change the levels programmers normally work with.
>Maybe C had a bad effect because it's low level and they should have been emulating the lisp model instead - for all I know.
Of course lisp machines were a thing. They went out of fashion when more conventional architectures could run lisp code faster than their dedicated machines.
> I began programming C and assembler on the VAX and the original PC. At that time, C was a reasonable approximation of the assembly code level. We didn't get into expanding C to assembly that much but the translation was reasonably clear.
Right: On the VAX, there wasn't much else for a compiler to do other than the simple, straightforwards thing, and I'm including optimizations like common subexpression elimination, dead code pruning, and constant folding as straightforwards. Maybe loop unrolling and rejuggling arithmetic to make better use of a pipeline, if the compiler was that smart.
> As far as I know, what's changed that mid-80s world and now is that a number of levels below ordinary assembler have been added.
You make good points about caches and memory protection being invisible to C, but they're invisible to application programmers, too, most of the time, and the VAX had those things as well.
Another thing that's changed is that chips have grown application-visible capabilities which C can't model. gcc transforms certain string operations into SIMD code, which vectorizes it and turns a loop into a few fast opcodes. You can't tell a C compiler to do that portably without relying on another standard. C didn't even get official, portable support for atomics until C11.
You can dance with the compiler, and insert code sequences and functions and hope the optimizer gets the hint and does the magic, but that's contrary to the spirit of a language like C, which was a fairly thin layer over assembly back in the heyday of scalar machines. I don't know any modern language which fills that role for modern scalar/vector hybrid designs.
SIMD design itself isn't constant between different processor families. Any purported standardized language for scalar/vector hybrid either has to rely on a smart optimizer or be utterly platform specific.
> SIMD design itself isn't constant between different processor families. Any purported standardized language for scalar/vector hybrid either has to rely on a smart optimizer or be utterly platform specific.
That is indeed part of the problem. There might be enough lowest-common-denominator there to standardize, like there is with atomics, I don't know, but I'm not saying that C needs to add SIMD support. I'm saying that any low-level language needs to directly expose machine functionality, which includes some SIMD stuff on some classes of processor.
Maybe there will be a shakeout, like how scalar processors largely shook out to being byte-addressable machines with flat address spaces and pointers one word size large, as opposed to word-addressable systems with two pointers to a machine word (the PDP-10 family) or segmented systems, like lots of systems plus the redoubtable IBM PC. C can definitely run on those "odd" systems, which weren't so odd when C was first being standardized, but char array access definitely gets more efficient when the machine can access a char in one opcode. (You could have a char the same size as an int. It's standards-conformant. But it doesn't help your compiler compile code intended for other systems.) C could standardize SIMD access once that happens. However, it would be nice to have a semi-portable high-level assembly which targets all 'sane' architectures and is close to the hardware.
You’re mistaken about the PDP-10. Yes, you could pack two pointer-to-word pointers into a single word; but a single word could also contain a single pointer-to-byte. See http://pdp10.nocrew.org/docs/instruction-set/Byte.html for all the instructions that deal with bytes, including auto-increment! And bytes could be any size you want, per pointer, from 1 to 36 bits.
C#/.Net has vector operations in System.Numerics.Vectors namespace, which will use SSE, AVX2 or neon, if avaliable. However, there are numerous Simd instructions cannot be mapped that way.
For that reason, .Net core 3 added Simd intrinsics, so now you can give AVX/whatever instructions directly.
If I remember correctly someone made a very performant physics engine with the vector API
I found it insightful, because it goes on to discuss how C has had a huge effect on the programming model all the way from the processor to the compiler and it’s led to problems in performance and security. They suggest that it may be worthwhile designing or using a language that can better handle how processors are structured today.
But it admits that processors today are structured to look like processors of the mid-80s (even though they definitely aren't). Today's processors have lower levels than but those lower aren't intended to be accessed by ordinary programs using any language. Maybe that's a bad thing but we've migrated to a very different argument here.
Maybe, they're saying we could have processors which wouldn't emulate the PDP-11. Sure, maybe we could. Like say, Nvdia GPUs programmed using the cuda system, which is based on ... C++, which also "isn't a low level language".
I mean GPU definitely have emerged as alternate way to use the bizillion transistors of modern chips but broadly, I don't see this as invalidating C (or c++) as a next-step-up-from-assembly level of programming. I mean, the reality is C actually is used in all sorts of complicated memory spaces and chips, often mixed with assembler.
IE, the "not low level" claim still is kind of a troll imo.
While CUDA is designed to make C++ run optimally well (several CppCon talks about it), they are also designed to run any language with a PTX backend well, including C, Fortran, Java, Haskell and .NET.
I think you touched close to an opinion of mine which is anything lower lever than c becomes very hard for ordinary people to work in. I remember DSP's in the 80 and 90's. Where to program them you needed to deeply understand how the machine worked. And guess what eventually manufacturers ported C over to them so that programmers could be productive when working on the non-performant parts of the code base.
If anything modern processors are even worse under the hood. With the added problem that you can't feed one of them raw 'true' instructions fast enough to keep them from stalling.
> Because... adding levels below C and conventional assembler still leaves C exactly as many levels below "high level" language as it was before and if there's a "true low level language" for today I'd like to hear about it.
Me too, actually. I know you meant that rhetorically, but what if you designed an instruction set that better matched modern processor designs and then built a low-level compiled language on top of it? My hunch is that modern processor designs are so complicated that you’d have to do similar amounts of abstraction to make the language usable, but I’m not sure.
The optimizers are much stronger nowadays. They rewrite the program, so that the resulting assembly might have nothing to do with the code you wrote.
Especially if undefined behavior is invalid. Decades ago you did not need to care about undefined behavior. You write a + b, and you know the compiler emits an ADD instruction for +, and that ADD of x86 does not distinguish between signed and unsigned instructions, and you get the same result for signed and unsigned numbers, regardless of overflow. But nowadays the optimizer comes, says, wait, signed overflow is undefined, I will optimize the entire function away.
I began programming C and assembler on the VAX and the original PC. At that time, C was a reasonable approximation of the assembly code level. We didn't get into expanding C to assembly that much but the translation was reasonably clear.
As far as I know, what's changed that mid-80s world and now is that a number of levels below ordinary assembler have been added. These naturally are somewhat confusing but they aim to emulate the C/assembler model that existed way back then. These levels involve memory protection, task switching, caches and all things involved with having the current zillion-element Intel CPU behave approximately like the 16-register CPU of yore but much-much faster.
I get the "there's more on heaven and earth than your flat memory model, Horatio" (apologies to Shakespeare).
BUT, I still don't see any of that making these "Your Ceeee ain't low-level no more sucker" headlines enlightening. A clearer way to say it would "now the plumbing is much more complicated and even c programmers have to think about it".
Because... adding levels below C and conventional assembler still leaves C exactly as many levels below "high level" language as it was before and if there's a "true low level language" for today I'd like to hear about it. And the same sorts of programmers use C as when it was a low level language and the declaration doesn't even give any context, doesn't even bother to say "anymore" and yeah, I'm sick of it.
Edit: plus this particular actual article is primarily a rant about processor design with C just pulled into the fight as a stand-in for how people normally program and modern processors treat that.