> When both conditions are met, the loop body is replaced with a call to std::this_thread::yield().
Insert screaming here.
An infinite loop, with no library calls whatsoever, gets a system call inserted. That's a horrible surprise waiting to happen.
The entire concept of the "forward progress guarantee" is broken. An infinite loop should compile to an infinite loop. Nothing more, nothing less.
I guess given that it was UB before, the compiler was already allowed to put a system call here if it wanted for some reason
Not only that, the code was wrong. The specification is quite clear that correct programs don't cause UB to be executed at run time. If your wrong code now produces wrong results, that's because it's wrong. That your compiler allowed you to get away with it for decades is a compiler bug, not a feature.
Do I fully believe all of the above? Not exactly. But compiler authors do. Does it make a really good argument to never use C or C++? Yes. If only we had 50 years of optimization work in any language with better semantics.
Yeah, my slightly more verbose take is that a language that requires you to not ever make any mistakes in order to have a program behave in a predictable way is not a particularly good choice of language if you the ability to pick something else.
The mistake was declaring infinite loops to be UB in the first place.
> That your compiler allowed you to get away with it for decades is a compiler bug
That UB was added in C++11.
That sounds like 1.5 decades to me...
Yes, it was a horrible situation that has been replaced by an only slightly less horrible situation.
I don't totally agree with this. To me, UB is an order of magnitude worse than pretty much anything else, so this is more than "slightly less" horrible. I don't necessarily disagree that this is still horrible, but I also don't write an C++, so I'm mostly just commenting as an outside observer.
Yes, but now it has to. I guess that's better? than UB, maybe.
I grilled an LLM for a bit to see if it could justify the old forward progress rule. The only thing I got that passed the smell test was that it’s useful for the optimizer to be able to optimize:
by moving the store before the computation. (Stronger stores would require additional analysis.)I admit I’m unconvinced that this is particularly useful.
(I got many other ideas that did not pass my personal smell test.)
You will find the answer you seek not from an LLM, but from the talk Forward Progress Guarantees in C++ by Olivier Giroux at CppNow 2023. It's a long talk, with lots of details about forward progress, but I've set the timestamp[1] to the infinite loop bit.
[1]: https://youtu.be/g9Rgu6YEuqY?si=_l9JwKhjvIdFEDEX&t=3819
I thought it was generally so the compiler can merge two computation loops without proving if one of them runs forever .
Correct. See N1528: "Why undefined behavior for infinite loops?" https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1528.htm
But the compilers have to optimize the crap code in big tech codebases by 0.5%, it saves a lot of money.
Also performance doesn't matter that much and developer time is more important btw, keep using react.
It's not even about optimizing some big tech codebase by 0.5%. The progress guarantees in particular are in place s.t. Nvidia can choose a certain implementation strategy in Cuda C++ that has "surprising" consequences for users (one thread getting stuck in an infinite loop that never yields can livelock its entire warp) but still get to claim "full C++ standards compliance".
So let it livelock the entire warp when someone writes an infinite loop. Should we start replacing integer division by zero with INT_MAX so that people aren't "surprised" by their program crashing?
I mean that's what they did, and that's why there's that UB. All I'm saying is that this is the "weird platform behaviors exist and must be legalized by the standard" kind of UB and not the "we want a 0.5% win for benchmaxxing" kind of UB (the standard has plenty of both).
No, they didn't. UB is a cop out and inserting yield is just plain bad. Locking up one or more threads in an implementation defined manner would be the outcome of least surprise (I already know it's going to lock up at least the one thread).
The standards intended interpretation of UB was always intended to be something like "implementation defined, no documentation required" to allow for implementation weirdness, even unpredictable ones. It was compiler authors who decided do abuse this allowance to do really unintuitive things instead of weird platform weirdness.
100% There are implementations that have sane behavior for "UB" instead of making it an excuse to misbehave.
The ISO standard is not the same as a language from a single vendor.
IIRC a lot of it was benchmark gaming between GCC and LLVM.
Because one can bleeping see that that's what would happen. Locking up a thread isn't a good thing, but it's a lot better than UB. There was never a need to make this UB.
I don't see the issue. Just let wrong code do wrong things But let it do the expected wrong thing, rather than changing the code to something unexpected.
"No." <-- the C++ committee.
Isn't that the strategy for most of the stuff in C++? It's the common denominator of a wide variety of platforms. That's why numbers didn't have to be two's complement and characters didn't have to be ASCII for ages.
performance doesn't matter.... so datacenters would be equally happy running software that runs half as fast but uses 10% more power?
Yeah, this is almost the worst way they could choose to 'fix' the problem.
> An infinite loop should compile to an infinite loop.
I think that a compiler option should control this. It can be a nice optimization, but the programmer should be able to opt out.
Every flag that changes the semantics of the code bifurcates the language into two languages.
Alternatively every flag that changes the semantics of the code is a workaround for either legacy code no one will fix or language committee decisions that have unintended side effects.
Are C/C++ chars signed or unsigned? What a mess!
If we accept this definition, C++ already seems to be many different languages. At my job, I'm currently fighting floating-point determinism issues across different build configurations, compilers, CPUs, operating systems, and standard library and libm implementations so that snapshot tests pass with the same hashes on all platforms. I can confirm that this is a complete nightmare.
Yup. I tried hard to not allow D's behaviors to be changed based on a compiler switch. Yes, we have switches to enable certain features, but not silent behavior changes.
It's not perfect, but the forest of such switches in C compilers motivated D to not have them.
The programmer can opt out by terminating the loop.
Yeah, I don't get it either. Like if I wanted to call std::thread::yield() inside an infinite loop, I could, you know, just do that myself?
An obvious question (that TFA does not address) is, why is the forward-progress guarantee needed? Since that is the ostensible justification for this new invisible behavior.
I'm curious, what exactly do you imagine going wrong here?
The biggest headache will probably be it getting emitted in inappropriate contexts: where there is no actual means to sched_yield for whatever reason (bare metal, kernel, whatever). The second is just that the behaviour of the infinite loop changes: suddenly you're getting a bunch of extra system calls from your spinning thread instead of just a high CPU usage, which could disguise the issue or perhaps cause problems for other parts of the system. I don't see a good reason for the transformation: pretty much any time you are writing a bare infinite loop like this you don't want anything else to happen (it's also silly that it only happens with a particular spelling of an infinite loop, keeping the others still undefined).
"Emitted in inappropriate contexts" is very much one of the shapes I would expect unpleasant surprises to take, yeah. If you're writing code in C, you often need a lot of control over exactly what's happening. You might, for instance, be writing a .so for use with LD_PRELOAD, where it's important that you know everything being called so you can't accidentally recurse. You might be writing code for a sandbox, where you have an allowlist of permitted syscalls.
Exact control is only available in Assembly, minus unavoidable hardware flaws, everything else even a minor compiler update might change the outcome of the code.
> suddenly you're getting a bunch of extra system calls from your spinning thread instead of just a high CPU usage
Isn't the point that the loop was undefined behavior and so the spinning thread might not actually be spinning to begin with? It could be doing anything and sometimes did stuff like run the next block of code.
If you really want an infinite loop that does nothing (not sure why), you can do that now on any standards conforming compiler with some of the methods Sandor described.
It being undefined behaviour before doesn't make all possible definitions of that behaviour equally reasonable. The strangest thing to me is that I don't know who this behaviour definition is for. Infinite loops like this are a pattern that's almost entirely mutually exclusive with situations where a scheduler is relevant.
I'm not too concerned about it being possible to make a loop at all (there's a lot of ways to add a 'side-effect' that will probably result in the same assembly), I'm concerned with a) the strange unwillingness to just define a sensible behaviour in this case, especially when C already has one (and GCC already in practice implements a slightly different but also perfectly reasonable interpretation, both of which work for all the normal ways someone might write such a loop), and b) the huge amount of existing code which uses this construct because for the most part compilers did not actually cause problems with it.
Impl specific. If you're building bare metal, pass -ffreestanding so GCC knows it's not allowed to call OS functions.
arguably there are already several places in the language where things like this can happen. for example, initializing a static function variable has certain thread safety guarantees (two threads entering the function won't step on each other), and while it's nice to not worry about it, this can certainly be a problem if you're trying to stay close to the metal and not pull in any dependencies.
> I don't see a good reason for the transformation: pretty much any time you are writing a bare infinite loop like this you don't want anything else to happen (it's also silly that it only happens with a particular spelling of an infinite loop, keeping the others still undefined).
I'm not disagreeing with you, but two things worth considering are 1) you don't always write loops like that _intentionally_; 2) if a bug like that slips into production system, it would be good to make sure it doesn't starve other threads.
This is true. Static initialization will often generate calls to lock functions and that can be a faff to deal with. But I don't see what the point of the sched_yield() is. Using it at all is already a code smell and calling it repeatedly in a tight loop is the kind of thing kernel developers were trying to beat out of application developers decades ago because it just isn't really very helpful (and often actively harmful) with any but the dumbest of schedulers. It's certainly not very useful for avoiding thread starvation.
The language is already littered with these "the compiler shall insert" and then a reference to the STANDARD LIBRARY FEATURE N.X. Which means if you're compiling in a freestanding environment half the time you'll get linker errors such as "couldn't find symbol whatever". And what's worse the compiler inserts a call to a function that is LITERALLY STD NAMESPACED. Meaning you have to provide that signature yourself. See how std vector is hardcoded into compare/meta and I can't remember what else.
This then forces developers to create undefined behaviour because according to the standard you can't namespace std your own functions even though it's required to get it to work.
Freestanding environments specifically don't have to do this, they get a carve-out. (I dunno if this applies elsewhere though.)
Sounds like a compiler bug that a standard feature implementable in freestanding doesn't work in freestanding.
I would expect an infinite loop
to be designed to play nice with the scheduler, while I would assume a infinite loop to not play nice with the scheduler. Now, I can't really imagine where this matters except for horrible hacky attempts at faking a real time scheduler on windows, but breaking horrible hacky attempts at faking a real time scheduler sounds like the kind of bug you hear about in the evening news.I would expect them to be the same or for the former to be worse. It's rarely useful to call sched_yield at all, but calling it repeatedly in a loop seems more likely to expose bad behavior in a scheduler than improve the interaction. Schedulers are already perfectly well designed to handle threads trying to take up 100% of the CPU: that's the default state for any CPU-bound task.
under what conditions would the infinite loop be scheduled over something else after it has run out of its time slice?
I would expect quite a number of embedded use cases to suddenly break. Yay, free CVEs!
Forward progress guarantee is what allows for conversion between recursion and iteration for performance optimization. Otherwise these have different characteristics (recursion blows the stack, a loop hangs).
I don't understand your problem. Did you expect your C++ program to get uninterrupted access to the computer? What progression do you think isn't happening there?
I think you are misinterpreting that. That phrase unambiguously says the loop is preserved on the final binary.
I expect an infinite loop to be compiled into, for instance, a jump instruction jumping to itself. The OS, if there is any, is welcome to interrupt and context switch. I don't expect code that has no function calls at all to have a system call inserted into it.
A call to a standard library function is still subject to the as if rule. It doesn't have to manifest into a call instruction to a standard library function. Much like memcpy in source code doesn't have to manifest to a call instruction.
Yes, but the compiler does have to preserve any observable behavior produced by the call to the standard library function. Being able to omit this inserted yield() by the as if rule would mean that it isn't observable, which would also mean that the compiler could already add or not add it anywhere as needed without changing the behavior of the program. Which would seemingly make the inserted yield() pointless as it would have no effect.
Me: I don't expect to be stabbed.
You: But you only might be stabbed. It isn't required to happen only permitted.
Ok, I get this.
The problem is that what you want is completely against the spirit of the entire language.
If your point is that C++ should be more like C in general, I can agree with that. But if your point is that C++ should be literal on this specific case, performance be damned, and the rest of it is ok, then no, that's a bad one.
I was utterly unconvinced that the original infinite-loop UB gave the compiler any important performance optimization, and I'm unconvinced that this is providing useful value to compensate for its surprise. If I wanted a yield in my infinite loop, I'd add one.
The original UB was to allow the compiler to merge two loops without proving termination.
What difference does it make? If the loop doesn't terminate, it doesn't terminate, which is almost always a bug, except when it's not. If it does terminate, then great, it terminates.
Merging a buggy loop with another loop creates... a buggy loop.
Take this example:
It can be conveniently transformed into this: They are exactly equivalent except if the first loop never terminates.Now, the compiler could try to understand if the first loop does or doesn't terminate, and apply or not the optimization accordingly, but Turing tought us that is indeed a hard task!
Or it could decide to never apply it, for fear of those rare and usually pathological cases where the first loop doesn't terminate.
Or it could decide to apply it by default and accept that in those cases the program does something different than what the source code says. The latter is better known as UB.
The third option won, and that's why infinite loops are UB in the standard.
Any program in an OS only gets as much resources allocated to it as the OS allows (OK, in any general-purpose OS written in the past few decades). sched_yield() doesn't actually reduce that allocation in most cases, anyhow: in fact it has a higher chance of increasing the resources that the thread uses spinning in a loop because it's gonna be thrashing the scheduler as well.