16 ms·
There is no 'printf'
- guerrilla 5y agoMoar please. I'm loving these counterintuitive C optimization gotchas lately[1]. They are like little brain teasers. 1. https://news.ycombinator.com/item?id=28930271 https://news.ycombinator.com/item?id=28930271
- 0xcde4c3db 5y agoAbout a year ago there was something of a "joke isEven() implementation discourse" on Twitter, which eventually evolved a sort of informal optimizer abuse contest. For example: https://twitter.com/zeuxcg/status/1291872698453258241 https://twitter.com/zeuxcg/status/1291872698453258241 https://twitter.com/jckarter/status/1428071485827022849 https://twitter.com/jckarter/status/1428071485827022849
- aw1621107 5y agoOK, those are horrifying and fascinating, and they basically break my brain. Is there a explanation somewhere of why the first one "works"? The second one I think is the compiler assuming the default case will never be hit since it'll result in infinite recursion, which is UB under C++, so it's basically assuming 0<=x<=3 and optimizing from there. Is that correct? The first one I'm less certain about. The only thing I can think of is that the compiler deduces an upper limit of INT_MAX - 1 to avoid signed overflow, and then somehow figuring out the true/false pattern from there? Still a bit of a gap in my understanding there.
- barsonme 5y agoMy guess: since overflowing int is UB, and the only value of n that stops the recursion is zero, the compiler assumes that n must be zero and checks accordingly. That doesn’t explain why it uses test dil, 1 instead of test dil, dil or cmp 0 or whatever.
- eMSF 5y agoThe compiler cannot assume that much, because the argument is a signed integer (negative integers will not overflow and do have well-defined behaviour).
- davemp 5y agoThe rabbit hole goes deeper than that: https://gcc.godbolt.org/z/Tc1MTa6nj https://gcc.godbolt.org/z/Tc1MTa6nj
- rocqua 5y agoThat is a well defined function. And indeed implements isEven. Because unsigned int has defined overflow semantics. Essentially, it will eventually overflow and hit the correct base-case for 0.
- davemp 5y agoOptimizers have to keep the same input/output pairs unless there is undefined behavior. In the second function the truth table looks like: in | out ---------- 0b000 | 1 0b001 | 0 0b010 | 1 0b011 | 0 0b100 | don't care . . . MAX | don't care The compiler just chooses the most efficient way it knows to get the filled out entries correct which happens to be: in | ~in[0] ---------- 0b000 | 1 0b001 | 0 0b010 | 1 0b011 | 0 0b100 | 1 . . . MAX | 1 It would have been just as valid to do: in | in[2] or ~in[0] ---------- 0b000 | 1 0b001 | 0 0b010 | 1 0b011 | 0 0b100 | 1 0b101 | 1 . . . MAX | 1 The first function's table looks like: in | out ---------- 0b000 | 1 0b001 | don't care 0b010 | don't care . . . MAX | don't care And the compiler still likes the even check in this case, which makes sense.
- notriddle 5y agoThe first function (the `n == 0 || !isEven(n+1)` recursive function) has defined behavior for negative numbers. That's probably why it compiled to an even number check.
- davemp 5y agoHmm you're right that there's definitely more to the first function than coincidence. I dug a bit deeper and surprisingly when n is unsigned the function is still well defined and correct. [0] AFAIK clang will usually just treat a signed overflow as an unsigned overflow when optimizing as it simplifies things. So my bet would be that clang is just casting n to unsigned and performing some inductive reasoning similar to the proof I wrote below. Pretty impressive on the behalf of clang. [1] [0]: I did the proof below for anyone interested: int isEven(unsigned int n) { return n == 0 || !isEven(n + 1); } # assumptions MAX + 1 --> 0 # from the C standard MAX is odd # true for most machines # base case isEven(0) --> True # trivial short circuit # base case isEven(MAX): ==> return MAX == 0 || !isEven(MAX+1) # MAX is not zero ==> return False || !isEven(MAX+1) # MAX + 1 rolls over ==> return False || !isEven(0) ==> return False || !True ==> return False || False ==> return False ==> isEven(MAX) --> False # recursive steps isEven(n) where 0 < n < MAX ==> return n == 0 || !isEven(n + 1) # n cannot be zero ==> return False || !isEven(n + 1) # False || anything is tautological ==> return !isEven(n + 1) which will turn into an inverter chain of length MAX - n until you reach isEven(MAX) - an odd length inverter chain is the same as a single inversion - an even length inverter chain is the identity (can be proven by induction trivially) isEven(n) where 0 < n < MAX and n is even ==> return !isEven(n + 1) # MAX - n is odd, when n is even; replace with single inversion before isEven(MAX) ==> return !isEven(MAX) ==> !(False) --> True isEven(n) where 0 < n < MAX and n is odd ==> return !isEven(n + 1) # MAX - n is even, when n is odd; replace with identity of isEven(MAX) ==> return isEven(MAX) ==> False --> False All cases of n are accounted for so the function is correct (and equivalent to return n & 1 == 0). [1]: Yup clang just recognizes this optimization: https://gcc.godbolt.org/z/Tc1MTa6nj https://gcc.godbolt.org/z/Tc1MTa6nj
- archi42 5y agoIt's all fun and games until you write (or review) C/C++ test cases for a compiler or disassembler ;-) It never stopped to amaze me how good the compiler was to figure out that I actually wrote very complicated "return 0".
- perryizgr8 5y agoTldr: Not returning a value from main() is undefined behavior in ANSI C. So compiler will do whatever it likes. It will return 0 or 42 or crash. In this case, gcc just replaces printf with something else it likes more.
- _kst_ 5y agoNo, it's not undefined behavior. An aside: "ANSI C" usually refers to the C language as defined by the last standard directly published by ANSI, in 1989 -- but the ANSI organization has adopted each new ISO C standard after publication, so the C standard currently recognized by ANSI is ISO C 2017. We're not going to get people to change what they mean by "ANSI C", so I suggest referring instead to "C89" or "C90". In C90, not returning a value from main() causes an undefined status to be returned to the environment. It does not cause undefined behavior. The behavior of such a program is otherwise well defined. (C99 and later made reaching the closing "}" of the main function do an implicit "return 0;" instead, a rule borrowed from C++.)
- perryizgr8 5y ago> It does not cause undefined behavior. I haven't read the standard. I am just stating what TFA says. So are you saying it is inaccurate?
- _kst_ 5y agoYes, it's inaccurate. Here's what the C90 standard says: 5.1.2.2.3 Program termination A return from the initial call to the main function is equivalent to calling the exit function with the value returned by the main function as its argument. If the main function executes a return that specifies no value, the termination status returned to the host environment is undefined.
- deleted 5y ago[deleted]
- tptacek 5y agoHuh, this is pretty great; I've always fussily used fputs() when I'm just printing static strings, and apparently I don't need to bother, since the compiler will just do it for me.
- mid-kid 5y agoIt doesn't hurt to use (f)puts anyway when you intend to print a static string. This way you avoid extra overhead when using compilers that don't do this or building with flags that disable this optimization, as well as just clarify your intent.
- ncmncm 5y agoTypically if you're calling printf you are not concerned about overhead.
- eikenberry 5y agohttps://web.archive.org/web/20211019052752/https://www.netmeister.org/blog/return-printf.html https://web.archive.org/web/20211019052752/https://www.netme...
- cyberge99 5y agoApparently there is no available capacity for that site either.
- Bang2Bay 5y agohttps://search.yahoo.com/ https://search.yahoo.com/ for There is no 'printf' and look through the cache
- RcouF1uZ4gsC 5y agoThis is a bit like saying there is no '+'; Because if you put in return 1+2+3; And look at the assembly code, you will see that the compiler generated something like return 6; The compiler is allowed to take advantage of the standard to substitute in more efficient code that does the same thing. IIRC, for C++, it would actually be ok if std::vector was implemented completely as a compiler intrinsic with no actual header file. (No compiler I am aware of actually does it that way).
- malkia 5y agoIs there more info to this, I remember this from Commmon Lisp (but details evade me) that the compiler can take benefit of certain specific functions and rely on them being... "open coded" - e.g. it can produce more efficient code by replacing these with something more suitable... http://www.sbcl.org/manual/#Open-Coding-and-Inline-Expansion http://www.sbcl.org/manual/#Open-Coding-and-Inline-Expansion https://www.thecodingforums.com/threads/what-is-the-meaning-of-the-term-open-coded-in-the-following-extract.541773 https://www.thecodingforums.com/threads/what-is-the-meaning-...
- talaketu 5y ago> more efficient code that does the same thing In this case, it produces a different result.
- Someone 5y agoCode that does #include <vector> must compile, so that header must exist (whether it is stored in a file is the implementer’s choice. AFAIK, the standard carefully avoids the use of the term ‘header file’) Also, I think code that doesn’t do that include must fail to compile when it tries to use std::vector. So, logically, that header must exist.
- deleted 5y ago[deleted]
- mormegil 5y agoSo, why does puts do "return r ? EOF : '\n';"? Some backwards compatibility? Or is there a logical reason for that?
- masklinn 5y agoPer the man: > puts() and fputs() return a nonnegative number on success, or EOF on error. r is the result of the write, if it’s nonzero the write failed and thus so did puts.
- m45t3r 5y agoYeah, but I think the question was why EOF and "\n". It could as easily just return 1 or -1 for example, and it would make more sense I think.
- kevin_thibedeau 5y agoputs() always adds a line termination so success means that '\n' is the last char for that implementation.
- masklinn 5y agoThat's more of an implementation detail, probably a BC remnant from when it called putchar in a loop and you’d get the result if the last putchar.
- _kst_ 5y agoThat particular implementation probably returns the result of the last fputc() or equivalent that it called. puts() returns EOF (typically -1) on error, or some unspecified non-negative value on success. fputc() returns EOF on error or the written character, treated as an unsigned char and converted to int, on success. Don't expect all puts() implementations to do the same thing. For example, the glibc implementation appears to return the number of characters written on success. Implementations are free to rely on implementation-defined behavior. User code that's intended to be portable cannot.
- rrauenza 5y agoQuick Summary: The C compiler optimizer replaces printf("Hello World!\n") with puts("Hello World!\n") and the implicit return from main() changes from 13 (the return value of printf) to 10 (the return value of puts)
- moffkalast 5y agoCalls on puts you say?
- mypalmike 5y agoThe behavior is optional.
- QuestingElf 5y agoC is Citigroup
- littlestymaar 5y agoNo, C stands for Capitalism.
- helmholtz 5y agoBrilliant.
- enlyth 5y agoIn other words long volatility
- deleted 5y ago[deleted]
- Pxtl 5y agoThis sounds like a compiler optimizing bug, in that if the expected behavior of main without a return is that it will return the result of the last function call, then the last function in the main does use its return value and that should not be optimized.
- qwerty456127 5y ago> puts(3) only returns "a nonnegative integer on success and EOF on error" How does it decide which nonnegative integer to return?
- robotresearcher 5y agoIt's arbitrary. The article shows an implementation that returns 10 (ASCII '\n'). But the spec says it doesn't matter, so you should only be using it to test >0 for success.
- woodruffw 5y agoThat's answered below: > On success, puts(3) appears to return '\n', the newline or line feed (LF) character, which has ASCII value... 10. But note that that isn't standard behavior. The language in POSIX[1] is identical to that in the blog post. `puts` is free to return whatever positive number it wants on return. [1]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/puts.html https://pubs.opengroup.org/onlinepubs/9699919799/functions/p...
- ltr_ 5y ago[off topic] I always wondered how '%n' is used in production code.
- dmitrygr 5y agohttps://stackoverflow.com/questions/3401156/what-is-the-use-of-the-n-format-specifier-in-c https://stackoverflow.com/questions/3401156/what-is-the-use-...
- professoretc 5y agoThose answers were so frustrating to read. The question is explicitly asking "Why does this exist? When would you need it?" and yet most of the answers are "This is what it does." (sometimes with a few snide RTFMs thrown in for good measure).
- ltr_ 5y agothat was exactly what i was asking for, i know how it works, but i would like to know how it is used by pros, (if used at all)
- anonymousiam 5y agoCompiler optimization can sometimes cause unpredictable or even incorrect behavior. Below is a blob of C code for the TI MSP430 compiler that exemplifies at least one of TI's optimization bugs: // Define Common Communications Frame typedef volatile union commFrameType { struct { unsigned SyncHeader:16; unsigned MessageID:8; unsigned short MessageData[msgDataSize]; // ID-unique data unsigned CRC:8; // LSB of CCITT-16 for above data } __attribute__ ((packed)) Frame; unsigned char b[16]; // Accessible as raw bytes as well unsigned short w[8]; // Accessible as raw words as well unsigned long l[4]; // Accessible as raw long words as well } __attribute__ ((packed)) CommFrame; static CommFrame IpcMessage = { FRAME_SYNC_R, IpcBlankMessage }; // If frame was accepted into TX queue, prepare next frame for transmission // IpcMessage.Frame.MessageID++; // Bump up to next message type // IpcMessage.Frame.MessageID += 1; // The above two lines that are commented out cause a bizzare linker error if either are used instead of the line below. IpcMessage.Frame.MessageID = IpcMessage.Frame.MessageID + 1; // Bump up to next message type The MSP-430 is a 16-bit microcontroller and the packed CommFrame structure has Frame.MessageID on an odd-byte boundary. Some processors might raise a SIGBUS, but TI says that it's okay to access a byte on an odd address boundary. It's pretty silly that i++; and i+=1; don't work, but i=i+1; is just fine.
- secondcoming 5y ago'unsigned MessageID:8;' isn't the same as 'unsigned char MessageId'
- anonymousiam 5y agoI agree. It's explicitly an unsigned 8-bit integer (bit field). A 'char' can have a different number of bits on different architectures.
- GoblinSlayer 5y agoImagine somebody thought omitting the return statement and doing whatever the compiler likes is a good feature to have.
- dboreham 5y agoLike Scala?
- dnautics 5y agopretty sure scala (and most FP) has a well-defined "what to do when you leave off the return statement", not one that "is up to the compiler"
- gizmo686 5y agoNot just FP. "Return the value of the last statement" is fairly common in imperative languages as well. Off the top of my head both Pearl and R do so as well.
- bodyfour 5y agoNot that odd if you know the evolution of the language... and what it actually means for modern programmers. The very first C compilers ran on a PDP-11 in just a few dozen kilobytes of memory. The entire emphasis was on minimalism, and that meant that things like type enforcement was left to the human. One of the things the earliest language was missing was the "void" type. If you didn't give a return type to a function, it just defaulted to returning "int". Therefore it was totally normal to have a function fail to return anything. Since there was no distinction between "function returning int" and "function returning nothing", there was nothing stopping your program from using the value returned... it just meant you got whatever happened to be in the right register. What does it mean for us today? Basically nothing. "void" was added 30+ years ago when ANSI C appeared. The language couldn't just break the old behavior but in any rational environment you enable enough compiler warnings to avoid these ancient quirks entirely: $ gcc -Wall -c a.c a.c:1:1: warning: return type defaults to ‘int’ [-Wimplicit-int] 1 | foo() {} | ^~~ a.c: In function ‘foo’: a.c:1:8: warning: control reaches end of non-void function [-Wreturn-type] 1 | foo() {} | ^
- monocasa 5y agoThere is 'printf'. It's just that printf (and the rest of the standard library) is technically as much a part of the C language as the language grammar itself, and C compilers are welcome to use innate knowledge of those functions for optimizations. The other place you typically see this is calls to functions like memcpy/memset being elided to inline vector ops or CISC copies, or on simpler systems, large manual zeroing and copying being elided the other way to a memset or memcpy call. C compilers will typically have an escape hatch for envs like deeply embedded systems and kernels like gcc's -ffreestanding and -fno-builtin that says "but for real though, don't assume std lib functions exist or you know what they are based on the function's name". <rust_task_force> One of my favorite parts of rust as someone who uses it for deeply embedded systems is the separation of core and std (where core is the subset of std that only requires memcpy, memset, and one other I'm forgetting). The rest of the standard library is ultimately an optional part of the language with compiler optimizations focused on general benefits rather than knowing at the complier how something like printf works. no_std is such a nicer env than the half done ports of newlib or pdclib that everyone uses in C embedded land. </rust_task_force>
- deleted 5y ago[deleted]
- matheusmoreira 5y ago> C compilers will typically have an escape hatch for envs like deeply embedded systems and kernels like gcc's -ffreestanding and -fno-builtin that says "but for real though, don't assume std lib functions exist or you know what they are based on the function's name". GCC requires standard library functions even on freestanding environments! It will still emit calls to those functions in certain circumstances. https://gcc.gnu.org/onlinedocs/gcc/Standards.html https://gcc.gnu.org/onlinedocs/gcc/Standards.html > GCC requires the freestanding environment provide memcpy, memmove, memset and memcmp. > if __builtin_trap is used, and the target does not implement the trap pattern, then GCC emits a call to abort.
- quietbritishjim 5y agoThis comment isn't meant to disagree with what you said but is more like a related ramble. At least in C++, memcpy is special because it can be used to reinterpret bits of one object as another (in C you can use a union, and as of C++20 you can use the std::bit_cast function but it either just calls memcpy or it still needs its own magic) double d = 1.0; // type punning with union, allowed in C but not C++ union { double d; int64_t i; } u; u.d = d; int64_t y1 = u.i; // undefined behaviour // cast not allowed, not even reinterpret_cast int64_t y2 = *reinterpret_cast<int64_t*>(&x.d); // undefined behaviour // memcpy - allowed! (if sizeof() params is the same) int64_t y3; static_assert(sizeof(y3) == sizeof(d)); memcpy(&y3, &d, sizeof(y3)); // well defined! // C++20 bit_cast (probably just a convenience wrapper around memcpy) int64_t y4 = std::bit_cast<int64_t>(d); // well defined It seems wasteful to create a separate object in memory and copy bits from an old object, when all you want to do is reinterpret the bits of the existing chunk of memory. But memcpy doesn't do that (or at least might not) because it's optimised into a simple cast under the hood, but without the undefined behaviour. So memcpy looks like a standard library function but in a way it's more like a compiler instrinic. It would be possible to create a fully conformant version of memcpy, even in C++, by casting both parameters to char* which is special with respect to the aliasing rules, and copying char-by-char. But I suppose that would be harder to optimise into a simple cast.
- jeroenhd 5y agoMy compilers call printf just fine until you enable optimizations. -O0 adds references to printf, -O1 and higher switches to puts. I was kind of surprised about the fact that there was no warning about the missing return from main(). Normally, I'd expect the compiler to complain that a supposed int returning function does not return anything, because that would normally be undefined behaviour.
- comex 5y agoIf you use GCC, enable -Wall and you'll get the diagnostic you want: /tmp/cp.c: In function 'main': /tmp/cp.c:4:1: warning: control reaches end of non-void function [-Wreturn-type] 4 | } | ^ If you use Clang, there is no diagnostic even with -Wall (or even -Weverything), but it looks like Clang always implements the implicit `return 0;` from C99 regardless of the `-std` setting.
- kevinoid 5y agoTo clarify, gcc -Wall only produces the warning with -std=c89/gnu89 (or GCC < 5 without -std=c99/gnu99). As with clang, it implements the implicit `return 0;` for C99 and later. Example: https://godbolt.org/z/Y7Wx5Y89q https://godbolt.org/z/Y7Wx5Y89q
- jcranmer 5y ago> I was kind of surprised about the fact that there was no warning about the missing return from main(). Normally, I'd expect the compiler to complain that a supposed int returning function does not return anything, because that would normally be undefined behaviour. If main() returns without a return statement, it is defined to return 0 by the specification. (§5.1.2.2.3 of http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2596.pdf http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2596.pdf, but this hasn't changed in quite some time).
- quietbritishjim 5y agoThe article already explains this. What you said is only true in C99 (and they indeed show the return value of the overall program is 0 when compiled in that mode). In C89 it's undefined behaviour, so a warning when you do this would be very sensible.
- moron4hire 5y ago"I wanna be close to the metal. I wanna be in complete control. I wanna code in C!" Yeah. Right.
- kazinator 5y ago> But what if you're not using C99 or newer? If you're using C90, but under an implementation that supports C99, that implementation should obey all the new rules in all areas where there is no conflict between C90 and C99. The ISO C90 standard is obsolescent, so the fact that dropping off the end of main with no return value is an unspecified termination status is an obsolescent requirement (or non-requirement). It is possible for a conforming C90 implementation to return 0 in that case. A conforming C99 implementation must do that as a conformance requirement. There is no good reason for a C99 implementation behaving in a C90 compatibility mode to simply drop non-conflicting C99 requirements and revert a behavior such as this. This should be a don't care issue, unless you're targeting a bona fide nothing-but-C90 implementation. However, if you are telling your implementation to be C90, what reason are you doing that for? If it's not just some nerd gesture to show your contempt for C99, and you really care about portability to C90 implementations, then you probably want to be returning an explicit 0 from your main. Or even just to show contempt for C99, really, you should be returning that 0.
- _kst_ 5y agoRegardless of how you think a modern compiler in C99 mode should behave, the fact is that "gcc -std=c90" generates code that returns an arbitrary value from main if no return or exit is executed. clang doesn't do this as far as I can tell -- though it's difficult to be sure, since returning a status of 0 is valid C90 behavior.
- ikerdanzel 5y agoFault is at gcc. The option c90 should be deprecated totally removed. It is running Windows 11 and expecting program written for Windows 3.1 still runs. Those who still runs old copy of gcc, they know what they are getting. They assume the risk.
- kazinator 5y agoI would say that is a bug. More than 22 years ago, ISO C introduced this requirement for a reason. The reason was almost certainly the desire to fix that issue for as many C programs as possible, and that there was no intent that there be an exceptions for C programs that happen to request C90 compatibility in their accompanying Makefile. An indeterminate termination status causes real problems like, oh, if those programs are run out of POSIX script in "set -e" mode, the script will randomly terminate at that point. It has consequences. This requirement plugged a hole; when those programs are recompiled with compiler which adopts the requirement, that issue is fixed. From the developer's perspective, using GCC in C90 mode does mean they should be prepared to conform to C90 and not do things like that falling off the end of main if there is any risk that the termination status matters. In my original comment above, I made remarks to that effect. But that developer isn't the only stake holder. A developer working in C90 can still accidentally forget the return, and some downstream user who doesn't even program in C is affected by that. We also have to think from the point of view of a that downstream user building and operating a program received from such a developer. The user wants a program that has a successful termination status when it terminates normally an de facto successfully; the user doesn't care whether that comes from the compiler, or whether the program takes care of it. It's pretty shoddy from that not to come from the compiler, almost a quarter century after it was declared that, in the C language, "int main(void) {}" is now a successfully terminating program! When you request dialect compatibility from an implementation, you don't want old bugs to come back, unless they are specifically required. Your downstream user doesn't want that, certainly. If you really want old bugs, you go get the actual old compiler; -std=c90 passed to GCC 11 does not mean "emulate every detail of GCC 1.x".
- deleted 5y ago[deleted]
- deleted 5y ago[deleted]
- kazinator 5y agoThere is no printf because there is no defined behavior. To call a variadic function, you must have a prototype declaration in scope. (A correct prototype, needless to say.) A non-prototype (i.e. old-style) declaration cannot declare a variadic function such as printf, so no such declaration can be correct. If the function is not declared, then a declaration will effectively be assumed for the call, deduced from the types of the actual arguments, and a return value of int. In this case, the function will be "implicitly declared" to look something like int printf(char *); but that is not the correct declaration for printf. Therefore, the call has undefined behavior. The optimizer is allowed to rewrite this to: puts("daemons are running in your nose"); Likewise, it is allowed to rewrite it to: puts("Hello World!");
- lifthrasiir 5y agoNo, this optimization also happens when you `#include <stdio.h>`. More accurately speaking, GCC (among others) evidently assumes that printf is the standard C library function no matter prototype is defined or not and replaces it with puts under the same assumption plus the following additional conditions: - No other arguments are present. - The format argument has no format specifiers and ends with `\n`. - The return value is unused. (So `return printf("...\n");` would have prevented this optimization.)
- deleted 5y ago[deleted]
- SavantIdiot 5y agoIn other news: man discovers compilers do things you don't expect when you operate out of spec. Jeeze, don't tell him about semihost-supporting compilers like IAR or Keil, where printf can be one of several different things depending on your target or what configuration options are set for debugging.
- 1vuio0pswjnm7 5y ago"Pop quiz! What will the following program return? int main() { printf("Hello World!\n"); } Easy, right? It's gotta be 0, because since at least ISO/IEC 9899:1999 (aka "C99"), main shall implicitly return 0 if you, the forgetful programmer, didn't bother to explicitly return a value:" As hobbyist programer I write small programs and run them on older computers, running a variety of OS. Among other things, I use -std=c89 for various reasons. Thus, I know the answer to the pop quiz, for me, is not zero. Being forgetful triggers a warning. cat <<eof >1.c int main() { printf("Hello World!\n"); } eof cc -Wall -std=c89 -pedantic -ansi 1.c ./a.out 1.c: In function 'main': 1.c:4:10: warning: implicit declaration of function 'printf' [-Wimplicit-function-declaration] 4 | printf("Hello World!\n"); | ^~~~~~ 1.c:4:10: warning: incompatible implicit declaration of built-in function 'printf' 1.c:1:1: note: include '<stdio.h>' or provide a declaration of 'printf' +++ |+#include <stdio.h> 1 | 1.c:5:2: warning: control reaches end of non-void function [-Wreturn-type] 5 | } | ^ As a learning exercise I try to silence the warnings one by one. Instead of just including <stdio.h>, which teaches me nothing, I find the prototype string and insert it the .c file using a short script something like the following #!/bin/sh test $# = 1||exec echo usage: $0 function grep -r " $1(" /usr/include/* 2>/dev/null|sed 's/.*://;s/ *//' This enables me to learn that the printf protoype uses __restrict instead of restrict, and consequently a portability note: On Linux, __restrict is defined in /usr/include/features.h On NetBSD, __restrict is defined in /usr/include/sys/cdefs.h Now, with the printf() prototype included sed \$r1.c <<eof >2.c int printf(const char *restrict, ...); eof cc -Wall -std=c89 -pedantic -ansi 2.c ./a.out 2.c: In function 'main': 2.c:5:2: warning: control reaches end of non-void function [-Wreturn-type] 5 | } | ^ The -std=c89 option enables me to learn to use exit(). Using return statement will also suppress the warning. For the main() function however, cf. a subroutine inside main(), I use exit() instead of return. sed 4r2.c <<eof >3.c exit(0); eof cc -Wall -std=c89 -pedantic -ansi 3.c ./a.out 3.c: In function 'main': 3.c:5:35: warning: implicit declaration of function 'exit' [-Wimplicit-function-declaration] 5 | printf("Hello World!\n");exit(0); | ^~~~ 3.c:5:35: warning: incompatible implicit declaration of built-in function 'exit' 3.c:1:1: note: include '<stdlib.h>' or provide a declaration of 'exit' +++ |+#include <stdlib.h> 1 | Find the prototype for exit() and add it in, instead of just blindly including <stdlib.h>. sed 2r3.c <<eof >4.c void exit(int); eof cc -Wall -std=c89 -pedantic -ansi 4.c ./a.out
- matheusmoreira 5y agoCompiler optimization is really annoying sometimes. It's allowed to assume that functions with the same name as standard library functions behave according to standard. They'll swap out printf calls for puts if they don't have a format. The compiler just knows it can do that. Sure, it optimizes things but it gets to the point the code no longer reflects what's written on the source file. Try to hook into printf and it won't work because it's not actually calling printf. Even on freestanding environments they can and will generate calls to memcpy and memmove. That's insane...
- _kst_ 5y agoWhy is that a problem? (That's a serious question. I'm not suggesting that it isn't a problem. Apparently it is for you.) If I write printf("Hello, world\n"), all I care about is that those characters are written to the standard output stream when I run the program -- and that's all the language standard specifies. I rarely even look at the assembly or machine code. As someone else mentioned, "gcc -fno-builtin" inhibits optimizations like this. Other compilers are likely to have similar options.
- matheusmoreira 5y agoBecause it's surprising and breaks our expectations. The author of this article clearly expected a call to printf to be present in the generated code. > all I care about is that those characters are written to the standard output stream when I run the program Sometimes people care about a lot more. Such as the ability to hook into a specific function or ensuring the compiler doesn't generate calls to certain functions. > that's all the language standard specifies The author clearly cared about the undefined behavior. He had a mental model of what would happen that was perfectly reasonable. It was constantly invalidated by the optimizer. > I rarely even look at the assembly or machine code. I do. It's very jarring when you write some code and the compiler deletes some of your calls and reorders the rest. It can really complicate debugging sessions. > gcc -fno-builtin Yeah, that's become a standard flag for me. Just checked the documentation and it turns out it's also implied by -ffreestanding which is a better language than hosted C anyway just because it gets rid of all the libc cruft.
- DeathArrow 5y agoWhen you call printf you will actually issue a system call to write to stdout. Does stdlib directly issue system calls or does it use some inline assembly?
- unanswered 5y agoWhat do you feel the difference is between "directly issuing system calls" and "some inline assembly"?
- DeathArrow 5y agoUsing some system interrupts vs calling some driver.
- BenjiWiebe 5y agoTo issue system interrupts (afaik) it would have to use inline assembly. Or non-inline assembly, linked to the rest of the library. C doesn't have an 'int' (call interrupt) primitive. So the answer to your question is yes.