26 ms·
This article makes some valid points but is overall rather misleading I think. Almost all of the reasons given why C is "not a low-level language" also apply to
by mattnewport 9y ago
This article makes some valid points but is overall rather misleading I think. Almost all of the reasons given why C is "not a low-level language" also apply to x86/x64 assembly. Register renaming, cache hierarchies, out of order and speculative execution etc are not visible at the assembly / machine code level either on Intel or other mainstream CPU architectures like ARM or Power PC. If C is not a low level language then a low level language does not exist for modern CPUs and since all other languages ultimately compile down to the same instruction sets they all suffer from some of the same limitations.
It's really backwards compatibility of instruction sets / architectures that imposes most of these limitations. Processors that get around them to some degree like GPUs do so by abandoning some amount of backwards compatibility and/or general purpose functionality and that is in part why they haven't displaced general purpose CPUs for general purpose use.
- ge0rg 9y agoI also had the initial impression that the article is misleading, but later on the author made the point that the C compiler is doing significant work to reorder / parallelize / optimize the code. I agree that x86/x64 is not a low-level language either, but even if it was, with the description the author provided, I'd agree with his point of C not being low-level. Regarding cutting off backwards compatibility to improve the design, Intel's Itanium (affectionately called "Itanic") was a very progressive approach to shift the optimization work from the CPU (and the compiler) to just the compiler. I'm not sure what the reasons for its failing were, though.
- pcvarmint 8y agoItanic failed because Intel/HP initially still used a front side bus memory architecture, which could not support the bandwidth necessary for peak performance on anything but matrix multiplication and other computations where most of the work is done in-cache. Then Opteron came around, with its faster memory, and Intel was suddenly thrown back into reality. Itanic was also in-order (at least as far as dispatch), meaning anytime an instruction was stalled, so were all instructions in the same bundle or after it. One "non-low-level" idea on Itanic, which never really panned out in practice, was for the assembler to automatically insert stop bits ;; marking assembly code "sequence points", instead of the programmer having to do it manually. But in practice, everyone did it manually, because they'd rather know how well their bundles were being used, and whether they could move instructions around in order get the full 3 instructions / bundle (6 instructions / clock). And explicit stop bits did not provide any advantage to future hardware by marking explicit parallelism, because at every generation everyone was concerned about obtaining maximum performance on the current machine, which involved shuffling instructions into 6-instruction double-bundles, often at the expense of parallelism on future implementations (which never went beyond two bundles / clock).
- Avshalom 9y agoPart of it was the sort of giant blind spot that "C is a low level language" creates that made even Intel forget how many man-years of optimization were actually in C compilers and the attendant hubris that "oh we can whip up something that'll beat it between now and when we start shipping the chips"
- mattnewport 9y agoI think you can make a good case that the failure of the Itanium (and other similar attempts to un-hide some of this stuff like the IBM/Sony Cell used in the PlayStation 3) was precisely because they tried to shift optimization work from the CPU to the compiler / programmer.
- greglindahl 9y agoFunny, the compiler people I've worked with complain that Itanium tried to do too much in hardware, like the hw support for loop unrolling, which made superpipelining optimizations in existing compilers much more complicated.
- mattnewport 9y agoLoop unrolling is one of those optimizations that actually highlights the need for dynamic CPU optimizations like out of order execution and speculative execution. It's very difficult to statically make a good decision about the optimal amount of loop unrolling to do, especially if you want to generate code that will continue to perform well on future CPUs using the same ISA. Even when targeting a specific CPU model it's difficult however since you don't know statically how many iterations of the loop you're expecting, what's currently in cache, what other code might be running immediately before or after the loop, what's running at the same time on other threads, etc.
- greglindahl 9y agoItanium's hardware did not make any of these things easier.
- wbl 9y agoThe compiler wasn't smart enough. There is information available only at runtime that can be used to get out of order execution and superscalar execution, and the compiler doesn't see any of it. More effort on standard architectures paid off.
- PersistentCough 9y ago> the C compiler You mean C compiler X has the feature of Y. There are lots of compilers and that's not part of the language.
- umanwizard 9y agoTheir comment makes sense if you interpret "the C compiler" to mean "mainstream C compilers that 99% people use in production" or even just "gcc and clang". We're talking about concrete things in the real world here, not philosophizing about the language spec.
- PersistentCough 9y ago> not philosophizing about the language spec. That's exactly what's going on.
- Avshalom 8y agoAnd yet small device C and Gcc/clang are onky kissing cousins and we (well people that aren't me: ustedes) still rhetorically lump them together as a single language.
- deleted 9y ago[deleted]
- SolarNet 9y agoNot sure if facetious, but it failed because shifting that work caused it to be slow and not backwards compatible.
- pera 9y agoI'm not an expert, but I don't think the optimization phase should be really considered here: the same kind of pattern matching used by (e.g.) LLVM to find optimizable sequence of statements also could be used by any assembler. NASM for instance offer some level of optimizations, so I think optimization should only be considered when it's part of the language specification itself, like in Scheme. IMO C is close to low-level because it's relatively easy to imagine the resulting unoptimized assembly given some piece of code (which is why some people jokes about C being a macro assembly). Maybe this old debate should get an slight update... and this could be the starting point: Is modern x86 assembly still "low-level"? :)
- munificent 9y ago> the same kind of pattern matching used by (e.g.) LLVM to find optimizable sequence of statements also could be used by any assembler. Some of the simpler optimizations, sure. But modern backends do many incredibly sophisticated optimizations that are way beyond any kind of simple pattern-matching-and-substitution model. Even fundamental "optimizations" like register allocation use quite sophisticated algorithms. Optimal register allocation is NP-complete, so compilers use heuristics on top of graph coloring algorithms to do their best. Most other optimizations rely on type analysis, data flow analysis, liveness analysis, etc.
- vvanders 9y agoIt would be interesting to revisit a world where languages/compilers were built explicitly with common memory access semantics/out of order op/etc in mind. One of the things that really excites me about Rust is that it's single mutable reference enforcement means you can run run `restrict` 100% of the time if you wanted which is a non-trivial performance boost. I think it's not enabled today but from previous discussions it sounds like that's just a matter of plumbing through the right things to LLVM. Every time I've seen that rolled out in a C/C++ codebase someone invariably forgets about pointer aliasing and you spend a week tracking down some non-deterministic behavior.
- pera 9y agoMy point was that since optimization is also possible in assembly, and since assembly is considered low-level, then optimization per se shouldn't be used as something that characterize high-level languages exclusively. But it is true that some abstractions used by high-level languages enable quite complex optimization techniques, so there is a clear correlation between the level of a language and the ability of its compiler to analyze and optimize programs.
- skywhopper 9y agoThere was an article on Hacker News recently that covered some of the reasons for Itanium's failure to realize its theoretical benefits. I'm not finding it now, but IIRC, the argument made was that predicting likely-parallelizable code is actually a lot harder to do at compile time, and that, like so many ultra-optimized systems, the real world works much differently and a messier, more random approach ultimately yields far better performance.
- drbawb 9y ago>the argument made was that predicting likely-parallelizable code is actually a lot harder to do at compile time So don't do it at compile time? That's really a very weak argument against the Itanium ISA, and honestly more of an argument against the AOT complication model. Take a runtime with a great JIT, like the JVM or V8, and teach it to emit instructions for the Itanium ISA. (As an added advantage these runtimes are extremely portable and can be run, with less optimizations, on other ISAs without issue.) The problem, as always, is that nobody with money to spend ever wants to part with their existing software. (Likely written in C.) In 2001 Clang/LLVM didn't even exist, and I'm not familiar with any C compilers of the era that had so much as a rudimentary JIT.
- marshray 9y agoThis argument has been made since the introduction of the JVM in the early mid-90's. Seems to me like if, in practice, JIT provided better performance then by now people would be rewriting their C/C++ code in Java and C# for speed.
- lmm 9y ago> Seems to me like if, in practice, JIT provided better performance then by now people would be rewriting their C/C++ code in Java and C# for speed. It's a little bit faster, not faster by enough to matter. If you're going to rewrite C/C++ code for speed you'd go to Fortran or assembler, and even then you're unlikely to get enough of a speedup to be worth a rewrite. New projects do use Java or C# rather than C/C++ though.
- colin_mccabe 8y agoI think people often overestimate how smart the compiler is. DJB had a slide deck about this: http://cr.yp.to/talks/2015.04.16/slides-djb-20150416-a4.pdf http://cr.yp.to/talks/2015.04.16/slides-djb-20150416-a4.pdf Compilers are really useful... but, if it really has to be fast, you still need to have a human in the loop. I was also under the impression that there hasn't been much improvement in compiling C/C++ in a long time. It would be interesting to compare the performance of gcc from 15 years ago versus gcc today, on a real world piece of code. I suspect you wouldn't see much difference (aside from the changes in C dialect over time), and some added features in the new version. Has anyone run this experiment?
- deleted 9y ago[deleted]
- Avshalom 9y ago>If C is not a low level language then a low level language does not exist for modern CPUs correct, but that lack is not an argument for C being low level.
- swsieber 9y agoCorrect - it's an argument for broadening the title. Calling out a specific language as not being something leads one to ask, "Well what is?". In this case, there is no qualifying alternative, so the title might as well be, "There is no low-level language for CPUs".
- Avshalom 9y ago... any more. It's not a category that can't exist, it just doesn't have any members right now.
- mattnewport 9y agoIt actually can't exist at all for current modern CPUs (x64/ARM/PowerPC) since they don't expose a programming interface for many of the things discussed in the article (speculative / out of order execution, register renaming, full cache control).
- Avshalom 9y agoYeah but it can for all sorts of microcontroller-y chips; embedded CPUs; DSPs; theoretical future CPUs that aren't trying to both act like a PDP11 and go faster every year...
- munificent 9y ago> Calling out a specific language as not being something leads one to ask, "Well what is?". The article does basically answer this. The last section is about what it would mean to design a chip such that a low level language were possible to design for it.
- rbanffy 9y agoMicrocode is the new low level language. ;-) But, to be fair, C is not that low level. In fact, when I first learned it, it was considered a high-level language because CPUs we used it with didn't have functions with parameters, only subroutine jumps. C reaches into the realm of low-level languages because it allows you to arbitrarily read from and write to the "state" of the context you live in, but it also allows you to express constructs that have no counterpart even on the most complex CPU architectures (even if they have things that disagree fundamentally with C's point of view).
- nerdponx 9y agoOn the flipside, doesn't it allow more or less direct memory access?
- abofh 9y agoI mean, yes, no, it depends on what you mean? You can write to null in C, your operating system rejects it. You can write past your allocated memory, your OS rejects it. It's not like just because it's written in C it gets to read the kernel memory - it's just that you can try. All memory access in userspace is mediated by the MMU, so nothing in C gets "direct" memory access - but it does allow you to screw up your own memory space pretty well... I'm not sure that's C's fault though
- pjmlp 9y agoMany C targets also run bare metal, there is nothing to reject there, unless the hardware has an MMU and the code bothered to configure it on boot.
- saagarjha 9y agoBut is there really any programming language that lets you write to arbitrary memory? This is a problem of privileges rather than how low level your language is: it's not like you can write assembly that will give you arbitrary access to kernel memory.
- nerdponx 9y ago
- admax88q 9y ago> If C is not a low level language then a low level language does not exist for modern CPUs I think that's a fair conclusion though, I don't think the article is misleading. x86 assembly is a high level language. It's analogous to JVM bytecode. Modern x86 processors are more like a virtual machine for x86 bytecode.
- pvg 9y agoIt's analogous to JVM bytecode. That's only true in the way everything is analogous to everything.
- chubot 9y agoI think it's a good analogy because it's saying that nontrivial optimizations happen below the code layer. In early CPUs, nothing was optimized. They just executed your instructions. Now there are nontrivial optimizations and rewriting, just like the JVM.
- coldtea 9y agoNope, also in the way the parent explains right after that sentence.
- pvg 9y agoIt's a pretty terrible analogy in nearly every obvious technical way. What an x86 cpu does with its instructions is almost but not entirely unlike what a JVM does.
- admax88q 9y agoNone of the "registers" in x86 assembly are real. Few of the instructions are implemented in hardware, most are impelmented in software microcode. Advanced hardware will re-order and pipeline instructions based upon data dependencies. Sure it's not doing the exact same things the JVM does with bytecode but the point is that x86 assembly is not the language of the hardware. It's a language that the hardware+firmware knows how to interpret and optimize at runtime similar to what your JVM does with java bytecode.
- kazinator 9y ago> Register renaming, cache hierarchies, out of order and speculative execution etc are not visible That's different from C. In the history of x86, most new optimizations have preserved the semantics of code. For instance, register renaming isn't blind; it identifies and resolves hazards. In C, increasing optimization has broken existing programs. C is like a really shitty machine architecture that doesn't detect errors. For instance, overflow doesn't wrap around and set a nice flag, or throw an exception; it's just "undefined". It's easy to make a program which appears to work, but is relying on behavior outside of the documentation, which will change. Computer architectures were crappy like that in the beginning. The mainframe vendors smartened up because they couldn't sell a more expensive, faster machine to a customer if the customer's code relied on undocumented behaviors that no longer work on the new machine. Then, early microprocessors in the 1970's and 80's repeated the pattern: poor exception handling and undocumented opcodes that did curious things (no "illegal instruction" trap).
- gpderetta 8y agoThen again store-load reordering is visible across cores and undefined.
- 0xfaded 9y agoI played a little bit with gpgpu on the raspberry pi. I’d imagine it’s relativly primitive compared to whatever shaders are compiled to on modern GPUs, but it was humbling to have to manage things like separate, per core, disjoint register files which can only be read 4 cycles after write. The cores are heterogeneous, so there is special hardware for exchanging register reads between cores if necessary.
- hencoappel 8y agoWhat language do you use for GPGPU?
- vardump 8y agoSounded like Videocore 4 assembler.
- fixermark 9y agox86 is also there on modern architectures to supply an abstraction that is increasingly divergent from the actual die hardware (but compatible with expectations of e.g. a C compiler that already has an output target for previous x86 hardware). Modern Intel CPUs basically emulate x86; there are many layers of abstraction between individual opcodes and transistor switching.
- ryao 8y agoSpend a week writing in assembly and you will never call C a low level language again.
- kev009 8y agoBy David's postulation, even the native assembly language for the CPU is not low level. See my other comment on the parent topic for justification.
- wmu 8y ago> Register renaming, cache hierarchies, out of order and speculative execution etc are not visible at the assembly / machine code level Cache hierarchies are directly accessible with CLFLUSH, INV, WBINVD x86 instructions; we may count also PREFETCHx, but they call it "a hint". FENCE instructions touch even the multicore part of system. Many low-level CPU concepts leak to higher layer. A bright example is false sharing, which may manifest even in Java or C# programs.