Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How are chained macros resolved in C?

If I want to use preprocessor #define statements for easy definition and calculation of constants and common functions and take advantage of less RAM overhead (as opposed to using const values). However, I am unsure as to how they are resolved if many macros are used together.

I'm designing my own DateTime code handling, similar to linux timestamps but for a game with tick updates that represent 1/60th of a second. I would prefer to declare values chained, but wonder if hard coded valued would perform faster.

#include <stdint.h>

// my time type, measured in 1/60 of a second.
typedef int64_t DateTime;

// radix for pulling out display values
#define TICKS_PER_SEC  60L
#define SEC_PER_MIN    60L  
#define MIN_PER_HR     60L
#define HRS_PER_DAY    24L
#define DAYS_PER_WEEK   7L
#define WEEKS_PER_YEAR 52L

// defined using previous definitions (I like his style, write once!)
#define TICKS_PER_MIN    TICKS_PER_SEC * SEC_PER_MIN
#define TICKS_PER_HR     TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR
#define TICKS_PER_DAY    TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY
// ... so on, up to years

//hard coded conversion factors.
#define TICKS_PER_MIN_H    3600L      // 60 seconds = 60^2 ticks
#define TICKS_PER_HR_H     216000L    // 60 minutes = 60^3 ticks
#define TICKS_PER_DAY_H    5184000L   // 24 hours   = 60^3 * 24 ticks

// an example macro to get the number of the day of the week
#define sec(t)((t / TICKS_PER_DAY) % DAYS_PER_WEEK)

If I use the sec(t) macro, which uses TICKS_PER_DAY which is defined by 3 previous macros TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY, does that place everywhere in my code that calls sec(t):

(t / 5184000L) % 7L)

or does it expand out each time to:

(t / (60L * 60L * 60L * 24L)) % 7L)

so that extra multiplication instructions are executed at each step? Is this a tradeoff between macros and const variables, or do I misunderstand how the preprocessor works?

UPDATE:

Per many helpful answers, the best design for chaining macros which expand into constant expressions is to wrap the definition in parentheses for

1. Proper order of operations:

(t / 60 * 60 * 60 * 24) != (t / (60 * 60 * 60 * 24))

2. Encourage constant folding by the compiler by grouping constant values together:

// note parentheses to prevent out-of-order operations
#define TICKS_PER_MIN    (TICKS_PER_SEC * SEC_PER_MIN)
#define TICKS_PER_HR     (TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR)
#define TICKS_PER_DAY    (TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY)
like image 289
aaroncarsonart Avatar asked Dec 15 '15 19:12

aaroncarsonart


People also ask

How does macro works in C?

The macro in C language is known as the piece of code which can be replaced by the macro value. The macro is defined with the help of #define preprocessor directive and the macro doesn't end with a semicolon(;). Macro is just a name given to certain values or expressions it doesn't point to any memory location.

Can we change macro value in C?

You can't. Macros are expanded by the Preprocessor, which happens even before the code is compiled. It is a purely textual replacement.

How are macros expanded in C?

The LENGTH and BREADTH are called the macro templates. The values 10 and 20 are called macro expansions. When the program run and if the C preprocessor sees an instance of a macro within the program code, it will do the macro expansion. It replaces the macro template with the value of macro expansion.

How do you redefine #define in C?

The process to redefine a Macro is: Macro must be defined. When, you want to redefine the Macro, first of all, undefined the Macro by using #undef preprocessor directive. And, then define the Macro again by using #define preprocessor directive.


4 Answers

Macro expansion is nothing more than simple text substitution. After macros have been expanded, the compiler will parse the result and perform its usual optimizations, which should include constant folding.

However, this example illustrates a common mistake beginners make when defining macros in C. If the macro is intended to expand to an expression, good C practice dictates that the value should always be wrapped in parentheses if the result would otherwise contain exposed operators. In this example, look at the definition of TICKS_PER_DAY:

#define TICKS_PER_DAY    TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY

Now look at sec (note that the semicolon should not be present, but I'll ignore that for now):

#define sec(t)((t / TICKS_PER_DAY) % DAYS_PER_WEEK);

If this is instantiated as sec(x), it will expand to:

((x / 60L * 60L * 60L * 24L) % 7L);

This is clearly not what was intended. It will only divide by the initial 60L, after which the remaining values will be multiplied.

The correct way to fix this is to fix the definition of TICKS_PER_DAY to properly encapsulate its internal operations:

#define TICKS_PER_DAY    (TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY)

And of course, sec should be an expression macro and should not contain a semicolon, which would prevent it from being used, for instance, in a context like sec(x) + 10:

#define sec(t)  ((t / TICKS_PER_DAY) % DAYS_PER_WEEK)

Now let's see how sec(x) would be expanded with these bug fixes:

((x / (60L * 60L * 60L * 24L)) % 7L)

Now this will actually do what was intended. The compiler should constant-fold the multiplies, resilting in a single divide followed by a single mod.

Edit: It looks like the missing parentheses have since been added to the original post. Without them, it wouldn't work at all. Also the extra semicolon has been removed from the original post.

like image 37
Tom Karzes Avatar answered Oct 05 '22 23:10

Tom Karzes


The preprocessor just does text substitution. It will evaluate to the second expression with the "extra" multiplies. The compiler will generally try to optimize arithmetic between constants, however, so long as it can do so without changing the answer.

To maximize its chances to optimize, you'll want to be mindful that you keep the constants "next to each other" so that it can see the optimization, especially with floating point types. In other words, if t is a variable, you would like 30 * 20 * t instead of 30 * t * 20.

like image 56
Brick Avatar answered Oct 06 '22 01:10

Brick


See the gcc preprocessor macro docs, specifically Object-like Macros.

I think the compiler also comes into play here. For example, if we are only considering the preprocessor, then it should expand to

(t / (60L * 60L * 60L * 24L)) % 7L)

However, it may be that the compiler (regardless of optimization?) will resolve this to

(t / 5184000L) % 7L)

since these are independent constants and therefore would be faster/simpler code execution.

Note1: you should use "(t)" in your definition to protect against unintended expansions/interpretations. Note2: another best practice is to avoid using undef, because this makes the code less readable. See the notes about how the macro expansion is affected by this (section Object-like Macros).

UPDATE: from the section Object-like Macros:

When the preprocessor expands a macro name, the macro's expansion replaces the macro invocation, then the expansion is examined for more macros to expand. For example,

#define TABLESIZE BUFSIZE #define BUFSIZE 1024 TABLESIZE ==> BUFSIZE ==> 1024 TABLESIZE is expanded first to produce BUFSIZE, then that macro is expanded to produce the final result, 1024.

Notice that BUFSIZE was not defined when TABLESIZE was defined. The ‘#define’ for TABLESIZE uses exactly the expansion you specify—in this case, BUFSIZE—and does not check to see whether it too contains macro names. Only when you use TABLESIZE is the result of its expansion scanned for more macro names.

(emphasis mine)

like image 43
tniles Avatar answered Oct 06 '22 00:10

tniles


It expands to:

(t / (60L * 60L * 60L * 24L)) % 7L)

This is because macros are handled by the pre-processor, which simply expands macros to their values (recursively if necessary).

BUT this does not mean that the entire computation will be repeated at each point you use sec(t). This is because the computation happens at compile time. So you don't pay the price at run time. The compiler pre-computes such constant computations, and uses the computed value in the generated code.

like image 36
Ziffusion Avatar answered Oct 06 '22 00:10

Ziffusion