C might be low level from the perspective of other systems languages, but that is like calling Apollo 11 simple from the perspective of modern spacecraft. C as written is not all that close to what actually gets executed.
For a small example, there are many compilers who would absolutely skip incrementing 'a' in the following code:
uint32_t add_and_subtract_1(uint32_t a) {
a += 1;
a -= 1;
return a;
}
Even though that code contains `a += 1;` clear as day, the chances of any incrementing being done are quite small IMO. It gets even worse in bigger functions where out-of-order execution starts being a thing.
Why would you want it to increment 1 if we decrement 1 from the same variable? That would be a waste of cycles and a good compiler knows how to optimize it out, or what am I misunderstanding here? What do you expect "it" to do and what does it really do?
I'm pretty sure that's replying directly to the comment about how c is close to assembly and that if you add that line of code somewhere you know there's a variable getting incremented. Doesn't really matter whether or not it's useful, the point is that the behavior isn't exactly what you wrote
To reiterate, claiming that C can be described as "portable assembly" is not a claim that it is literally a package of assembler macros that emit deterministic machine code for each individual source expression.
I linked these in another comment, but here's some examples of straightforward-looking integer addition emitting more complex compiler output for other languages that compile to native code:
The C standard guarantees certain behaviours that will not change, even if your C compiler changes. That's the whole point of the standard. And it has nothing to do with the problem of induction.
But the standard does not guarantee that specific assembly instructions will be used.
That’s a contrived example but in a serious program there would often be code in between or some level of indirection (e.g. one of those values is a lookup, a macro express, or the result of another function).
Nothing about that is cheating, it just says that even C programmers cannot expect to look at the compiled code and see a direct mapping from their source code. Your ability to reason about what’s actually executing requires you to internalize how the compiler works in addition to your understanding of the underlying hardware and your application.
What optimizer would remove the increment/decrement if the value was accessed in between? That seems like something that would be really easy to detect.
It would be very normal for a compiler to do an increment (or merge it into a later instruction), but never do the decrement, and instead use the old copy of the value.
Then in the next step, it would see that the result of the increment is never used and thus the increment instruction is dead code and can also be removed.
In what languages can you do that that is not assembly though? The higher level the language is, the "worse" or difficult it gets, perhaps I am not following the thread right.
Yes, this is normal for languages. The only pushback here is against the term “portable assembler” being applied to C, where it’s incomplete often enough that many people feel it’s no longer a helpful label.
I think it’s also reflecting the maturity and growth of the industry. A turn of the century programmer could relatively easily find areas where dropping down to assembly was useful, but over the subsequent decades that’s become not only uncommon but often actively harmful: your code hand-optimized for a particular processor is likely slower on newer processors than what a modern compiler emits and is definitely a barrier to portability in an era where not only are ARM and potentially RISC-V of interest but also where code is being run on SIMD units or GPUs. This makes the low-level “portable assembler” idea less useful because there’s less code written in that middle ground when you want either a higher-level representation which gives compilers more flexibility or precise control. For example, cryptography implementers want not just high performance but also rigid control of the emitted code to avoid a compiler optimizing their careful constant-time implementation into a vulnerability.
I'm not an embedded expert but a friend of mine has complained about compiler optimizations breaking things in his programs. I could see incrementing by one being used to set some bits in a memory location for a cycle that may mean something to some peripheral and then decrementing by one to set some other bits that may mean something else. In that case, the compiler removing those two lines would cause a very hard to debug issue.
> It gets even worse in bigger functions where out-of-order execution starts being a thing.
In addition, add that your processor isn't actually executing x86 (nor ARM etc) instructions, but interprets/compiles them to something more fundamental.
So there's an additional layer of out-of-order instructions and general shenanigans happening. Especially with branch prediction in the mix.
For a small example, there are many compilers who would absolutely skip incrementing 'a' in the following code:
Even though that code contains `a += 1;` clear as day, the chances of any incrementing being done are quite small IMO. It gets even worse in bigger functions where out-of-order execution starts being a thing.