Live data from Hacker News

There is no 'printf'

netmeister.org

71–80 of 158 posts

Re: There is no 'printf'

#71
post #64

Quick 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)

This 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.

Yeah honestly this blog post just seems to be highlighting a bug in gcc when it is compiling to an older version of C. Not sure why the author chose to frame it as some big point about the printf function.

Re: There is no 'printf'

#72
post #40

Earlier quoted context omitted.

Optimizers 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…

The 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.

Hmm 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  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  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  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

Re: There is no 'printf'

#73

Quick 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)

Calls on puts you say?

The behavior is optional.

Re: There is no 'printf'

#74
post #65

> 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…

Regardless 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.

Fault 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.

Re: There is no 'printf'

#75
Compiler 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...

Re: There is no 'printf'

#76
post #67

Earlier quoted context omitted.

My 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.

The compiler cannot assume that much, because the argument is a signed integer (negative integers will not overflow and do have well-defined behaviour).

The rabbit hole goes deeper than that: https://gcc.godbolt.org/z/Tc1MTa6nj

Re: There is no 'printf'

#77
post #65

> 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…

Regardless 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.

I 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".

Re: There is no 'printf'

#78

Quick 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)

Calls on puts you say?

C is Citigroup

Re: There is no 'printf'

#79

Compiler 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 i…

Why 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.

Re: There is no 'printf'

#80

> 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…

> 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.

A compatibility with a code which depends on a behaviour of a previous version of the compiler is a reason enough.

If compatibility is dropped you now have a very subtle bug which is hard to discover and understand unless you read about that particular GNUism before. It's also a perfect setup for Schrödinger's bug.

Post reply on HN