8. Efficient coding

The Calypsi C compiler tool chain is designed for coding in memory-constrained environments, typically embedded systems. This chapter explores proper data type usage and considerations for both smaller and larger targets.

Even if your current coding targets a specific system, the code may evolve and migrate to different system sizes. This chapter discusses trade-offs between such environments.

8.1. Data types

Data type selection significantly impacts overall program size and execution speed. Here are some rules of thumb:

  • Generally, use the smallest possible types.

  • Floating-point operations are typically inefficient, consuming space and often executing slowly.

  • Use variables with the narrowest possible scope.

  • Avoid taking a variable’s address. If necessary, limit its use as much as possible. The compiler cannot allocate such variables in registers (registers have no addresses), which prevents the optimizer from fully understanding how function calls affect the variable.

Integer types

While smallest types are recommended, for larger targets, int and unsigned int can be more efficient, often being the sweet spot.

The stdint.h header provides integer types useful for selecting appropriate sizes, optimizing efficiency, and ensuring portability.

A type like uint_fast16_t defines an unsigned integer of at least 16 bits, optimized for speed. An 8-bit target will use a 16-bit type, while a 32-bit target might choose a 32-bit type if its instruction set handles 32-bit operations more efficiently than 16-bit operations.

Floating point types

Floating-point operations are often implemented using assembly or C library routines, which consume code space and incur execution overhead.

Alternatively, use fixed-point integers (Q notation). For instance, a Q24.8 number has 24 integer bits and 8 fractional bits, allowing manipulation with standard integer operations. This requires scaling values, which is straightforward.

Implicit conversions

C’s conversion rules aim to convert integers to int and float to double. This can introduce extra code. If this is a concern, you must understand these rules or examine the compiler’s generated code.

Note

While studying generated assembly code may seem daunting, a basic understanding of its mechanics can be highly beneficial. The compiler can generate a list file (-l or --list-file) showing C code intermixed with the resulting assembly.

8.2. Use prototypes

Functions can be declared using two styles. Traditional C, which lacks prototypes, is not recommended as it hinders compiler error detection. Prototypes offer safer, more efficient code generation by avoiding certain type promotion rules that introduce extra code. Traditional style is supported only for compatibility with older code. In this style, a declaration does not specify parameters:

/* Traditional style */
int foo();   /* declaration */

int foo(c, i)   /* definition */
  char c;
  int i;
{
  return i - c;
}

In this case, the function takes a char parameter, which is promoted to int during the function call, introducing additional code.

Using prototypes with Standard C you specify parameters in the declaration:

/* Standard C prototype style */
int foo(char c, int i);   /* declaration */

int foo(char c, int i)   /* definition */
{
  return i - c;
}

In this case the char parameter is not promoted to int and is passed as a char type.

8.3. Use tables

Consider the trade-off between calculating a value and using a table for knowledge or operation encoding. For example, a sine operation in Q (fixed-point) notation can be approximated with a table lookup instead of calculation, making it very fast and compact.