I've recently decided to give C a go and wondered whether it performs obvious optimisations, so I don't have to bother with assembly.
The optimizations the compiler can and knows how to perform, are rather funky set, compared to what seems obvious to us as human programmers.
A lot of this is due to how the C standard defines itself via an
abstract machine, using
sequence points and
side effects. (I recommend glancing through the final revision,
n1256.pdf, of the C99 standard that is available on the net, and then building on top of that with
n1570.pdf (the same for C11) and
n2176.pdf (the same for C17). While the standards describe the common functionality, compilers can and occasionally do diverge (often not intentionally, but due to language-lawyerism dispute of what a specific paragraph in the standard actually means), especially when compile-time options (that specify such diversion, like say "unsafe-math" optimizations) are used, so do let reality dictate your choices, instead of using the standard as the book of Law that is not to be crossed. Reality beats theory every time.)
For example, it may come as a surprise, or not, that the compiler is not allowed to reorder the members in a structure, or that a simple cast limits a numeric expression to the range and precision of that type but does not force the compiler to use a temporary variable, or that an union is a practical way to type-pun (reintrepret the same memory pattern as a different type), and so on. Many C programmers do not know that there is a
freestanding environment where the standard C library is not available (only some of the header files are), and the typical environment is the
hosted environment; and that many embedded environments actually support a funky mix of freestanding C and a subset of freestanding C++ (omitting standard C++ library, exceptions, and perhaps other stuff depending on the target).
Freestanding C is less implementation-defined than freestanding C++, and many embedded targets have partial standard C library support via avr-libc, newlibc, or nanolibc.
Using C as a tool to produce specific assembly is becoming harder and harder, because the compilers – especially gcc and clang – are implementing more and more optimizations at the abstract syntax tree level: detecting uses and patterns, rather than idioms. It is important to realize that the way the C standard is specified, makes some "obvious" optimizations impossible, simply because the C standard requires the abstract machine to perform certain steps in certain order, and the obvious optimization changes that.
If you intend to use C (or the mix of freestanding C and freestanding C++) in embedded and resource-constrained targets as opposed to in hosted environments (like in Linux, Windows, Macs, BSDs in general, where the standard libraries are available), then I'd recommend looking at
GCC/ICC/Clang extended assembly, because
that can be a powerful tool in defining macro-like functions; with a key difference being that you can let the compiler choose the registers to use if they don't matter, via machine constraints. The compiler can optimize both the code around such, as well as the registers used in the assembly itself if you want; and if inlined, do it separately at each inline site. As I said before, typically you'll only want to examine the generated code for pathologically bad patterns, and not try to finesse each C expression to produce the exact code you want: that way lies only frustration and hair loss.
If you intend to use C for systems programming, take a serious look at the
POSIX.1-2017 interfaces that are provided by all POSIXy systems including Linux, Mac OS, and *BSDs. In particular, I often rant that when an example or tutorial uses
fgets() (and not
getline()), one ought to ignore that example or exercise; and the same applies to anything using
opendir(),
readdir(), and
closedir() to traverse directory trees instead of using
nftw(),
scandir(),
glob(), or the
fts family of functions (originating in BSDs, but included in Linux and other POSIXy systems standard C libraries).
These are the facilities provided by the standard libraries every binary is linked against that one is expected to use in the real life, and not the antiquated poor C89 ones. You'll also find localization, character set conversion (
iconv), and even atfile support (meaning you can give a directory descriptor as a parameter to use as the base directory if the pathname parameter is relative path), and lots more. I also recommend looking at the
Linux man-pages project (sections 2 and 3, specifically), since it is currently the best maintained man page repository for functionality included in standard C libraries, and each page contains a section
Conforming to, which describes where that functionality is defined, and therefore where one can expect it to be available. (On all Linux distributions, the man pages available include this set, plus any man pages provided by installed packages; so this is NOT the "complete set of man pages" in Linux, just the core set.)