5

Consider the following code:

#include <stdio.h>

void foo() {
    printf("Hello world\n");
}

void bar() {
    printf("Hello world");
}

The assembly produced by both these two functions is:

.LC0:
        .string "Hello world"
foo():
        mov     edi, OFFSET FLAT:.LC0
        jmp     puts
bar():
        mov     edi, OFFSET FLAT:.LC0
        xor     eax, eax
        jmp     printf

Now I know the difference between puts and printf, but I find this quite interesting that gcc is able to introspect the const char* and figure out whether to call printf or puts.

Another interesting thing is that in bar, compiler zero'ed out the return register (eax) even though it is a void function. Why did it do that there and not in foo?

Am I correct in assuming that compiler 'introspected my string', or there is another explanation of this?

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
skgbanga
  • 2,477
  • 2
  • 20
  • 29
  • Probably it didn't see any argument past the first one (like an "%u", so it assumed it's a single string. – Michael Chourdakis Feb 05 '20 at 16:26
  • The compiler doesn't even need to inspect your format string. Calling `printf` without any parameter after the format string cannot have too many format specifiers anyway – Gerhardh Feb 05 '20 at 16:26
  • 3
    BTW: A good compiler does inspect the format string anyway as it verifies number and types of the provided parameters and issues a warning if there is a mismatch. – Gerhardh Feb 05 '20 at 16:27
  • 1
    @Gerhardh the compiler would need to inspect the string for the presence of the newline. – Weather Vane Feb 05 '20 at 16:27
  • @WeatherVane is there a difference between `printf` and `puts` regarding the buffering with or without `'\n'`? Isn't it sufficient if puts does the check? I assume the formatted output of `printf` ends up in `puts` afterwards anyway – Gerhardh Feb 05 '20 at 16:29
  • 3
    @Gerhardh `puts` outputs a newline after the string so if there isn't one, it can't be substituted. – Weather Vane Feb 05 '20 at 16:30
  • @WeatherVane indeed. I never noticed that a newline is added. Shame on me. ;) – Gerhardh Feb 05 '20 at 16:32
  • 1
    @Gerhard `puts` does not check anything: it outputs what you give it, but there is another runtime difference too. If I pass a `NULL` pointer as a string to `printf`, MSVC kindly (but does not have to) ouputs `(null)` but with `puts` it crashes. – Weather Vane Feb 05 '20 at 16:34
  • Related (xor eax, eax): https://stackoverflow.com/q/1396527/6699433 – klutt Feb 05 '20 at 16:42
  • 6
    This is not introspection of the string; the compiler is not looking into itself. It is plain old inspection. – Eric Postpischil Feb 05 '20 at 17:26

2 Answers2

13

Am I correct in assuming that compiler 'introspected my string', or there is another explanation of this?

Yes, this is exactly what happens. It's a pretty simple and common optimization done by the compiler.

Since your first printf() call is just:

printf("Hello world\n");

It's equivalent to:

puts("Hello world");

Since puts() does not need to scan and parse the string for format specifiers, it's quite faster than printf(). The compiler notices that your string ends with a newline and does not contain format specifiers, and therefore automatically converts the call.

This also saves a bit of space, since now only one string "Hello world" needs to be stored in the resulting binary.

Note that this is not possible in general for calls of the form:

printf(some_var);

If some_var is not a simple constant string, the compiler cannot know if it ends in \n.

Other common optimizations are:

  • strlen("constant string") might get evaluated at compile time and converted into a number.
  • memmove(location1, location2, sz) might get transformed into memcpy() if the compiler is sure that location1 and location2 don't overlap.
  • memcpy() of small sizes can be converted in a single mov instruction, and even if the size is larger the call can sometimes be inlined to be faster.

Another interesting thing is that in bar, compiler zero'ed out the return register (eax) even though it is a void function. Why did it do that there and not in foo?

See here: Why is %eax zeroed before a call to printf?


Related interesting posts

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Marco Bonelli
  • 63,369
  • 21
  • 118
  • 128
  • Thanks. Any clue regarding the zeroing out the `eax` register? – skgbanga Feb 05 '20 at 16:35
  • @skgbanga `puts()` and `printf()` have slightly different return meanings. – chux - Reinstate Monica Feb 05 '20 at 16:37
  • @klutt OP was referring to `bar` there. – Marco Bonelli Feb 05 '20 at 16:49
  • @chux-ReinstateMonica: OP was talking about the *caller* zeroing EAX before `jmp printf`, not about what they return; that's not visible in the compiler's asm output. (Because it's variadic and the x86-64 SysV calling convention requires AL>=number of FP args in XMM regs) – Peter Cordes Feb 05 '20 at 17:17
  • 2
    @MarcoBonelli: Fun fact: compilers also know how to optimize `printf("%s\n", string)` into puts. The mechanism is that `printf` is treated as a builtin function. – Peter Cordes Feb 05 '20 at 17:18
  • 2
    Related links, not exact duplicates: [Why doesn't GCC optimize this call to printf?](//stackoverflow.com/q/37435984) has more details about what GCC can/can't do, and a link to an article about it. [Can printf get replaced by puts automatically in a C program?](//stackoverflow.com/q/25816659) is a variant of this. [Why it shows puts when I disassemble no matter whether I'm using printf or puts?](//stackoverflow.com/q/41371002) is a dup of this with a less detailed answer, closed it. Also [Difference between printf@plt and puts@plt](//stackoverflow.com/q/39007002) – Peter Cordes Feb 05 '20 at 17:25
  • There's nothing to stop a compiler also recognising the non-newline forms and changing them into `fputs(string, stdout)` as well. – Toby Speight Feb 05 '20 at 17:26
  • Also [-O2 optimizes printf("%s\n", str) to puts(str)](//stackoverflow.com/q/36343733) covers clang/LLVM. – Peter Cordes Feb 05 '20 at 17:29
  • @PeterCordes thank you very much for the links. I'll add them to my answer as reference. – Marco Bonelli Feb 05 '20 at 17:36
  • @TobySpeight: Beyond the fact that the latter version would probably generate bigger code, and a programmer who wrote `printf` would probably have been satisfied with its performance. – supercat Jul 23 '21 at 22:38
  • @TobySpeight: Yes, it's just a technical limitation that gcc doesn't do it (the way the compiler is written doesn't make it easy to reference `stdout` when it's not already part of the expression). See https://github.com/gcc-mirror/gcc/blob/b37351e3279d192d5d4682f002abe5b2e133bba6/gcc/gimple-fold.c#L3994. However `fprintf(stdout, "Hello world");` does get optimized - in fact it bypasses `fputs` and calls `fwrite` directly. https://godbolt.org/z/YKKdYMWcq – Nate Eldredge Oct 12 '21 at 17:16
6

Another interesting thing is that in bar, compiler zero'ed out the return register (eax) even though it is a void function. Why did it do that there and not in foo?

This is completely unrelated to the question in the title, but is interesting none the less.

The xor zeroing %eax is before the call to printf so is part of the call and has nothing to do with the return value. The reason this happens is that printf is a varargs function, and the x86_64 ABI for varargs function requires passing floating-point arguments in xmm registers, and requires passing the number of such arguments in %al. So this instruction is there to ensure that %al is 0 as no arguments are being passed in xmm registers to printf.

puts is not a varargs function, so it is not required there.

Chris Dodd
  • 119,907
  • 13
  • 134
  • 226