It will teach you how a PDP-11 thinks, at best. The cache hierarchy and speculative execution are completely hidden at the level of C, to start with.
You want to learn how computers think, pick up an assembly language. (Any one will do.) You want to build portable low-level software, learn Rust, Zig, C, or C++ (in my personal order of preference).
[0] https://www.intel.com/content/www/us/en/developer/articles/t...
The performance gains of fine-tuned code is typically two orders of magnitude higher
A quick glance at Intel's Software Developer's Manuals [0] falsifies this:
>> TLB and Cacheability control: CLFLUSH, CLFLUSHOPT, CLWB, INVD, WBINVD, INVLPG, INVPCID, and memory instructions with a non-temporal hint (V/MOVNTDQA, V/MOVNTDQ, V/MOVNTI, V/MOVNTPD, V/MOVNTPS, V/MOVNTQ, V/MASKMOVQ, and V/MASKMOVDQU).
> [..] speculative execution are completely hidden at the level of assembly too
Section 18.1.13 specifically mentions side-channels for speculative execution, and there are more than 100 other matches for "speculative" across the document (some of which also refer to load barriers).
So no, these things are not completely hidden at the assembly level, and at least in the case of the (or one of the?) most popular consumer CPU architecture in the world, they are actively documented in the primary reference for an assembly programmer.
[0] https://www.intel.com/content/www/us/en/developer/articles/t...
And error recovery (that is fucking crazy on x86), and wide instructions, and I/O, and stack management, and... I am not sure C is even a good approximation for the PDP-11.
For which you'd need to be familiar with a low-level language like C.
It's a good way of helping people with no programming knowledge get past the stage of "what are those magic colourful words you type that somehow make the computer do things?".
https://tobiasvl.github.io/blog/write-a-chip-8-emulator/
I haven't made a Gameboy emulator, just learned the assembly so I could modify some roms.
You can check out my blog post[1] if you're interested how I went about it (there are a couple resources at the bottom I used to build it, including the one that HideousKojima recommended).
As for language, in hindsight, C was fine. CHIP-8 is so basic that you don't really need to worry about performance or to implement a JIT compiler unless you want to learn how to do those things specifically. Just pick any one out of the three (assembly is a bit of a weird choice though, why not write a CHIP-8 emulator and then a brand new program to run on that emulator in CHIP-8 assembly if you want to learn assembly?)
As for Gameboy, it would have more instructions, a different graphics system, sound etc. and overall be more complicated than CHIP-8. Try it out if you either feel a bit more adventurous or have implemented CHIP-8.
[0](https://sr.ht/~gotlou/chip8-emulator) [1](https://gotlou.srht.site/chip8-emulator.html)
Check this post out - Should you learn C to "learn how the computer works"? (https://steveklabnik.com/writing/should-you-learn-c-to-learn...). Spoiler alert - no.
I think summarizing that article as "no" really undercuts how diplomatic and reasonable Steve Klabnik is in it.
But is it necessary to understand now a computer works? No.
You can also check out "C is how the computer works" is a dangerous mindset for C programmers (https://steveklabnik.com/writing/c-is-how-the-computer-works...)
If, however, we're talking about the kernel then perhaps we should listen to what Torvalds says about this topic.
"People who designed C, designed it at a time where the language had to be geared towards the output, so when I read C I can think about the Assembly that it will create":
http://www.youtube.com/watch?v=MShbP3OpASA&t=20m45s
Consider that he said this in a time with C++, even if Rust was in its infancy: it is not as tied to the assembly it produces. Not by a long shot.
The issue with Assembly directly is that it's absurdly non-portable, too low level and too unstructured to do anything reasonable with it.
There's a reason that Linux is written in C and not ASM Directly.
Even interpreting this as charitably as possible--that you don't mean literally learning assembly but rather learning everything besides specific assembly syntax that you would have picked up from learning assembly--this claim doesn't hold water.
For starters, C isn't that much closer to hardware than, say, Java. The main differences you have with C are that a) C has a pointers-are-almost-integers model [1] and b) C has a more limited runtime than other languages. C is still a fundamentally based on an abstract machine semantics model, and that abstract machine doesn't have a close bearing on modern hardware.
In particular, C does not distinguish between registers and memory, and if I had to pick the single most salient feature of modern hardware you need to understand well to be able to say you understand how computers work, it's the register/memory distinction. If you think processors are mostly like they were in the 1970s--when you could say "add 1 to this memory location"--then C's shrugging off the register/memory distinction makes sense, but this is a situation that hasn't been true for several decades.
Another failure of C is that it's far less expressive than assembly. There are features in other languages that are impossible to express in C. How do you write a function with multiple entry points, as you can in Fortran? Or a coroutine, as you can in Algol? Or nested functions, as in Pascal? Or try-catch, as in C++? Or functions with multiple return values, as in Matlab? Indeed, how do you write something like JMP *reg in C, a useful primitive for state machines?
No, C does not come close to being a good description of modern processors, where "modern" means "anything that came out since I was born."
[1] Not going to open up the can of worms that is pointer provenance.
Just nitpicking, but doesn't C have the register keyword? (I know it does nothing at all nowadays, but AFAIK it did make a slight difference back in the dark ages.)
Your C compiler might conclude - even if it doesn't decide to put the variable in a register - that since the variable notionally doesn't have an address some optimisations are available which it can't otherwise be sure are safe.
[1] Okay, the official semantics of "volatile" strictly speaking are completely orthogonal to memory, but the practical effect that everyone agrees on (don't optimize this memory access) is inherently oriented towards memory, and the fact that the semantics are so vague about what it actually does is partially a reflection of the fact the semantics themselves don't distinguish between register and memory.
I don't believe this at all. You won't learn how to deal with limited numbers of registers. You won't learn about method prologues and epilogues. You won't learn about oodles of stuff.
Those things takes like an hour to learn, translating C to assembly is trivial, it takes time and will be verbose but it isn't hard at all. So even though you don't learn all the details you still learn all the higher level stuff related to assembly programming when you program in C.
This isn't true for other languages, translating arbitrary C++ or Java or Javascript to assembly is really hard, so they don't teach you to think like an assembly programmer.
I mean; you will likely end up stepping through debuggers where you will see registers being used. At least if you learned C like I learned C.
Similarly for Prologue and Epilogue.
"why does a function call have a cost" is a common question and it's solved forever after an hour googling.
Rust is it's own thing; I personally really like it but going from C to Rust is harder than just going to Rust as you have to "unlearn" C paradigms.
You'll probably be "better" if you go C first, but it will take a lot of time and energy.
In the end its completely up to you.