I think the author misses the point of the phrase "C is just portable assembly". It does't literally mean that C is just like assembly, it's usually a shorthand for saying the following things:
1. C is used in most contexts where you previously had to use assembly, and would use assembly if C didn't exist.
2. C is the language with the "lowest level" code imaginable.
3. What this means is, you can map almost any command in C into a specific command or a few specific commands in assembly.
That last line is the important part. C is optimized in the sense that every command in C will give you a deterministic amount of commands in assembly. When reading C code, someone who knows assembly can usually tell you what will happen in the compiled code. You won't see an operator which doesn't have a deterministic runtime or memory footprint. In fact, most of the questions on "Why C has this"/"Why C doesn't have that" can be answered exactly like that: you can't implement it with a deterministic set of commands.
In those senses, C is just like Assembly, at least more so than any other language around.
"every command in C will give you a deterministic amount of commands in assembly"
This is most definitely not true on any modern compiler. The generated assembly depends on the program as a whole and may not correspond fragment to fragment in any readily comprehensible way.
One example is a variable in C may correspond to a number of different storage locations at different points in the program graph, owing to SSA form (or individual optimizations that transform the program in a similar manner).
Another example is how pointer aliasing affects generated code: copying arguments into temporaries before arithmetic can actually result in fewer assembly instructions as the compiler can determine no aliasing is possible.
Add to this the more mundane and well understood optimizations like inlining, constant propagation, later passes of optimizations over the results of the prior and the result is that it's very hard to know exactly what assembly will correspond to any arbitrary part of a c program. Mapping memory locations to variables or assembly instructions to c program lines is not trivial.
I know this seems like nitpicking, but programmers using the mistaken mental model of c being textual macros for assembly leads to poorly performing code at best, and a variety of security vulnerabilities at worst. I think if you actually need to know what's going on at the machine level it's very important to know that the c abstract machine is most definitely NOT what the real machine on your desk is doing.
> 3. What this means is, you can map almost any command in C into a specific command or a few specific commands in assembly.
Is that really true? It depends very much on your compiler and on the optimizations that it uses. I'd say that the level of optimization performed is proportional to the similarity between input and output. Straightforward translation makes for crappy optimization.
The important point is that a C command will result in a deterministic amount of actual commands.
For example, C won't implement the "raise to nth power" operator (in Python, 210 means 2 to the power of 10, for example), because it can't be done with one assembly instruction, only with a loop.
Even with optimizations, you can reason about your code as if every C command is one Assembly command, and you won't be far off (you can say that every C command is O(1) assembly commands). This is very different from other languages.
C has a division operator, despite the fact that plenty of CPUs (ARM, for a common example) don't have a divide instruction. Typically, the compiler generates a call to a runtime support library (e.g. libgcc) with division functions for various datatypes, and yes, those functions typically involve some looping.
That's true, and it's also a bit of a nitpick. How many of us actually work on multiple architectures or with different C compilers? For most systems-level stuff, you are simply using gcc on x86 linux, with plain-vanilla glibc, so honestly, I know what that div op is going to map to. If you've been at this for a while, you know what to expect when you write a simple for loop vs some pointer arithmetic. And if you happen to be an ARM guy, you probably are familiar with all the quirks of your platform as well.
I agree that experience will develop an intuition for how C statement map to assembly. But experience is no replacement for standards -- no implementation defines the language. In most cases, it's more useful to reason about the guarantees within the language rather than inferring what the compiled assembly will be. Possible? Yes, if as you said development is on homogeneous platforms. Painful? If you need to know how 5 compilers act instead of 1, I suspect so.
The point is not to know the actual assembly. The point is that you'll never run an operation that might take a non-deterministic amount of time to run.
Contrast to, say, Python (as an obviously exaggerated counterexample). Just calling a function in Python means performing a lookup in a hash table.
Dividing one 32-bit number by another is a constant-time operation even if on some machines (e.g. ARM) it is implemented as a loop. This is what matters. Raising a number to the nth power is an O(log n) operation.
To decode? Why does the user care how long it takes to decode, they care how long it takes to run. We can provide upper bounds for exp, sin, and erfc too.
"3. What this means is, you can map almost any command in C into a specific command or a few specific commands in assembly."
Do you have any idea how wildly inaccurate that statement is? If you have no experience at all with assembly on any processor, or perhaps only on the x86 family, then I can understand your statement. It's still wildly inaccurate, though.
1. C is used in most contexts where you previously had to use assembly, and would use assembly if C didn't exist.
2. C is the language with the "lowest level" code imaginable.
3. What this means is, you can map almost any command in C into a specific command or a few specific commands in assembly.
That last line is the important part. C is optimized in the sense that every command in C will give you a deterministic amount of commands in assembly. When reading C code, someone who knows assembly can usually tell you what will happen in the compiled code. You won't see an operator which doesn't have a deterministic runtime or memory footprint. In fact, most of the questions on "Why C has this"/"Why C doesn't have that" can be answered exactly like that: you can't implement it with a deterministic set of commands.
In those senses, C is just like Assembly, at least more so than any other language around.