You can paste the code at compiler explorer to see what happens. From your code, this is the assebler generated:
print(): # @print()
mov rcx, qword ptr [rip + stdout]
mov edi, offset .L.str
mov esi, 11
mov edx, 1
jmp fwrite # TAILCALL
main: # @main
push rax
mov rcx, qword ptr [rip + stdout]
mov edi, offset .L.str
mov esi, 11
mov edx, 1
call fwrite
mov rcx, qword ptr [rip + stdout]
mov edi, offset .L.str
mov esi, 11
mov edx, 1
call fwrite
xor eax, eax
pop rcx
ret
.L.str:
.asciz "hello world"
The important part is at the end:
.L.str:
.asciz "hello world"
The "hello world"
is declared only there like a global variable, and is used every time you call the function print
.
It is like if you declared it like this:
#include<stdio.h>
const char* hello = "hello world";
void print()
{
fprintf(stdout,"%s",hello);
}
void main()
{
print();
print();
}
In this case, the compiler saw the optimisation and made it. But, I cant say for sure this will always be the case, because it cant depend on the compiler.