Jump to content
Sign in to follow this  
pabigot

An exercise in advanced C semantics

Recommended Posts

Dealing with memory mapped registers, and ensuring the consistency of shared values in the face of interrupts, often requires use of the volatile qualifier in C.

Given the following code:

typedef struct sFIFO {
  volatile uint16_t head;
  volatile uint16_t tail;
  const uint16_t length;
  volatile uint8_t buffer[1];
} sFIFO;

#define FIFO_ADJUST_OFFSET_NI(fp_,v_) ((v_) % (fp_)->length)

static __inline__
int
fifo_push_head_ni (sFIFO * fp, uint8_t v)
{
  uint16_t h = fp->head;
  /* ... */
#if PICK_ONE
  h = fp->head = FIFO_ADJUST_OFFSET_NI(fp, 1 + h);
#else
  fp->head = h = FIFO_ADJUST_OFFSET_NI(fp, 1 + h);
#endif
  /* ... */
}

consider these questions:

  • Does the value of PICK_ONE affect the code generated for the statement variant it selects?
  • Does your answer hold for all C compilers?
  • Can you find the part of the C11standard (draft N1570) that supports your answers to the first and second questions?

If you're not interested in guessing, the answer for GCC is here, and text that supports that decision explicitly is in footnote 111 at section 6.5.16 paragraph 3 of N1570 (ISO/IEC 9899:2011), possibly supported by the last sentence of section 6.7.3 paragraph 7.

I was surprised by the answer.

Share this post


Link to post
Share on other sites

Obscurity of the sort I used in the last post irritates me, so let me correct it by providing the details of the teaching I meant to convey:

 

The basic understanding of volatile-qualified objects in C is that the qualifier forces the compiler to execute exactly the operations specified in the source: the optimizer cannot add, remove, or change the order of reads and writes.

 

This is true. What's also true is that, subject to certain limitations imposed by the presence of sequence points, the C standard states that the definition of access to a volatile object is implementation-defined (6.7.3 graf 7).

 

In a more simple example:

/* N is a normal variable */
unsigned char n;
/* INVERTER is a MCU register which, when read, provides the bitwise
 * inverse of the last value that was written to it. */
extern volatile unsigned char INVERTER;
/* ... */
n = INVERTER = 0x5a;
The value of n after the last expression may be 0x5a or it may be 0xa5, depending on what compiler and optimization flags you use, because the language does not require that INVERTER be read after it's written.  Both are values that may be provided by a conforming C implementation. gcc specifies that n will be 0x5a.

 

Similarly and related to a CMSIS discussion on Stellarisiti in this code:

  v = do_set ? (v | p) : (v & ~p);
whether v is read once or twice is implementation defined. gcc will read it once.

 

In the original case of:

#if PICK_ONE
  h = fp->head = FIFO_ADJUST_OFFSET_NI(fp, 1 + h);
#else
  fp->head = h = FIFO_ADJUST_OFFSET_NI(fp, 1 + h);
#endif
I had written the first version.  While analyzing code size, I looked closely and thought "Oh, that's got an unnecessary read, I'll change the order".

 

Doing this had no effect on code size, which confused me.  So I tried it with gcc for x86: same behavior.  Then I tried it for mspgcc: same behavior.  (Which really threw me, because three years ago I fixed a huge number of bugs in how mspgcc manipulated MCU registers that are declared volatile, and I was sure I'd done it "right".)

 

Turns out my notion of "right" was wrong.

 

So the lesson: Use volatile, but be aware of its subtleties, and (as always) don't try to be too smart.  I should have written:

  h = FIFO_ADJUST_OFFSET_NI(fp, 1 + h);
  fp->head = h;
in the first place.

Share this post


Link to post
Share on other sites

I toyed with your original example with arm-none-eabi-gcc and got different results.  Meant to post this but ran out of time.

 

Using arm-none-eabi-gcc -mthumb -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=softfp -MD -std=c99 -Wall -pedantic -g -c test.c

With #define PICK_ONE 1:

test.o:     file format elf32-littlearm


Disassembly of section .text:

00000000 <fifo_push_head_ni>:
#define FIFO_ADJUST_OFFSET_NI(fp_,v_) ((v_) % (fp_)->length)
#define PICK_ONE 1

int
fifo_push_head_ni (sFIFO * fp, uint8_t v)
{
   0:   b480            push    {r7}
   2:   b085            sub     sp, #20
   4:   af00            add     r7, sp, #0
   6:   6078            str     r0, [r7, #4]
   8:   460b            mov     r3, r1
   a:   70fb            strb    r3, [r7, #3]
  uint16_t h = fp->head;
   c:   687b            ldr     r3, [r7, #4]
   e:   881b            ldrh    r3, [r3, #0]
  10:   81fb            strh    r3, [r7, #14]
  /* ... */
#if PICK_ONE
  h = fp->head = FIFO_ADJUST_OFFSET_NI(fp, 1 + h);
  12:   89fb            ldrh    r3, [r7, #14]
  14:   3301            adds    r3, #1
  16:   687a            ldr     r2, [r7, #4]
  18:   8892            ldrh    r2, [r2, #4]
  1a:   fb93 f1f2       sdiv    r1, r3, r2
  1e:   fb02 f201       mul.w   r2, r2, r1
  22:   1a9b            subs    r3, r3, r2
  24:   b29b            uxth    r3, r3
  26:   687a            ldr     r2, [r7, #4]
  28:   4619            mov     r1, r3
  2a:   8011            strh    r1, [r2, #0]
  2c:   81fb            strh    r3, [r7, #14]
#else
  fp->head = h = FIFO_ADJUST_OFFSET_NI(fp, 1 + h);
#endif
  /* ... */
}
  2e:   4618            mov     r0, r3
  30:   3714            adds    r7, #20
  32:   46bd            mov     sp, r7
  34:   f85d 7b04       ldr.w   r7, [sp], #4
  38:   4770            bx      lr
  3a:   bf00            nop

Without #define PICK_ONE:

test.o:     file format elf32-littlearm


Disassembly of section .text:

00000000 <fifo_push_head_ni>:
#define FIFO_ADJUST_OFFSET_NI(fp_,v_) ((v_) % (fp_)->length)
//#define PICK_ONE 1

int
fifo_push_head_ni (sFIFO * fp, uint8_t v)
{
   0:   b480            push    {r7}
   2:   b085            sub     sp, #20
   4:   af00            add     r7, sp, #0
   6:   6078            str     r0, [r7, #4]
   8:   460b            mov     r3, r1
   a:   70fb            strb    r3, [r7, #3]
  uint16_t h = fp->head;
   c:   687b            ldr     r3, [r7, #4]
   e:   881b            ldrh    r3, [r3, #0]
  10:   81fb            strh    r3, [r7, #14]
  /* ... */
#if PICK_ONE
  h = fp->head = FIFO_ADJUST_OFFSET_NI(fp, 1 + h);
#else
  fp->head = h = FIFO_ADJUST_OFFSET_NI(fp, 1 + h);
  12:   89fb            ldrh    r3, [r7, #14]
  14:   3301            adds    r3, #1
  16:   687a            ldr     r2, [r7, #4]
  18:   8892            ldrh    r2, [r2, #4]
  1a:   fb93 f1f2       sdiv    r1, r3, r2
  1e:   fb02 f201       mul.w   r2, r2, r1
  22:   1a9b            subs    r3, r3, r2
  24:   81fb            strh    r3, [r7, #14]
  26:   687b            ldr     r3, [r7, #4]
  28:   89fa            ldrh    r2, [r7, #14]
  2a:   801a            strh    r2, [r3, #0]
#endif
  /* ... */
}
  2c:   4618            mov     r0, r3
  2e:   3714            adds    r7, #20
  30:   46bd            mov     sp, r7
  32:   f85d 7b04       ldr.w   r7, [sp], #4
  36:   4770            bx      lr


Compiler details:

Using built-in specs.
COLLECT_GCC=arm-none-eabi-gcc
COLLECT_LTO_WRAPPER=/opt/local/gcc-arm-none-eabi-4_8-2013q4/bin/../lib/gcc/arm-none-eabi/4.8.3/lto-wrapper
Target: arm-none-eabi
Configured with: /home/build/work/GCC-4-8-build/src/gcc/configure --target=arm-none-eabi --prefix=/home/build/work/GCC-4-8-build/install-native --libexecdir=/home/build/work/GCC-4-8-build/install-native/lib --infodir=/home/build/work/GCC-4-8-build/install-native/share/doc/gcc-arm-none-eabi/info --mandir=/home/build/work/GCC-4-8-build/install-native/share/doc/gcc-arm-none-eabi/man --htmldir=/home/build/work/GCC-4-8-build/install-native/share/doc/gcc-arm-none-eabi/html --pdfdir=/home/build/work/GCC-4-8-build/install-native/share/doc/gcc-arm-none-eabi/pdf --enable-languages=c,c++ --enable-plugins --disable-decimal-float --disable-libffi --disable-libgomp --disable-libmudflap --disable-libquadmath --disable-libssp --disable-libstdcxx-pch --disable-nls --disable-shared --disable-threads --disable-tls --with-gnu-as --with-gnu-ld --with-newlib --with-headers=yes --with-python-dir=share/gcc-arm-none-eabi --with-sysroot=/home/build/work/GCC-4-8-build/install-native/arm-none-eabi --build=i686-linux-gnu --host=i686-linux-gnu --with-gmp=/home/build/work/GCC-4-8-build/build-native/host-libs/usr --with-mpfr=/home/build/work/GCC-4-8-build/build-native/host-libs/usr --with-mpc=/home/build/work/GCC-4-8-build/build-native/host-libs/usr --with-isl=/home/build/work/GCC-4-8-build/build-native/host-libs/usr --with-cloog=/home/build/work/GCC-4-8-build/build-native/host-libs/usr --with-libelf=/home/build/work/GCC-4-8-build/build-native/host-libs/usr --with-host-libstdcxx='-static-libgcc -Wl,-Bstatic,-lstdc++,-Bdynamic -lm' --with-pkgversion='GNU Tools for ARM Embedded Processors' --with-multilib-list=armv6-m,armv7-m,armv7e-m,armv7-r
Thread model: single
gcc version 4.8.3 20131129 (release) [ARM/embedded-4_8-branch revision 205641] (GNU Tools for ARM Embedded Processors) 

Share this post


Link to post
Share on other sites

In some cases it might depend on context; in my situation there were subsequent uses of h in the code, and changing the expression did swap a couple instructions around but did not change whether the volatile object was read after it was written, or the code size and instructions used.

 

I can reproduce your results showing size variation in an isolated example with the flags you provided, but after adding -Os the difference reduces to whether an instruction appears before or after a compiler-generated but unreferenced label. The instruction sequence is the same for both alternatives with -Os.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  

×