2

Is there a difference between declaring a variable first and then assigning a value or directly declaring and assigning a value in the compiled function? Does the compiled function do the same work? e.g, does it still read the parameters, declare variables and then assign value or is there a difference between the two examples in the compiled versions?

example:

    void foo(u32 value) {

      u32 extvalue = NULL;

      extvalue = value;

    }

compared with

    void foo(u32 value) {

    u32 extvalue = value;

    }

I am under the impression that there is no difference between those two functions if you look at the compiled code, e.g they will look the same and i will not be able to tell which is which.

JakeParis
  • 11,056
  • 3
  • 42
  • 65
user1501127
  • 865
  • 1
  • 18
  • 32
  • 6
    So why don't you look at the compiled code, then? This is precisely in the realm of something implementors can optimise or not, so long as the end result works the same. Beyond that, the question is one of style: is there any point to using that initial `NULL`? If not, then initialise with the real initial value. Semantics and safety (e.g. to avoid having a meaningless/dangerous value temporarily) are more important than optimisation, especially when you can easily check the latter for yourself. (Of course, sometimes an initial `NULL` or other sentinel is needed to avoid reading uninitialised) – underscore_d Jan 11 '18 at 09:42
  • 1
    @underscore_d: Looking at compiled code cannot definitively answer questions like this. Two sources might have the same compiled code because the C standard allows an implementation to give them the same behavior, but the C standard might permit different behavior too. Two sources might have different compiled code because the C standard allows them to have different behvior, but the C standard might permit the same behavior. Two sources might have different compiled code because the C standard requires them to have the same behavior but that can be accomplished with different compiled code. – Eric Postpischil Jan 11 '18 at 10:05
  • 2
    In C89 you *had to* declare all variables at the top of the function, as declarations and code were not allowed to be mixed. There is no reason to follow that rule anymore. – Bo Persson Jan 11 '18 at 12:04
  • 1
    @BoPersson: At the start of brace-enclosed blocks, not necessarily the start of functions. – Eric Postpischil Jan 11 '18 at 13:14

2 Answers2

4

it depends on the compiler & the optimization level of course.

A dumb compiler/low optimization level when it sees:

  u32 extvalue = NULL;
  extvalue = value;

could set to NULL then to value in the next line.

Since extvalue isn't used in-between, the NULL initialization is useless and most compilers directly set to value as an easy optimization

Note that declaring a variable isn't really an instruction per se. The compiler just allocates auto memory to store this variable.

I've tested a simple code with and without assignment and the result is diff erent when using gcc compiler 6.2.1 with -O0 (don't optimize anything) flag:

 #include <stdio.h>
 void foo(int value) {

      int extvalue = 0;
      extvalue = value;

      printf("%d",extvalue);
    }

disassembled:

Disassembly of section .text:

00000000 <_foo>:
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   83 ec 28                sub    $0x28,%esp
   6:   c7 45 f4 00 00 00 00    movl   $0x0,-0xc(%ebp)  <=== here we see the init
   d:   8b 45 08                mov    0x8(%ebp),%eax
  10:   89 45 f4                mov    %eax,-0xc(%ebp)
  13:   8b 45 f4                mov    -0xc(%ebp),%eax
  16:   89 44 24 04             mov    %eax,0x4(%esp)
  1a:   c7 04 24 00 00 00 00    movl   $0x0,(%esp)
  21:   e8 00 00 00 00          call   26 <_foo+0x26>
  26:   c9                      leave
  27:   c3                      ret

now:

 void foo(int value) {

      int extvalue;
      extvalue = value;

      printf("%d",extvalue);
    }

disassembled:

Disassembly of section .text:

00000000 <_foo>:
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   83 ec 28                sub    $0x28,%esp
   6:   8b 45 08                mov    0x8(%ebp),%eax
   9:   89 45 f4                mov    %eax,-0xc(%ebp)
   c:   8b 45 f4                mov    -0xc(%ebp),%eax
   f:   89 44 24 04             mov    %eax,0x4(%esp)
  13:   c7 04 24 00 00 00 00    movl   $0x0,(%esp)
  1a:   e8 00 00 00 00          call   1f <_foo+0x1f>
  1f:   c9                      leave
  20:   c3                      ret
  21:   90                      nop
  22:   90                      nop
  23:   90                      nop

the 0 init has disappeared. The compiler didn't optimize the initialization in that case.

If I switch to -O2 (good optimization level) the code is then identical in both cases, compiler found that the initialization wasn't necessary (still, silent, no warnings):

   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   83 ec 18                sub    $0x18,%esp
   6:   8b 45 08                mov    0x8(%ebp),%eax
   9:   c7 04 24 00 00 00 00    movl   $0x0,(%esp)
  10:   89 44 24 04             mov    %eax,0x4(%esp)
  14:   e8 00 00 00 00          call   19 <_foo+0x19>
  19:   c9                      leave
  1a:   c3                      ret
Jean-François Fabre
  • 137,073
  • 23
  • 153
  • 219
  • @Mat Good point about `NULL` being semantically wrong here. It'd work if someone just defined it as `0`, though, but that's wrong too. If it was defined as `(void*)0` then it should be a compile error with sane flags. – underscore_d Jan 11 '18 at 09:49
  • @Mat no sweat. So I had edited the answer in the meantime, and there _is_ a difference when using `-O0`. – Jean-François Fabre Jan 11 '18 at 09:54
1

I tried these functions in godbolt:

void foo(uint32_t value)
{
      uint32_t extvalue = NULL;
      extvalue = value;
}

void bar(uint32_t value)
{
      uint32_t extvalue = value;
}

I ported to the actual type uint32_t rather than u32 which is not standard. The resulting non-optimized assembly generated by x86-64 GCC 6.3 is:

foo(unsigned int):
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-20], edi
        mov     DWORD PTR [rbp-4], 0
        mov     eax, DWORD PTR [rbp-20]
        mov     DWORD PTR [rbp-4], eax
        nop
        pop     rbp
        ret

bar(unsigned int):
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-20], edi
        mov     eax, DWORD PTR [rbp-20]
        mov     DWORD PTR [rbp-4], eax
        nop
        pop     rbp
        ret

So clearly the non-optimized code retains the (weird, as pointed out by others since it's not written to a pointer) NULL assignment, which is of course pointless.

I'd vote for the second one since it's shorter (less to hold in one's head when reading the code), and never allow/recommend the pointless setting to NULL before overwriting with the proper value. I would consider that a bug, since you're saying/doing something you don't mean.

unwind
  • 391,730
  • 64
  • 469
  • 606
  • 3
    It's similarly pointless to worry about unoptimised code generating an extra line or two, though. Anyway, definitely agreed that showing intent is the more important factor (it usually is). – underscore_d Jan 11 '18 at 09:54
  • 5
    The unoptimized code is compiled to follow the code flow of source "per line" to make "sense" while debugging, so the variables will act as the programmer wrote in source, when stepping over single lines. With any [serious] level of optimization the compiler will drop that and simply focus to produce correct code for the abstract C machine, which means that both functions in original post can be completely removed, as they have no effect on the abstract machine. – Ped7g Jan 11 '18 at 10:18
  • 1
    Why are you answering with un-optimized code? Don't encourage people to look at un-optimized compiler output; it tells you very little about what will be efficient for real. (And yes, `gcc -O0` de-optimizes on purpose by spilling/reloading to support async modification of variable values with a debugger before every C statement. And even to allow gdb `jump` to a different line in the function without recompiling.) See https://stackoverflow.com/questions/46378104/why-does-integer-division-by-1-negative-one-result-in-fpe for an example of this mattering. – Peter Cordes Jan 11 '18 at 20:24
  • @Ped7g good point. In fact debugging code which has been compiled with -O2 can be pretty hairy, with all "value optimized out" and strange jumps. (sometimes you don't have a choice) – Jean-François Fabre Jan 11 '18 at 20:26
  • 2
    @Jean-FrançoisFabre: But if you want to know if the compiler did a good job, you should look at optimized code. See also Matt Godbolt's CppCon2017 talk: [“What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid”](https://youtu.be/bSkpMdDe4g4) – Peter Cordes Jan 11 '18 at 20:28
  • @PeterCordes yes, unfortunately, I slightly suck at modern asm like x86 or ppc or arm, even if I can recognize really unefficient code. Me only masters 680x0 and 6502. Rarely useful (and 68k compilers aren't very good at optimizing). That's why goldbolt site is just a killer. In my day job, our team codes using Ada, though... – Jean-François Fabre Jan 11 '18 at 20:30
  • @PeterCordes I meant debugging at source level. At asm level, it's maybe less a problem. – Jean-François Fabre Jan 11 '18 at 20:32
  • @Jean-FrançoisFabre: yeah, for real debugging, it's often best to compile with `-Og` at most, or even `-O0` so the debugger can show you the value of locals. Apparently debug formats aren't advanced enough to track variable values that stay live in registers, even if they do exist as written :/. But debugging source logic is a totally separate task from single-stepping the asm in a debugger to see what instructions actually run, or looking at profiler output. (`perf report`). – Peter Cordes Jan 11 '18 at 21:50