4 ms·
> 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 c
by JoshTriplett 8d ago
> 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.
- rcxdude 8d agoYeah, this is almost the worst way they could choose to 'fix' the problem.
- ameliaquining 8d agoI'm curious, what exactly do you imagine going wrong here?
- rcxdude 8d agoThe 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).
- JoshTriplett 8d ago"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.
- pjmlp 7d agoExact control is only available in Assembly, minus unavoidable hardware flaws, everything else even a minor compiler update might change the outcome of the code.
- criddell 8d ago> 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.
- rcxdude 7d agoIt 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.
- nicebyte 7d agoarguably 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.
- rcxdude 7d agoThis 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.
- mitxela 7d agoImpl specific. If you're building bare metal, pass -ffreestanding so GCC knows it's not allowed to call OS functions.
- rfgplk 8d agoThe 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.
- ameliaquining 8d agoFreestanding environments specifically don't have to do this, they get a carve-out. (I dunno if this applies elsewhere though.)
- mitxela 7d agoSounds like a compiler bug that a standard feature implementable in freestanding doesn't work in freestanding.
- yk 8d agoI would expect an infinite loop while(true) std::this_thread::yield(); to be designed to play nice with the scheduler, while I would assume a infinite loop while(true); 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.
- nicebyte 7d agounder what conditions would the infinite loop be scheduled over something else after it has run out of its time slice?
- rcxdude 7d agoI 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.
- 112233 7d agoI would expect quite a number of embedded use cases to suddenly break. Yay, free CVEs!
- ozgrakkurt 8d agoBut 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.
- muvlon 8d agoIt'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".
- JoshTriplett 8d agoSo 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?
- muvlon 8d agoI 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).
- fc417fc802 8d agoNo, 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).
- mitxela 7d ago
- ibobev 8d ago> 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.
- Xirdus 8d agoThe programmer can opt out by terminating the loop.
- WalterBright 8d agoEvery flag that changes the semantics of the code bifurcates the language into two languages.
- ibobev 7d agoIf 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.
- WalterBright 7d agoYup. 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.
- cryptonector 7d agoAlternatively 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.
- WalterBright 7d ago
- saghm 8d agoI guess given that it was UB before, the compiler was already allowed to put a system call here if it wanted for some reason
- fc417fc802 8d agoYes, it was a horrible situation that has been replaced by an only slightly less horrible situation.
- saghm 8d agoI 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.
- chowells 7d agoNot 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.
- saghm 7d agoYeah, 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.
- JoshTriplett 7d agoThe mistake was declaring infinite loops to be UB in the first place.
- marcosdumay 8d agoI 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.
- JoshTriplett 8d agoI 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.
- marcosdumay 8d agoOk, 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.
- JoshTriplett 8d agoI 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.
- mitxela 7d agoThe original UB was to allow the compiler to merge two loops without proving termination.
- CamperBob2 7d agoWhat 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.
- usefulcat 8d agoYeah, 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.
- deleted 8d ago[deleted]
- UncleMeat 8d agoForward 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).
- amluto 8d agoI 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: messy_pure_computation(); some_atomic.store(1, relaxed); 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.)
- raphlinus 8d agoYou 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 https://youtu.be/g9Rgu6YEuqY?si=_l9JwKhjvIdFEDEX&t=3819
- amluto 7d agoI don’t really buy that justification, for three reasons: 1. Most implementations do not do this automatic cooperative multitasking trick and most users [0] don’t want it done to their code. 2. The fact that a “step” is guaranteed to happen in finite time is far too weak for most use cases. I’ve done plenty of kernel programming, and a lot of kernels are partially or fully cooperative scheduled. Even somewhat long loops need manually inserted preemption points. 3. “Finite” can be a very long time indeed. There are literally competitions to see who can make the largest busy beaver machine. Put another way, undefined behavior is a sharp line - if code has UB, it has UB and it if doesn’t, it doesn’t. But code being slow is not a sharp line - something can take 1 ns or 1 ms or 1 second or 1 hour or 1 year or 100 years or 1M years, etc. A scheduler that fails to schedule a runnable thread in finite time is wrong, but so is a scheduler that fails to schedule it for 100 years or for a week. If it merely takes a minute, then whether it’s right or wrong depends on the situation. So if you’re talking about schedulers (which that part of talk mostly is), then I don’t think the ability to say “infinite loop without side effects are UB, so my scheduler is correct if I assume that all side-effect-free loops are finite” is actually useful. There are real world examples. At one point, Go only preempted its cooperative threads at certain points, but this was a problem and newer versions of Go can even preempt tight loops. Python, which threads the worst-of-both-worlds middle ground between asynchronous and cooperative preemption, does not allow an infinite loop (in ordinary Python code) to starve other threads. [0] Most users of C- or Rust-like languages anyway. Quite a few more managed languages (e.g. Go) are the other way around.
- classified 7d ago> Insert screaming here. I have the suspicion that the members of the C++ standards committee are increasingly not from this planet.