I agree John Regher's take on teaching C: http://blog.regehr.org/archives/1393 In summary, it's a useful skill to have but you're not doing your duty as an instructor if you don't make your students aware of its many shortcomings.
I particularly want to take issue with the claim in the original post that "C helps you think like a computer". C's machine model might have been a good model for the machines at the time at which it was developed, but it hasn't been an accurate model for a long time. A simple example is C's assumption that memory is flat and all locations have an address. With multiple layers of caching and nearly a couple hundred registers in current CPUs this is far from the truth. If you think programming in C is programming at the machine level you are deluding yourself.
> With multiple layers of caching and nearly a couple hundred registers in current CPUs this is far from the truth.
Add to that the aggressive optimisations performed by modern compilers and that mental model breaks completely. It may be a good conceptual model, but it's not the model under which your program actually executes.
However, even programming in assembly keeps you very "far from the truth", since modern CPUs are very complex beasts internally and that complexity is mostly not visible in the ISA.
The thing about speculative execution and out of order execution to remember is that you always get the expected result from a single threaded perspective. If you throw in interrupts or threads, you know you must tread cautiously in C.
Few things have made me more nervous in my career than seeing a ISR making use of C++ standard library features. When I asked the engineer how they were sure this was safe and worked under the covers, I was immediately dismissed as "the compiler takes care of all that." The project only ever worked at -O0 and was still buggy. The engineer blamed the hardware.
> The thing about speculative execution and out of order execution to remember is that you always get the expected result from a single threaded perspective.
Could you elaborate on this? I thought at least speculative execution was handled internally in the CPU and essentially not visible to the user apart from the performance impact?
I’m not sure what you refer to as out-of-order execution, is this execution reordering within the C compiler?
> If you throw in interrupts or threads, you know you must tread cautiously in C.
The x86 instruction set (and other instruction sets), abstract away much the underlying hardware architecture. The "flat memory" abstraction originates from and is presented by most CPU instruction sets -- C just tries to be as close to a "generic" assembly language as possible.
I wonder when we'll get an instruction set that gives us fine-grained control over the different levels of caching, or even the ability to create FGPA-esque microcode functions, etc. Compiler writers could have the time of their lives with such an ISA.
I think with today's CPUs a generic assembly language would at least make a distinction between registers and memory locations. Programming with a conceptually infinite number of registers would be fairly simple and should make reasoning about memory locality easier. Current x86 CPUs have 192 registers internally, so you'd almost never go outside the register file unintentionally.
I'm not very experienced with GPU programming, but my understanding is that the programmer has more control over memory locality there.
Another point to remember is that CPUs have coevolved with programming languages. I think if languages tried to expose more detail in their machine model, ISAs might evolve to match this.
Finally, hardware designers mostly aren't programming language people, and programming language people generally haven't been interested in languages in the same space as C (though that is changing.)
CPUs had registers back when C was invented too :) The reason C doesn't expose registers is because the flat memory model is more universal and more uniform between CPUs than the registers, which often have a lot of special-case behavior, especially on non-RISC CPUs: https://vanemden.wordpress.com/2016/03/15/why-does-c-not-hav.... Besides, C has a "register" keyword, although it's probably ignored by almost all compilers today in favor of their own optimizations, which are usually better (and if they're not, you're usually gonna go to assembly for maximum performance anyway).
The reason C didn't expose registers is because it was specifically made to run on a PDP-7 and PDP-11. That's the sole reason for many of its internal decisions. Doing things differently than a PDP-7 or PDP-11 would've hurt performance. So, they kept it consistent whenever performance or complexity was critical.
Decisions like that and x86 is why stack operations are still dangerous despite B5000 (1961) having protected stack and MULTICS (UNIX/C predecessor) having reverse stack immune to overflow. C kept the shitty PDP stack instead of MULTICS for performance. Reverse stack on x86 still has performance hit due to that decision. It's why Itanium did shadow stack in hardware.
Not clever design or forward thinking. Just making it work on a PDP with long-term problems following.
Yeah, I've used the keyword myself but it was discouraged. Instead, we were taught to work with the local variables, stack, frame, heap, etc. It's technically in there at some point as a compiler hint but the internal structure reflects the PDP's. So, I partly take the comment back in that it was there but not standard practice.
I've done a bit of reading and I believe the blog post you linked is incorrect. For example, it claims that an innovation of C was to have assignment as a generalised move operation, rather than load and store operations (and introducing registers).
One of the secrets behind C’s success is that just about any machine architecture has byte-addressable random-access memory. Moreover, pick just about any pair of machine architectures, and they differ in their register structures. By excluding registers from its computational model, C has hit the sweet spot in being close to the machine, yet not too close. So there is no place for LOAD and STORE in C’s computational model, while the assignment operator is an operation on random-access memory in the presence of an unspecified complement of registers.
Finally the PDP-11 had 8 general purpose registers, with two taken for program counter and stack. It seems that main memory loads cost about an extra cycle. So register allocation was not a pressing issue. There aren't enough registers to make sophisticated register allocation worthwhile, and you don't pay a high penalty on a register spill. This is in contrast to all modern machines (e.g. current Intel chips have 192 registers internally and a cache miss can cost a hundred cycles [http://stackoverflow.com/questions/4087280/approximate-cost-...)
I'm coming to the same conclusion as nickpsecurity: C was designed specifically to run on the PDP-11, taking advantage of its instruction set, and we're still paying the price of those decisions.
It's interesting to imaging what a better low-level for, e.g, the Parallela Epiphany-V currently on the front page would look like.
still better to have some concept on how memory works though, even if it's still an abstracted version of what happens on the machine. If all you ever see is Java or Python, the mental step to understanding memory optimization and caching becomes much larger.
I think there are a couple of ways that it could be argued that "C helps you think like a computer": most CPUs have been designed around running C code quickly and the oddities of the language force you to think like the compiler (which has both positive and negative aspects :/ ). For that matter, things like UNIX and POSIX make more sense if you underderstand C (and vice versa) and there are multiple complete and fairly popular open source operating systems you can examine that are written in C (and the mailing lists of these operating systems can be excellent places to learn a bunch of low level stuff). On the negative side (as you mention), I think it is very common to think of C as a lower level language than it really is (I'm planning on learning Ada, which it sounds like has more low level features, although still not the same as learning CPU architecture).
C forces or encourages you to learn a lot more about everything than other languages, which not a great thing if you just want to write a reliable program but isn't such a horrible deal if learning such things is part of the point. D, Ada, and to a lesser extent Rust seem similar in some respects and with significant benefits but without the easily accessible operating system projects (so far, that I know of).
So I guess my addition to that excellent post would be that if you are going to learn C you should get involved with an OS project. The BSDs are particularly good for this due to being more integrated systems and slower moving with more design consideration given to new features than in Linux. I'd personally recommend NetBSD, where the tech-* mailing lists are high quality and fairly low volume, but any of them should work well.
There's actually a very strong hobby kernel chunk of the Rust community, and it's fairly education-focused. http://os.phil-opp.com/ kicked it off, and my own project based on it, http://intermezzos.github.io/ is coming along.
There's also more serious efforts: https://www.redox-os.org/ has made tremendous progress in a relatively short period of time, https://robigalia.org/ is an extremely interesting effort for Rust on top of seL4.
Very cool, although not yet the same as well established production operating systems. I think Rust will be a great alternative to C in a decade and worth considering even now.
In case it wasn't clear, my "to a lesser extent Rust" comment was just about my impression that Rust can more easily be used without worrying about optimization level details vs. D or Ada or C. Which is a good thing if true since Rust can also deal with the low level stuff, although possibly it is just my mistaken impression (I only actually know C out of these languages).
I do think that you, overall, need to care less about optimization-level-details in Rust than in those other languages. But that's because we've made it so that the default ways you do things are already plenty fast; the idea is that the nice way of doing things is also the fast way of doing things, so there's no "well, looks like I can't do it how I want, time to replace it with something gross but performant." Of course, this can still pop up from time to time, but it happens more rarely.
I think you're definitely correct but it begs the question: is there another modern language that does help one get "closer to the machine" conceptually in a correct manner?
Further, C helps you think like a particular evolutionary path of computer, and the language and architecture developed mostly in parallel. It's like saying that giraffes have long necks because they need them to reach the tall branches.
I particularly want to take issue with the claim in the original post that "C helps you think like a computer". C's machine model might have been a good model for the machines at the time at which it was developed, but it hasn't been an accurate model for a long time. A simple example is C's assumption that memory is flat and all locations have an address. With multiple layers of caching and nearly a couple hundred registers in current CPUs this is far from the truth. If you think programming in C is programming at the machine level you are deluding yourself.