That's not the issue.
Suppose you have some state like this:
const node *head1;
int sum1, sum2;
And you have:
void func()
{
for ( int *p = head; p; p = p->next )
sum1 += p->val1;
for ( int *p = head; p; p = p->next )
sum2 += p->val2;
}
The compiler really wants to merge the loops (this will be a nearly 2x speedup in this contrived case). In other words, the compiler would like to generate this instead:
void func()
{
for ( int *p = head; p; p = p->next ) {
sum1 += p->val1;
sum2 += p->val2;
}
}
Naively, this optimization looks obviously correct: since there is no synchronization in func(), nothing could validly observe the changes in the
order of the stores.
Here's the problem. While C and C++ consider data races to be UB (which is why the compiler is allowed to mess with the order in which potentially shared state is written here), the presence of a data race is still observable in a problematic sense. Suppose thread 2 is doing something like this:
while (true) {
printf("%d\n", sum2);
}
If thread 1 calls func() while this loop is running, then the program has undefined behavior [0].
Except there's a really nasty corner case. If the linked list has a cycle, then func() contains an infinite loop. (All it takes to cause this is head->next == head.) And, if func() has an infinite loop then, as originally written, sum2 is never modified and there is
not a data race. So a sneaky programmer could set up the infinite loop, call func() in one thread, do the printf loop in another thread, and the compiler would need to run that code correctly
because it's not UB. If the compiler transforms func() as above, then it introduces a data race where none existed, and it's a bug.
But this optimization seems important, and C and C++ sidestep this issue by declaring that func() itself is UB if the linked list contains a cycle. So the transformation does not introduce UB in my example because, in the problematic case, the UB is already there in the original code. Problem solved. Yuck.
(Realistically the compiler will also probably accumulate the sum in registers and add to sum1 and sum2 at the end. One could quibble that this subsequent transformation invalidates my point, but it's easy enough to make a slightly more complex example that doesn't have this problem.)
None of this is to say that I like C and C++'s solution. It's gross. The new C++ change to sort-of-solve it is extremely gross.
FWIW (and I sort of alluded to this above), there is an IMO much more interesting reason that compilers should care about infinite loops that doesn't apply to C/C++. In languages like Lean (but borrowing C-like syntax), you can write something like:
ProofType proof()
{
// some body here
}
The entire basis of the proof model in Lean is that the existence of a "term" like proof() that returns the type ProofType implies that an object of ProofType can be constructed (I think this is usually described as saying that ProofType is "inhabited"). This is pretty concrete -- you could literally run proof() to obtain this object.
But infinite loops completely break it: you could just write:
ProofType proof()
{
while (true)
;
}
(Sure, a clever compiler could reject this particular function. But a clever programmer can out-clever the compiler.)
So, in Lean, you either need to prove to the compiler that all your loops terminate or you need to mark the function as "partial", which tells the compiler that it cannot assume that the existence of the function means that the return type is inhabited. This would be a pretty radical change to C and C++, but it would fully solve forward-progress problem :)
[0] This one is no joke. I can come up with examples that would jump to inappropriate addresses using a construct like this if there's a data race -- just replace sum2 with a function pointer.