Wednesday, April 8, 2020

Better asserts in C with link-time optimization

I've been a fan of link-time optimization for several years.  I've been a fan of efficient programming for even longer.  I was an early fan of C++ because features like function overloading made it easier to move decisions done at run-time in C to compile-time with C++.  As C++ has become more complex over the decades, I've become less of a C++ fan, and appreciate the simplicity of C.

For small embedded systems like 8-bit AVRs and ARM M0, run-time error checking with assert() has minimal usefulness compared to UNIX, where a a core dump will help pinpoint the error location and the state of the program at the time of the error.  Even if the usability problems were solved, real-time embedded systems may not be able to afford the performance costs of run-time error checking.

Both C++ and C support static assertions.   Anyone who has tried to use static_assert likely has encountered "expression in static assertion is not constant" errors for anything but the simplest of checks.  The limitations of static_assert is well documented elsewhere, so I will not go into further details in this post.

I had long understood that LTO allowed the compiler to evaluate expressions in code at build time,  I never realized it's potential for static error checking.  The idea came to me when looking at a fellow embedded developer's code for fast Arduino digital IO.  In particular, Bill's code introduced me to the gcc error function attribute.  The documentation describes the attribute as follows:

  • If the error or warning attribute is used on a function declaration and a call to such a function is not eliminated through dead code elimination or other optimizations, an error or warning (respectively) that includes message is diagnosed.  This is useful for compile-time checking ...
Despite the fact that it seems the error attribute was introduced to address some of the limitations of static asserts, it doesn't seem to be commonly used.  After some experimentation, I came up with a basic example.
pll.c:
__attribute((error("")))
void constraint_error(char * details);

volatile unsigned pll_mult;


void set_pll_mult(unsigned multiplier)
{
    if (multiplier > 8) constraint_error("multlier out of range");
    pll_mult = multiplier;
}

main.c:
extern void set_pll_mult(unsigned multiplier);

int main()
{
    set_pll_mult(9);
}

$ gcc -Os -flto -o main *.c
In function 'set_pll_mult.constprop',
    inlined from 'main' at main.c:6:5:
pll.c:9:25: error: call to 'constraint_error' declared with attribute error:
     if (multiplier > 8) constraint_error("multlier out of range");
                         ^
When set_pll_mult() is called with an argument greater than 8, a compile error occurs.  When it is compiled with a valid multiplier, the "if (multiplier > 8)" statement is eliminated by the optimizer.  One drawback to the technique is that the caller (main.c in this case) is not identified when the called function is not inlined.  Increasing the optimization level to O3 may help to get the function inlined.

2 comments:

  1. Have you considered static_assert() ?

    static_assert(multiplier > 8, "oh no!");

    /Users/user1/Documents/Arduino/sketch_may27a/sketch_may27a.ino: In function 'void loop()':
    sketch_may27a:15:3: error: static assertion failed: oh no!
    static_assert(multiplier > 8, "oh no!");
    ^~~~~~~~~~~~~
    exit status 1
    static assertion failed: oh no!

    ReplyDelete
    Replies
    1. The big problem is argument to static_assert must be known within the translation unit (the same .o). As I said in the third paragraph, "The limitations of static_assert is well documented elsewhere, so I will not go into further details in this post."

      Delete