optimization(3)

πŸ“¦ version 2.2.1, in fedora 43, fedora 44, opensuse-leap 16.0

🌸 NAME 🌸

optimization β€” compiler optimization, bestie. that's it, that's the whole vibe. and specifically? the chaos that happens when your compiler decides it knows better than you about ordering. πŸ’…


πŸ’– Problems with reordering code πŸ’–

Author: Jan Waclawek

okay so listen. 🫢 programs are literally just sequences of statements, right? and a naive lil baby compiler would just do them in exactly the order you wrote them, no thoughts, head empty. but an optimizing compiler? she's got main character energy and she is free to reorder your statements β€” or even just parts of them β€” as long as the resulting "net effect" comes out the same.

and what even is the "net effect"? the standard calls it side effects, and it's accomplished exclusively through accesses (reads and writes) to variables qualified as volatile. so as long as all volatile reads and writes hit the same addresses, in the same order, and the writes write the same values, the program is correct β€” no matter what else is going on in there. periodt. ✨

one huge thing to note here: the time duration between consecutive volatile accesses is not considered at all. like, not even a little. πŸ’€

unfortunately 😭 there are also operations that volatile accesses just do not cover. iconic example in avr-gcc/avr-libc: the cli() and sei() macros defined in <avr/interrupt.h>, which convert directly into the respective assembler mnemonics through the __asm__() statement. these don't constitute a variable access at all β€” not even a volatile one β€” so the compiler is free to move them around like they're nothing. πŸ™ˆ

now, there is a 'volatile' qualifier you can attach to the __asm__() statement, but its effect on (re)ordering is not clear from the documentation (and it's more likely only there to prevent complete removal by the optimiser). as it says (among other things):

Note that even a volatile asm instruction can be moved relative to other code, including across jump instructions. [...] Similarly, you can't expect a sequence of volatile asm instructions to remain perfectly consecutive.

See also:


πŸ¦‹ memory barriers (the other girl) πŸ¦‹

there's another mechanism that gets you something similar: memory barriers. this is done by adding a special 'memory' clobber to the inline asm statement, and it ensures that all variables are flushed from registers to memory before the statement, and then re-read after the statement.

ngl, the purpose of memory barriers is slightly different from enforcing code ordering β€” it's supposed to make sure there are no variables "cached" in registers, so it's safe to change register contents, e.g. when switching context in a multitasking OS. (on "big" processors with out-of-order execution they also imply usage of special instructions that force the processor into an "in-order" state β€” this is not the case on AVRs, just so you know. πŸ’…)

BUT. here's the tea. πŸ“ a memory barrier does work well at ensuring that all volatile accesses before and after the barrier occur in the given order with respect to the barrier. however, it does not ensure the compiler isn't moving non-volatile-related statements across the barrier. Peter Dannegger gave a gorgeous little example of this effect:

#define cli() __asm volatile( "cli" ::: "memory" )
#define sei() __asm volatile( "sei" ::: "memory" )

unsigned int ivar;

void test2( unsigned int val )
{
  val = 65535U / val;

  cli();

  ivar = val;

  sei();
}

compiles with optimisations switched on (-Os) to:

00000112 <test2>:
 112: bc 01          movw r22, r24
 114: f8 94          cli
 116: 8f ef          ldi  r24, 0xFF ; 255
 118: 9f ef          ldi  r25, 0xFF ; 255
 11a: 0e 94 96 00    call 0x12c     ; 0x12c <__udivmodhi4>
 11e: 70 93 01 02    sts  0x0201, r23
 122: 60 93 00 02    sts  0x0200, r22
 126: 78 94          sei
 128: 08 95          ret

where the potentially slow division got moved across cli(), resulting in interrupts being disabled longer than intended. 😭

note that the volatile access does occur in order with respect to cli() or sei() β€” so the "net effect" required by the standard is achieved as intended. it's "only" the timing that's off. but for most embedded applications, timing is an important, sometimes critical factor. πŸ”₯

See also:


πŸ’€ so what do we do πŸ’€

unfortunately, at the moment, in avr-gcc (nor in the C standard), there is no mechanism to enforce complete match of written and executed code ordering β€” except maybe switching the optimization completely off (-O0), or writing all the critical code in assembly. that's the whole menu, bestie. πŸ™ˆ

to sum it up:

  • memory barriers ensure proper ordering of volatile accesses
  • memory barriers don't ensure statements with no volatile accesses to be reordered across the barrier

and that's the page, bestie!! πŸŽ€ this was avr-libc optimization(3), Version 2.1.0, dated Sat Jan 29 2022 β€” go forth and order your volatile accesses correctly, you absolute icon. πŸ‘‘πŸ’–βœ¨

πŸ“¦ where this page comes from