Skip to main content

Module libcall

Module libcall 

Source
Expand description

A call to the library, folded into the call it really is.

Section 20.2 of spec/optimizer/20-idioms-and-libcalls.md, the half of it that is about a library call rather than about arithmetic. printf("hello world\n") writes the same bytes as puts("hello world"), and the second one does not read a format string at run time, so gcc rewrites it and has done since 2000. A program that checks which of the two it was left with, which is what gcc.c-torture/execute/builtins/printf.c does by defining a printf of its own that aborts, fails outright on a compiler that leaves the call alone. tamnd/rucc#1636 is that program and the two beside it.

§The rules

All of them measured against gcc 16.2.0 on x86-64 rather than read out of its source, and all of them conditional on the format being a string this module holds the bytes of.

strstr(s, "") is s, since the empty string is found at once wherever it is looked for. strstr(s, "w") is strchr(s, 'w'), which is a search for a character rather than for a string and is worth doing wherever the haystack came from. strstr of two strings this module holds is the answer itself, which is a place in the haystack or a null pointer, and nothing is called.

strlen, strnlen, strcmp, strncmp, strchr, strrchr, memchr, strspn, strcspn and strpbrk of strings this module holds are the answer itself in the same way, which is a number for the first four and the last two of the six that search, and a place in the first argument or a null pointer for the other ones. The number goes in with the width the call was declared to give back, because a program that declared strlen as something returning an int is a program whose reader of that answer reads an int.

Three of them have a rule about the shape rather than about the bytes. strpbrk(s, "") is a null pointer and strspn(s, "") is zero, since nothing at all is in an empty set, and strcspn(s, "") is strlen(s) for the same reason read the other way. strpbrk(s, "c") is strchr(s, 'c'), which is the strstr rule again for a set of one character instead of a needle of one.

Two of them are told how far to read rather than going looking for a terminator, and those two read the object’s bytes rather than the string in it. memchr(s, c, n) needs the object to have n bytes from s on, and it is refused where it does not, since a call reading past the end of what the compiler can see is a call whose answer the compiler does not know. strnlen(s, n) is the count where nothing terminated the string inside it.

printf with the format alone: nothing at all when it is empty, putchar when it is one character, and puts of the format without its last character when the format holds no % and ends in a newline. printf("%s\n", p) is puts(p) and printf("%c", c) is putchar(c), whatever p and c are. printf("%s", p) where p is a string this module holds is the same question again asked of that string, and where it is not, the call stays: printf has no stream argument to hand to fputs, and stdout is not a name a compiler may invent.

fprintf is the same list with a stream in hand, so the case printf cannot take is the case this one can. A format holding no % becomes fputc of its one character or fwrite of the whole of it, fprintf(s, "%c", c) becomes fputc(c, s), and fprintf(s, "%s", p) becomes fputs(p, s) however little is known about p.

fputs(p, s) needs the length of p and nothing else. Zero is nothing at all, one is fputc when the character is known as well, and anything longer is fwrite(p, 1, len, s).

The _unlocked spellings get the one fold that names no function, which is that a call writing nothing is removed. gcc stops in exactly the same place and the reason is in the torture program’s own comment: a system need not have a puts_unlocked for the compiler to name.

The checking spellings _FORTIFY_SOURCE writes, __memcpy_chk and the thirteen beside it, carry the size of the destination as one more argument and abort when the call would not fit. Where that size is all ones, which is __builtin_object_size not knowing, or where what the call writes is known to fit, the check cannot fail and the call is the plain one without it. Where it can fail the call may still get cheaper: a checking stpcpy whose answer nothing reads is a checking strcpy, and a checking strcpy of a known string is a checking memcpy. An append of nothing is the destination. The plain name has to be one the module does not declare with some other shape, and a call made plain is looked at again, up to three times.

§What a call has to be

For the printf family, its result has to be read by nothing. printf answers the number of characters written and puts answers a non-negative number that is not that count, so a program looking at the answer is a program this may not touch. The str and mem families are the other way round: the answer is the whole point of the call and the fold produces it, so a program reading it is the ordinary case.

It has to give back one value of the kind its name says it does. A program that declared strchr as something returning two values, or a number, declared a function of its own and a pointer into a string literal is not what it answers.

The name has to be the one the source spelled rather than the one the object file will carry. extern char *strstr (const char *, const char *) __asm ("my_strstr"); is a declaration of strstr, and a compiler that reads the symbol alone sees a call to a function it knows nothing about. So the callee is looked up through rucc_ir::Func::spelled, and a call this leaves behind is a call to whatever symbol the module says that name has, which is the rename again read from the other end.

The name has to be one this module does not define. A translation unit holding the body of its own fputs means that body, which is the rule crate::heap applies to malloc and for the same reason.

The function must not carry memory SSA yet, which where this runs it does not. Memory is threaded by crate::number, that pass is in the function pipeline, and this runs before the pipeline starts. The check is here anyway, because a call with a memory operand rewritten into one without would be a use of a value nothing defines.

§Where it runs, and why it is not a rule

Section 20.2 asks for folds like these to be rules in the rewrite DSL with the callee’s identity in the pattern, and most of them can be. These cannot. A rule rewrites one instruction into instructions, and two of the rewrites here need something no rule has: the name puts has to be interned before a call can name it, and printf("hello world\n") has to leave behind a string that is not in the module yet, because “hello world” with a terminator is not a suffix of “hello world\n” with one. So this is a module at a time transformation beside crate::ipcp and crate::ipasra, which is where the interner and the module both are.

-O1 and above, which is one level below where those two run. gcc folds these at -O1, the torture programs are compiled at every level from -O1 up, and the fold makes the program smaller as well as faster, so there is no level above -O0 where declining it is right.

Off under -fno-builtin and -ffreestanding, which is the flag pair section 20.1 describes, and off for one name at a time under -fno-builtin-<name>. A freestanding program left with a call to a puts it never wrote is a link failure, and that is the whole reason the flag exists.

Constants§

NAME
What the pass is called in -fopt-info and -fpass-fuel=.

Functions§

fold
Folds every call in the module whose output the compiler can work out.