1

I have compared simple define vs computed define and the assembly codes surprise me.

#define HOURS 365*24 and #define HOURS 8760

Then in the main, I do int a = HOURS.

Both of them generate assembly codes that look exactly the same. This holds true in both gcc (version 7.3.0, 64-bit arch) and avr-gcc (version 5.4.0, 8-bit arch) Why is this the case? From my understanding, define is only a preprocessor which will replace the exact string in the file before compiling. If that's the case, why the computation is not being done at runtime? Is that a trick from gcc when it sees a constant?

Nakarin
  • 67
  • 1
  • 4
  • 5
    It's not a trick. Any decent compiler does an optimization called "constant folding." The arithmetic is performed by compiler, not in compiled code, whenever it can determine the answer would be the same either way. If you're interested in this topic, then a good book on compiler design might be of interest. – Gene Sep 23 '18 at 06:02
  • 5
    `#define` is a red herring. Look at tthe assembly generated by `a = 365*24;`. It's called optimisation. – n. m. could be an AI Sep 23 '18 at 06:03
  • https://stackoverflow.com/questions/1560357/can-the-c-preprocessor-perform-integer-arithmetic – roryrjb Sep 23 '18 at 06:05
  • 1
    Why would the computation be done at runtime? That would be pretty silly. – Roflcopter4 Sep 23 '18 at 06:20
  • at the end of the day, the value stored in the binary will be 8760 due to compiler optimization – Mox Sep 23 '18 at 06:31
  • The preprocessor substitutes macros and produces source code, which is "compiled" in a later phase of compilation. So the compiler sees the output of the preprocessor, not the input to it. If the compiler is able to fold literal constants it sees (which most modern compilers can) the calculation will be done at compile time. – Peter Sep 23 '18 at 06:48
  • The preprocessor just does simple text substitution. You will get the same compiled code by just substituting the macro bodies where they're used, as n.m. points out. Also, always wrap the body of an expression macro in parentheses, e.g. `(365*24)`, to avoid misassociation in the substituted context. – Tom Karzes Sep 23 '18 at 07:08
  • Why all you commenters do not place an answer? – alk Sep 23 '18 at 08:55
  • @TomKarzes: Preprocessing substitutes preprocessor tokens, not text. – Eric Postpischil Sep 23 '18 at 10:21
  • @Roflcopter4 because I want the compiler to generate the code that's predictable enough for me to be able to count the clock. I didn't mention earlier, but I write the code for a microcontroller. I sometimes want to know how many clocks exactly a part of the code will spend. – Nakarin Sep 23 '18 at 21:02

1 Answers1

3

Any good compiler evaluates simple expressions at compile-time. The behavior you are seeing is unrelated to preprocessing. If you directly write:

int a = 365 * 24;

then any good compiler will generate code that initializes a to 8760. (Some compilers might not if they are executed with their optimization features disabled. But any decent compiler will perform this optimization when optimization is enabled.)

In modern compilers, optimization will do a great deal more than this. For example, they will identify invariant code outside of loops. (That is, if there are expressions or even full statements inside of a loop whose results do not depend on the data in the current loop iteration, they will move those expressions or statements outside of the loop, so that they are only evaluated once instead of each time the loop executes.) They will remove objects that are never used. If one path of a conditional branch has undefined behavior in some circumstances, they may deduce that that path is never taken, so they will optimize code by removing that path and the conditional branch in those circumstances. They may even replace entire loops with equivalent mathematical expressions. For example, when I compile:

#include <stdio.h>
#include <stdlib.h>


int main(int argc, char *argv[])
{
    int n = atoi(argv[1]);
    int s = 0;
    for (int i = 1; i <= n; ++i)
        s += i;
    printf("%d\n", s);
}

with Apple LLVM 9.1.0 (clang-902.0.39.2), the resulting assembly code does not contain a loop. After calling atoi, it executes instructions that are, in effect1, printf("%d\n", 0 < n ? n*(n+1)/2 : 0);. So the loop is entirely replaced by a test and a simple arithmetic expression.

Footnote

1 The actual instructions are:

leal    -1(%rax), %ecx
movl    %eax, %edx
addl    $-2, %edx
imulq   %rcx, %rdx
shrq    %rdx
leal    -1(%rdx,%rax,2), %esi

A closer representation of those is (n-1)*(n-2)/2 + 2*n - 1. One reason for the compiler to do this is that it gets the “correct” result if n is INT_MAX, as n*(n+1)/2 would produce zero because n+1 wraps, whereas (n-1)*(n-2)/2 + 2*n - 1 gets the same result that the loop gets (assuming wrapping arithmetic), which is INT_MAX+1 or INT_MIN due to wrapping. By C rules, the compiler could have used the simpler n*(n+1)/2 since the behavior on overflow is not defined by C, but this compiler may be conforming to stricter rules such as using wrapping two’s complement arithmetic.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • Thank you for the answer. I forgot to mention that I didn't add the optimization flag. I only called `gcc -S source.c`. By the way, regarding your extra example, how can we debug step by step, say with gdb, then when at runtime the binary doesn't do exactly the same as the source? – Nakarin Sep 23 '18 at 20:01
  • 1
    @Nakarin: Debugging is something of an art. If a problem manifests when debugging is disabled, then it can be debugged with direct correlations between source code and machine instructions. If the problem vanishes or changes when debugging is disabled, that is a significant clue the code does something that has undefined behavior. Fault isolation can help identify which routines are causing problems. If debugging with optimization is necessary, some compilers are good about supplying information that relates lines of source code to machine instructions, even after optimization, to some extent. – Eric Postpischil Sep 23 '18 at 23:24
  • 1
    And, as a programmer becomes experienced, they learn to recognize optimizations the compiler has made and to debug around them. One can also insert code to reveal diagnostic information (often as simple as putting `printf` statements in useful places). – Eric Postpischil Sep 23 '18 at 23:25