|
|
Subscribe / Log in / New account

Footguns

Footguns

Posted Jul 10, 2021 18:21 UTC (Sat) by khim (subscriber, #9252)
In reply to: Footguns by tialaramex
Parent article: Rust for Linux redux

> Maybe it should have said platform defined, too late now.

Why is it too late? Correct programs would stay correct and incorrect programs (which their authors perceive as correct!) would become truly correct.

In fact his is exactly what was supposed to happen: Undefined behavior gives the implementor license not to catch certain program errors that are difficult to diagnose. It also identifies areas of possible conforming language extension: the implementor may augment the language by providing a definition of the officially undefined behavior (emphasis mine).

Signed overflow was declared “undefined” not because of differences in hardware, because of ones-complement — but simply because it was obvious to the developers of C almost half-century ago that it's hard to add new undefined behaviors but easy to remove them.

That was an era when developers of the standard cared about users of the language, though. Over time users stopped being direct influencers of the standard. Instead it caters more to the developers of the compilers. Who do care about how can they write a compiler and don't really care about the need to, you know, use that compiler. To write real programs. That is not a problem. They can/should navigate the footguns when they no longer make any sense.

> Rust gets to just say this up front.

And C++20 says that, too. Except they kept most the footguns there to trip the unwary. They just demand two's complement now.

Rust is not entirely consistent there, as you have correctly noted. Overflow may panic (in debug builds or if you use -C overflow-checks or it may silently wrap (like it does in C with unsigned types). What it can not do under any circumstances is to give the compiler license to turn the whole program into a pile of goo. Not even in unsafe blocks.

> It's not true that Rust doesn't try to outright remove footguns.

Okay. You are right. Some footguns are removed entirely. Many more still live. But only in unsafe code blocks.

> that's a much smaller set than you might have expected.

You sound just like von Jolly. Who said to Max Planck back in the 1878: in this field, almost everything is already discovered, and all that remains is to fill a few unimportant holes. Same with Rust: yes, unsafe gives you a tiny number of superpowers, but abuse of these superpowers can easily cause effects which will infect the whole program and would cause undefined behaviors god know where.

That's why is important to see how many unsafe blocks real drivers would requite and what kind of code would you need to include in such unsafe blocks.

Very few unsafe blocks (but tricky and complex to use ones) may herald the return of majority of C-style footguns. Just the ability to transmute to the value which doesn't exist in enum may lead to very misterious crashes in completely superficially unrelated code, e.g.


to post comments

Footguns

Posted Jul 10, 2021 20:07 UTC (Sat) by tialaramex (subscriber, #21167) [Link] (112 responses)

> Why is it too late?

In practice when they have an opportunity to do so, all the platforms you care about (e.g. GCC or LLVM) will leave the definition blank. They don't want to commit to anything in particular and so given the option they won't. As you observe the C++ committee is gradually moving towards just defining that you get two's complement in the language standard which sidesteps this problem.

However, some of the authors of the C++ work in the area you cited observe that programmers are unduly hopeful about what this means. If your code has an overflow you don't handle correct, you are unlikely to be happier with what happens under twos complement arithmetic than with the practical results (not pregnant male cats, nasal demons or formatted hard disks) of Undefined Behaviour. That is:

If you write Rust and it overflows under debug and panics, and so then you fix your code, that's something a hypothetical alternate C++ doesn't give you.

Only if you write Rust, switch off overflow detection, and it overflows anyway but that works out OK due to two's complement arithmetic, is that something you'd get with the hypothetical alternative C++. This Rust code smells funny, if I had to review it I would flag this and ask why you didn't explicitly say (not in a comment, in the code) what's going on. If you actually *want* a wrapping multiply, in Rust you can explicitly say that's what you wanted, the compiler couldn't care less but a human maintainer does.

Now C++ standards people are smart, and so perhaps in reality C++ 23 gets Wrapping<int> and Saturating<int> for all integer types, as well as a family of standard overflow arithmetic check operators so programmers who *know* they have a potential problem don't have to contort themselves as they would today. *That* would be equivalent to what Rust has already today.

> Just the ability to transmute to the value which doesn't exist in enum may lead to very misterious crashes in completely superficially unrelated code, e.g.

What sort of error are you imagining here? Are you thinking about a C enumerated type? Or Rust's more sophisticated enum? And how is it being transmuted? I mean, sure, unsafe pointer dereference could do absolutely anything, but somehow a lot of C and C++ footguns do not involve any pointers being dereferenced at all so I still think you're underestimating how much difference this makes.

Footguns

Posted Jul 10, 2021 21:17 UTC (Sat) by khim (subscriber, #9252) [Link] (111 responses)

> I mean, sure, unsafe pointer dereference could do absolutely anything, but somehow a lot of C and C++ footguns do not involve any pointers being dereferenced at all so I still think you're underestimating how much difference this makes.

It does a lot of difference. But look on this simple example (without any crazy pointers): somehow Some have become None… but only in debug build. In optimized build it's both Some and None.

> *That* would be equivalent to what Rust has already today.

No, it wouldn't. Rust would never just throw away assert(x + 100 > x); and introduce the security hole. It may trigger an assert message. Or panic. But it wouldn't remove security check. C compiler can and would do that. Huge difference.

> In practice when they have an opportunity to do so, all the platforms you care about (e.g. GCC or LLVM) will leave the definition blank.

Except GCC and LLVM are not “platforms”. They are just compilers. And they are replaceable. Yes, their authors work diligently on making C and C++ unfit for any purpose. But they are not there yet. And while Rust uses LLVM as a base… somehow the very simple to be or not to be is… false gotcha is not reproducible with Rust. At least not as easily as doing the direct conversion.

This being said I wouldn't trust unsafe Rust to produce sane results (e.g. I'm pretty sure that “to be or not to be gotcha” can be reproduced in Rust… just maybe with larger structures or something).

Footguns

Posted Jul 11, 2021 0:58 UTC (Sun) by tialaramex (subscriber, #21167) [Link] (107 responses)

> But look on this simple example (without any crazy pointers)

Fair. However std::mem::transmute is an intrinsic, its implementation isn't Rust - presumably under the hood it just tells the compiler this 4 is actually a Foo not an i8, and thus it isn't pointer shenanigans, but I don't actually know how to check.

The Linux kernel will for the foreseeable future have tonnes of C code in it, which likewise you can call from unsafe Rust and might do anything, but hopefully even the Rust sceptics don't blame Rust for that.

I was only making claims about what the unsafe Rust itself can do, all bets are off once it's not Rust. For one thing there's an intrinsic which just emits the assembly you write, binding Rust variables to input or output registers of your choosing - which obviously might do anything, and again is not Rust and doesn't have Rust's safety guarantees.

Let's take a contrasting example. Suppose I have a function which takes arrays of exactly 4096 bytes. I can't write unsafe { x = array[4100]; }. That's an error, the compiler will explain that this would definitely panic at runtime and so it won't compile. If I add a parameter z and write unsafe { x = array[z] } then the function compiles, but if I call it with z = 4100 the program panics, it does not have Undefined Behaviour, even though I used the unsafe keyword. In fact, if I haven't disabled warn(unused_unsafe) the compiler warns me that my unsafe was useless, this code is safe because it will just panic if bounds are exceeded.

> But it wouldn't remove security check. C compiler can and would do that.

The C compiler removes this check because the scenario where the assert fails is Undefined Behaviour. But in the hypothetical language revision I describe as "equivalent to what Rust has already today" (e.g. a hypothetical C++ 23) this is not Undefined Behaviour, like in Rust you get two's complement arithmetic and so the assert survives the removal of redundant code and fires if you overflow.

This code still smells bad. In Rust I think I would prefer to see something like let result = x.checked_add(100).expect("x + 100 overflowed");

This way I get my result or, I panic with a message explaining what impossible thing happened. If upon reflection it isn't so impossible after all I could write x.checked_add(100).unwrap_or(sane_default); to carry on with sane_default instead when x + 100 overflows.

> Yes, their authors work diligently on making C and C++ unfit for any purpose. But they are not there yet.

They produce programs with good performance characteristics. For a correct C or C++ program this gives you metrics you can work with. The compiler that turned my correct program into a binary which consumed 45MB and took 800 seconds to get the answer is strictly worse than the compiler which turned the correct program into a binary which consumed 40MB and took 680 seconds to get that answer by these metrics.

What can we say about the compiler which turned my incorrect program into a binary which gave one result, versus a different compiler which turned my incorrect program into a binary with a different result? Nothing whatsoever.

They also use their greater understanding of the language to do something you might value more. They write sanitisers. You can often use LLVM with its sanitizers to find out why your program isn't correct. If you fix all the problems this finds, maybe your program will be correct and you get the results you wanted.

Consider something that's pretty "old hat" these days but I still remember as a breakthrough. One day GCC learned how to tell whether your printf() format string is bogus at compile time. In Rust this isn't so hard, the println format string is required to be a literal string, so although it has to be a macro (no variadic functions in Rust) the checking is always viable at compile time. But in C printf takes any arbitrary pointer to some chars that obey the format string rules at runtime, and then any number of auxiliary parameters of any type. What a mess. But they got it done, today newbies expect the compiler to tell them when they try to printf() four integers but only provide two or vice versa.

Finally:

Messing about with unsafe { } to cause yourself problems seems like a poor use of time. Follow Mara Bos https://twitter.com/m_ou_se/status/1411294073969950722 for actual mischief with Rust. That link for example is Mara's ludicrous macro which tricks the Rust compiler into letting you selectively enable nightly features in your otherwise stable code. To make her trick work, Mara has a proc macro which re-runs the compiler after setting a flag so that it will think it's recompiling itself not your code. Rust's compiler is allowed to use new features that aren't in the stable language yet because otherwise things get a bit too meta. Doing this (using Mara's macro or actually just tricking the compiler yourself) invalidates your stability guarantees of course.

Footguns

Posted Jul 11, 2021 1:56 UTC (Sun) by khim (subscriber, #9252) [Link] (106 responses)

> But in the hypothetical language revision I describe as "equivalent to what Rust has already today" (e.g. a hypothetical C++ 23) this is not Undefined Behaviour, like in Rust you get two's complement arithmetic and so the assert survives the removal of redundant code and fires if you overflow.

This was proposed and rejected. Apparently benchmarks are sacred. Thus it's not happening.

Well… it may happen if people would start abandoning C/C++ ship and switch to Rust (and other languages). But if that would be massive enough movement to make C/C++ committees actually care… then I think it wouldn't matter anymore. So don't expect anything like that any time soon.

> They produce programs with good performance characteristics.

They turn working programs into broken ones. Once you pass that threshold “performance characteristics” become secondary concern.

> What can we say about the compiler which turned my incorrect program into a binary which gave one result, versus a different compiler which turned my incorrect program into a binary with a different result? Nothing whatsoever.

What can we say about compiler which produces mathematically impossible result? Function which takes an integer and the returns b || !b should always return true. Any mathematician would say so and even just someone who studied logic a bit would say so.

Yet clang would happily turn it into a function which returns false if you give it some variable which is not initialized.

This goes so far beyond the abilities of someone to reason about the program it's not funny.

In theory it's possible to write program which doesn't trigger any undefined behaviors. In practice I haven't see anyone who have done that with programs of size comparable to Linux kernel.

And since undefined behaviors are infectious in modern compilers you have no idea when and how your program would explode. Huge bodies of C code are land mines just waiting to explode is what LLVM developer wrote, not me.

And they are diligently working on a way to detonate them.

> They also use their greater understanding of the language to do something you might value more. They write sanitisers. You can often use LLVM with its sanitizers to find out why your program isn't correct. If you fix all the problems this finds, maybe your program will be correct and you get the results you wanted.

Let's go to that infamous realloc example.

Can you show me what kind of sanitizer should I use to catch error in it? Or what compilation option? Or… anything at all, really? Can you even explain why compiler was allowed to provide the output it does?

When I pushed really hard on the issue of “how can two pointers be identical yet one is usable while other is not usable” I have got the answer that yes, standard permits that — when one one is a pointer one past the end of one array object and the other is a pointer to the start of a different array object that happens to immediately follow the first array object in the address space… and apparently after realloc pointer which was passed to it magically turns into pointer one past the end…

Seriously? This what I should have expected from function which returns a pointer to the new object (which may have the same value as a pointer to the old object), or a null pointer if the new object could not be allocated?

Why does it even say that it may have the same value as a pointer to the old object if I couldn't use that fact for anything?

These sanitizers are just catching certain common mistakes. They don't even try to catch many cases of undefined behaviors. Which compiler, nonetheless, would ruthlessly exploit.

> One day GCC learned how to tell whether your printf() format string is bogus at compile time.

These were different days and these were different compiler developers. Back then they were not afraid to help developers to write code instead of catering to KPIs. Remember that rant? It may be opinionated but it highlights the story well. 10+ years ago GCC developers cared about real code, and real problems faced by real developers.

But after rise of clang… it all turned into a rat race: gcc and clang looking for a more and more clever ways to punish the developer. Often without even providing any benefits on benchmarks (again: that realloc crazyness definitely doesn't affect any benchmarks). They break the code just because they are allowed to do that.

As simple as that.

> Rust's compiler is allowed to use new features that aren't in the stable language yet because otherwise things get a bit too meta

Nah. It's because otherwise we would have waited for the stable release of Rust for 10 more years.

It's similar crime to the undefined behavior issue of C: once upon time it wasn't the monstrosity which it turned into recently. But over time something which was supposed to highlight areas of possible conforming language extension turned into monster which threatens to destroy everything.

Only time would tell if Rust would, eventually, stabilize enough internal features to make standard-library stable-Rust compatible or if it would follow in the C footsteps.

I, certainly, hope that lessons were learned. But we wouldn't know any time soon.

Footguns

Posted Jul 11, 2021 2:24 UTC (Sun) by pizza (subscriber, #46) [Link] (77 responses)

> They turn working programs into broken ones.

This rant, and many others, seems to ignore the plain fact that *every single* optimization that GCC or Clang performs is *optional*. In other words, don't tell it to try and optimize your code, and it won't.

Why are C compilers so "awful"? Because the *users* of those compilers *demand* that awfulness.

Footguns

Posted Jul 11, 2021 2:52 UTC (Sun) by khim (subscriber, #9252) [Link] (58 responses)

> Why are C compilers so "awful"? Because the *users* of those compilers *demand* that awfulness.

Seriously? Can you show me one used who wanted to see realloc broken? Or wanted to see code which treats null pointer as just a pointer broken? Or bazillion other things which break the code without giving a significant speedup?

As for “all passes are optional”… is that a joke? Yes, sure, all passes are optional. But e.g., the only way to turn pointers back into addresses is to disable all of them. There are no -fno-provenance flag (even if it was actually officially proposed) and there no plans to introduce it.

This is in spite of the fact that zero existing standards mention pointer provenance and no one can actually explain the rules which developer should follow. In reality the rules which are used to deal with it are currently unsound. It's also telling that the only guys who are bothered by the fact that these rules can turn valid programs into invalid ones are… as you have guessed… Rust developers.

Compiler developers? They just happily apply these, as of now, unwritten and untested rules when they compile existing programs. And yes, they routinely break valid (according to the letter of the existing standard) programs. In fact all these half-dozen or so proposals are dedicated to the attempt to add more undefined behaviors to the standard to turn valid programs into invalid ones and thus, retroactively, justify what compilers are doing today.

Do you really want to say that this is what users wanted? Are they even aware that's what they wanted?

Footguns

Posted Jul 11, 2021 6:01 UTC (Sun) by wtarreau (subscriber, #51152) [Link] (57 responses)

> Compiler developers? They just happily apply these, as of now, unwritten and untested rules when they compile existing programs. And yes, they routinely break valid (according to the letter of the existing standard) programs. In fact all these half-dozen or so proposals are dedicated to the attempt to add more undefined behaviors to the standard to turn valid programs into invalid ones and thus, retroactively, justify what compilers are doing today.

I totally agree with this and it's why I'm extremely angry at gcc's developers for destroying what used to be the most portable compiler. I suspect they're sponsored by the same who sponsor rust because over the last 5 years or so, their only contribution has been to carefully break programs that used to work flawlessly for 20 years on the very same compiler. Worse, they break boundary tests that were purposely placed in programs to avoid security issues, just because one day they decide that it's useful to drop an overflow test that is based on an undefined behavior, without ever being able to emit the slightest warning about it! We're seeing an increase of compiler-induced bugs that are sometimes security issues, and given that new optimizations come by default and the compiler continues to complain about unknown -fno-foobar options, you cannot easily disable these in a portable way in your programs without breaking older compilers that were still sane.

The last trustable gcc version I've used was 4.7. After that it progressively became utter crap introducing random breakages in your back. It coincides with the rewrite in C++, maybe it indicates a lack of interest for the practical C language by their developers, though I don't know.

Ah and please don't speak about optimizations. Code size has since significantly increased, is often slower (likely due to L1 cache footprint and/or extreme vectorization/unrolling for short iterations), and the build time has increased 2-3 fold.

Footguns

Posted Jul 11, 2021 9:16 UTC (Sun) by khim (subscriber, #9252) [Link] (55 responses)

Do you really hope anyone would treat your rants seriously when you don't do even a simple fact-checking?

> The last trustable gcc version I've used was 4.7.

How is it “trustable” when it happily removes your beloved security checks? Heck, even GCC 4.1 (oldest available on godbolt) does that. And if you would look on the appropriate bug you'll know GCC 2.95 did that as well!

And it was released last century when Rust wasn't even a thing! Graydon Hoare may have played with some ideas which eventually lead to creation of Rust back then, but it wasn't even presented to Mozilla employees, let alone general public.

In fact in the beginning Rust wasn't even about replacement of C or C++ (it still isn't… some Rust fanboys are trying to portray Rust as “modern C/C++ replacement” but it's developers are not aiming that high).

It was about moving low-level programming field from 70th to 90th (yes, Rust includes zero ideas from XXI century, all things which you see in it were “boring and old” when it was conceived…). Some of these ideas were quite “modern and unproven” at the end of XX century but today they are “old and boring”.

> It coincides with the rewrite in C++, maybe it indicates a lack of interest for the practical C language by their developers, though I don't know.

If you “don't know” then why do you write all these rants which one step away from “reptiolds are using Rust to destroy human civilization!” ?

Yes, compilers have become more and more clever at exploiting what they consider undefined behavior. But this “fall from grace” was very slow and gradual. And it wasn't caused by a switch to C++ or creation of clang, let alone Rust.

To claim that all that was done by some hidden beneficiary which did all that to, somehow, push modern language into system developer's community and bring them, kicking and screaming, to XXI century… that's new low, I think.

Sure, at first the breakage could be contained with the use of some simple flags… but as time marched on… it became harder and harder to do.

And these flags were introduced as a concession to developers, not as something compiler developer promised to always deliver.

The new thing that happened recently is situation where complexity of compiler optimizations have grown enough that not only offering such flags have become more-or-less impossible, but where they couldn't even compile correct programs reliably — and instead of thinking about how they can change the compiler to bring it back to standards-compliance they started thinking about way to move the goalposts in the standard text!

That is when C and C++ have become “languages unfit for any purpose” and Rust have started gaining popularity.

> Code size has since significantly increased, is often slower (likely due to L1 cache footprint and/or extreme vectorization/unrolling for short iterations), and the build time has increased 2-3 fold.

Yet execution time for benchmarks decreased which was the goal. The fact that kernel community was burned in the process is just a side effect.

Footguns

Posted Jul 11, 2021 11:40 UTC (Sun) by wtarreau (subscriber, #51152) [Link] (54 responses)

> > The last trustable gcc version I've used was 4.7.
> How is it “trustable” when it happily removes your beloved security checks? Heck, even GCC 4.1 (oldest available on godbolt) does that. And if you would look on the appropriate bug you'll know GCC 2.95 did that as well!

Indeed it does with constants! With variables (which used to be my main use case) it resisted much longer, and 7 still emits the test, which was dropped in 8: (x+y < x) with both x and y ints simply checks for the sign of y now.

Anyway what I said still translates my experience. From 2.5.8 to 4.7, my concerns were mostly varying optimization quality (e.g. -Os giving significantly larger executables past 3.4), some jokes about structures initialization, and variable array declaration, changes to asm input/output/clobbers, out-of-block asm being reordered, but most of these resulted in build-time errors that were annoying but addressable. I started to spot runtime breakage once in 4.8, a few times in 5, then 6 broke quite some builds, then runtime breakage again in 8, 10, and 11 with each time a good explanation such as "your 25-years old expression needed 12 extra casts to be fully spec-compliant, even if it works fine like this on any other compiler".

In my opinion all this doesn't deserve yet-another-flag, it instead deserves a dialect to be used like "-std=safe11" for example, which would turn UB and IDB to well-defined behavior, even if it makes SPECint look slightly less advantageous. Defining valid pointer arithmetics, not breaking the absurd realloc() case nor the "b||!b" case, using signed chars by default etc would respond to the principle of least surprise for the vast majority of developers, and actually get back to when C was and easy and portable assembly language.

Footguns

Posted Jul 11, 2021 13:32 UTC (Sun) by khim (subscriber, #9252) [Link] (53 responses)

> In my opinion all this doesn't deserve yet-another-flag, it instead deserves a dialect to be used like "-std=safe11" for example, which would turn UB and IDB to well-defined behavior, even if it makes SPECint look slightly less advantageous.

This wouldn't “make SPECint look slightly less advantageous”, this would outlaw any and all optimizations.

Let's take a look on a concrete example:

#include <stdio.h>

void foo(int* i) {
  i[1] = 42;
}

int bar() {
  int i = 3, j = 3, k = 3;
  foo(&i);
  return j + k;
}

int main() {
  printf("%d\n", bar());
}

What kind of compiler can you imagine that would optimize anything while leaving such example (and other similar examples) intact?

All optimizations in C rely on absence of UB. All. No exceptions.

You cound't even turn 2 + 2 into 4 without breaking something if you would allow any and all UB (don't show such an example because GCC optimizes that even with -O0!… so much about “just turn off optimizations off if you don't like them” BTW).

To permit all UBs would mean to go back to “C from 70th” where only variables declared as register can be put in registers. Result would be not “make SPECint look slightly less advantageous”, but something that no one (not even you, I suspect) would want to use.

That's just how C and C++ are made.

> Defining valid pointer arithmetics, not breaking the absurd realloc() case nor the "b||!b" case, using signed chars by default etc would respond to the principle of least surprise for the vast majority of developers, and actually get back to when C was and easy and portable assembly language.

Before you can introduce such switch (in a meaningful manner, not in “stop turning 2 + 2 into 4” manner) you have to do insane amount of work. Classify all these undefined behaviors, clarify what compiler is allowed and not allowed to do about them and so on.

You need to, basically, invent completely new language.

And if you are inventing completely new language then why not add some markups there which would make compiler job easier? And why not make things which existing C and C++ compiler try to glean from the shaky “developer is smart and wouldn't write code which triggers UB” rule explicit if you are inventing new language anyway?

Someone may end up creating another Rust-alike but there are literally zero chances of ever getting that -std=safe11 switch. Not gonna happen. Deal with it.

Footguns

Posted Jul 11, 2021 14:47 UTC (Sun) by smurf (subscriber, #17840) [Link] (52 responses)

> This wouldn't “make SPECint look slightly less advantageous”, this would outlaw any and all optimizations. […]
> All optimizations in C rely on absence of UB. All. No exceptions.

Huh? There are plenty of optimizations that work perfectly well precisely *because* they do *not* rely on any UB. In fact, the desire to do whatever real hardware would do with the C code even in the presence of UBs (like aliased pointers that can modify literally anything, including CPU registers in some architectures) is a main blocker of optimizations. Or rather, it was a blocker – before the C people decided to grab that desire by the neck and throw it under a bus, regardless of the collateral damage.

The example program you cite is a case in point. It's buggy trash; calling it "intact" is an insult to any and all less-broken programs. Please use an example that at least has some chance of doing what you think it should be doing.

> you have to do insane amount of work.

Yeah, so? It's been done, most recently by writing a Rust compiler instead, you can do it again. That's not the problem.

> You need to, basically, invent completely new language.

Exactly. But the reason for that isn't that UB-free C code can't be optimized. The reason is that a C compiler can't know whether any piece of code is in fact UB-free. The language simply doesn't have the features required to convey the necessary information.

Footguns

Posted Jul 11, 2021 15:17 UTC (Sun) by khim (subscriber, #9252) [Link] (51 responses)

> Please use an example that at least has some chance of doing what you think it should be doing.

Hmm. How about something like this:

#include <stdio.h>

int main() {
  const int this_is_zero = 0;
  *(int*)(&this_is_zero) = 42;
  printf("Who said this is zero: %d\n", this_is_zero);
  return 0;
}

Better?

> It's buggy trash; calling it "intact" is an insult to any and all less-broken programs.

Yet without proper guadance about what kind of “trash” you have to accept and execute correctly and what kind of “trash” you are allowed to turn into pile of goo you have to handle it. Somehow.

Both program are 100% syntactically correct, both work quite reliably on many platforms and many compilers (with optimizations disabled). Why do you can them “trash”? Just because you dislike them? Well, compiler developers dislike programs which rely on integer overflow, so what's the difference?

> It's been done, most recently by writing a Rust compiler instead.

Except Rust compiler wouldn't even accept such program thus there are no need to think about what would it do when executed. But yeah, it can be done, sure.

> you can do it again.

True. And then you would have another Rust-like language. Because if we are starting splitting the hairs and starting to put some valid programs into “this is trash, we don't plan to support that” bucket then why not change the rules and turn them just a compile-time errors instead?

When C was developed… then answer was obvious: “computers are slow and limited, they don't have resources to deal with that”. But what excuse would you have to do that today?

> But the reason for that isn't that UB-free C code can't be optimized.

Of course not! UB-free C can be optimized! It's C code which triggers some UB where sh$t hits the fan. Please read what you argue against, please! Optimizations rely not on UB but on the absence of UB!

> The language simply doesn't have the features required to convey the necessary information.

Precisely and exactly my point. In practice anyone who tries to invent that hypothetical -std=safe11 language end up adding such features to C and the end result becomes C# or Go, or Rust.

What you don't get out of these attempts are -std=safe11. Precisely and exactly because it's “too little gain for too much effort”.

Footguns

Posted Jul 15, 2021 13:11 UTC (Thu) by HelloWorld (guest, #56129) [Link] (50 responses)

> Both program are 100% syntactically correct, both work quite reliably on many platforms and many compilers (with optimizations disabled). Why do you can them “trash”?
Because you're writing to a memory location that you previously declared to be const. Why would you expect sane results when you do things that are so obviously not sane? And in fact this code requires a cast, which is a pretty clear signal by the compiler that you're doing something fishy.

Footguns

Posted Jul 15, 2021 14:37 UTC (Thu) by khim (subscriber, #9252) [Link] (27 responses)

> Because you're writing to a memory location that you previously declared to be const.

But I “know” it's on stack and stack is not const!

> Why would you expect sane results when you do things that are so obviously not sane?

Because to me they look “sane”. The exact same way I “know” that if you overflow int it becomes negative I know also that there are no const objects on stack (at least on most OSes).

Why one thing would be supported, while other wouldn't be?

> And in fact this code requires a cast, which is a pretty clear signal by the compiler that you're doing something fishy.

Well… in C you couldn't eve call qsort without casts, but if you want to try to optimize something without casts… here we go:

#include <stdio.h>

int foo() {
  int i;
  i = 42;
}

int bar() {
  int i;
  return i;
}

int main() {
  foo();
  printf("%d\n", bar());
}

As you wanted: no casts, no const. Am I safe now?

And yes, I know, there are UB, too. But that's exactly the point: you can't optimize anything in C unless you would declare some subset of syntactically correct C “improper and not allowed”. And that is where all attempts to imagine -std=safe11 fail.

The problem is: there are no C or C++ “community”. Just lots of people who don't really talk to each other. Even here: you don't even try to understand the point which these program illustrating. Instead you are attacking me as if I'm proposing to write code like that. Thus, as I have said: there would be no -std=safe11. Ever. Deal with it.

Footguns

Posted Jul 15, 2021 16:38 UTC (Thu) by HelloWorld (guest, #56129) [Link] (1 responses)

> But I “know” it's on stack and stack is not const!

const isn't only there to set the relevant pages read-only, it also allows things like constant folding. So I think it's reasonable to make it UB when you try to write to a location that is marked const, whether or not it is static.

> Because to me they look “sane”.
Well, your example doesn't look sane to me 🤷🏻‍♂️

Footguns

Posted Jul 15, 2021 16:48 UTC (Thu) by khim (subscriber, #9252) [Link]

> Well, your example doesn't look sane to me 🤷🏻‍♂️

And that's how we end up with bazillion incompatible “friendly C” proposals which would never be implemented.

Because none of them would even reach “written spec” stage… and you need it before an actual implementation stage.

Footguns

Posted Jul 16, 2021 9:21 UTC (Fri) by anselm (subscriber, #2796) [Link] (24 responses)

As you wanted: no casts, no const. Am I safe now?

That example may have no casts or const but it is still really crummy C code. I'm not sure exactly what this is supposed to illustrate, let alone prove.

Footguns

Posted Jul 16, 2021 12:39 UTC (Fri) by khim (subscriber, #9252) [Link] (23 responses)

> That example may have no casts or const but it is still really crummy C code.

But it's syntactically valid and works on most C compilers with optimizations disabled, isn't it?

> I'm not sure exactly what this is supposed to illustrate, let alone prove.

The point was: that hypothetical proposed -std=safe11 or -std=friendly-c C dialect is impossible to create without creating another full-blown language spec.

You couldn't just say anything syntactically valid and working in -O0 mode should work. Because that would mean you have to, somehow, correctly compile also an atrocities I have shown.

And if you start creating spec… and trying to keep it sane… you end up, eventually with a spec for D, C# or Rust… not with a spec for a “friendly C”

Footguns

Posted Jul 16, 2021 16:12 UTC (Fri) by HelloWorld (guest, #56129) [Link] (6 responses)

> But it's syntactically valid and works on most C compilers with optimizations disabled, isn't it?
I don't think it does. What if a signal or interrupt is delivered after the call to foo()? It would invoke a signal handler which is probably going to clobber the memory where foo's stack frame used to be.

> The point was: that hypothetical proposed -std=safe11 or -std=friendly-c C dialect is impossible to create without creating another full-blown language spec.
That may or may not be true, but either way, your example doesn't demonstrate that. Every compiler these days has warnings to prevent that sort of thing.

Oh, and coming back to your previous example:
> const int this_is_zero = 0;
> But I “know” it's on stack and stack is not const!
Why would the compiler allocate this on the stack? It's not using any local variables or function parameters, so it might as well allocate it statically on a read-only page.

Footguns

Posted Jul 16, 2021 18:08 UTC (Fri) by khim (subscriber, #9252) [Link] (5 responses)

> What if a signal or interrupt is delivered after the call to foo()?

There's sigaltstack for that. Or, if that's MS-DOS, cli and sti.

> Every compiler these days has warnings to prevent that sort of thing.

Well… the fact that it's a warning says that it's dangerous, but supported thing, isn't it? And I can change the code to make sure there are no warnings:

void foo(int* i) {
  i[1] = 42;
}

int bar() {
  int i = 0, j = 0;
  foo(&i);
  return j;
}

int main() {
  printf("%d\n", bar());
}

Better now?

> Why would the compiler allocate this on the stack?

You mean: without explicit auto? I think even C89 had implicit auto. I can add explicit auto, if you want. Compiler still misbehaves.

The really funny thing: just a tiny modification of that code makes it compileable with -O2, too.

Thus modern compilers don't really propagate all “undefined behaviors” blindly. No, they are more-or-less testing C and C++ developers patience.

Footguns

Posted Jul 16, 2021 23:42 UTC (Fri) by HelloWorld (guest, #56129) [Link] (4 responses)

There's sigaltstack for that. Or, if that's MS-DOS, cli and sti.
Interesting, I didn't know about that. Anyway, it's a platform-dependent mechanism which isn't guaranteed to be available, and even on Linux it's only used when SA_ONSTACK is used. So I still believe it's reasonable to make that undefined.
Well… the fact that it's a warning says that it's dangerous, but supported thing, isn't it?
That's up to you. If you want, make it -Werror=uninitialized instead.
Better now?
I don't consider it reasonable to make assumptions about how the compiler lays out the memory for local variables. That said, I'll grant you that reasonable people can differ about something like this:
void foo(int *i, int o) {
        i[o] = 42;
}
int bar() {
        int i = 0, j = 0;
        foo(&i, &j - &i);
        return j;
}
int main() {
        printf("%d\n", bar());
}
But I'm not enough of a compiler expert to be able to assess what the performance impact would be if this were allowed. I suspect it would be severe.

Footguns

Posted Jul 17, 2021 0:11 UTC (Sat) by khim (subscriber, #9252) [Link] (3 responses)

> Anyway, it's a platform-dependent mechanism which isn't guaranteed to be available

Seriously? POSIX guarantees it's availability, Windows doesn't need (it doesn't support signals), MS-DOS & embedded are handled with cli/sli (and analogues)… so what kind of “important platform” is in the problem?

> So I still believe it's reasonable to make that undefined.

Maybe, but that's the thing: it works on almost everything. Thus simple rule “things are disallowed if they couldn't ever work” doesn't cover it.

> That's up to you. If you want, make it -Werror=uninitialized instead.

Maybe, but safer option would be to switch to something that doesn't change it's rules every year.

> But I'm not enough of a compiler expert to be able to assess what the performance impact would be if this were allowed. I suspect it would be severe.

Most likely. This would break almost all optimizations based on these “noalias” rules which power a lot of optimizations.

But the story here is: what's the point of something being speedy if said something is no longer correct?

If there were some kind of dialogue between C/C++ compiler developers and users of said compilers then maybe they would have come to an agreement. But there are none.

Footguns

Posted Jul 17, 2021 13:26 UTC (Sat) by HelloWorld (guest, #56129) [Link] (2 responses)

> Seriously? POSIX guarantees it's availability, Windows doesn't need (it doesn't support signals), MS-DOS & embedded are handled with cli/sli (and analogues)… so what kind of “important platform” is in the problem?
As I already mentioned, sigaltstack is only used if you specify SA_ONSTACK. Or that is how I understand the documentation, anyway.

> Maybe, but safer option would be to switch to something that doesn't change it's rules every year.
Sure, most software shouldn't be written in C or C++.

> But the story here is: what's the point of something being speedy if said something is no longer correct?
Well, nobody said writing high performance software was going to be easy. But there are legitimate use cases where you really do want to squeeze every last cycle out of the hardware.

Footguns

Posted Jul 17, 2021 13:49 UTC (Sat) by khim (subscriber, #9252) [Link] (1 responses)

> As I already mentioned, sigaltstack is only used if you specify SA_ONSTACK.

Please read the documentation again. You can not ever set SA_ONSTACK. It's not something you set, but something you can read: sigaltstack couldn't change state of altstack if you are in the middle of a signal handler and said altstack is currently in use!

> But there are legitimate use cases where you really do want to squeeze every last cycle out of the hardware.

But C and C++ are making it harder and harder each year! Because time which is spent making pointless changes which would keep tested and debugging programs working could have been spent on the changes which would make these programs faster instead.

And in wast majority of cases you can get more speedup from doing that. Where new version of compiler can buy you 3% or maybe 5% (if you are lucky) manual tuning can often bring 2x, 3x or even 10x improvements.

And the more artificial constructs you add to your program the slower it becomes.

No wonder that developers of embedded systems where efficiency would be obviously desired typically stick to one version of the compiler (which is not upgraded ever) or, sometimes, even use -O0 to free time for manual optimizations.

Footguns

Posted Jul 17, 2021 14:51 UTC (Sat) by excors (subscriber, #95769) [Link]

> No wonder that developers of embedded systems where efficiency would be obviously desired typically stick to one version of the compiler (which is not upgraded ever) or, sometimes, even use -O0 to free time for manual optimizations.

Based on a majority of the embedded code I've seen, the actual reason is that most embedded software developers are bad software developers. You're lucky if they'll write code that isn't full of obvious buffer overflows, race conditions, etc, and there's almost no chance they'll appreciate concepts like aliasing or memory barriers. That's why it's no surprise their 'tested and debugged' code breaks when they turn on new optimisations - it was only working in the first place by luck and by the very limited extent of their testing. And even when projects are run by competent software developers, they'll probably have to import some third-party libraries (chip SDKs, drivers, etc) which are badly written and/or are precompiled binaries from an ancient compiler version, so they face the same problems.

(To be fair these embedded software developers may have a much better understanding of hardware than the average software developer, which is crucial for doing their job; but they're still bad at software.)

Footguns

Posted Jul 17, 2021 21:08 UTC (Sat) by anselm (subscriber, #2796) [Link] (15 responses)

But it's syntactically valid and works on most C compilers with optimizations disabled, isn't it?

What do you mean “works”? Prints “42”? Only by happenstance. That's not something I would rely on, ever. As I said, it's really crummy C code, and it doesn't prove anything.

Footguns

Posted Jul 17, 2021 21:59 UTC (Sat) by HelloWorld (guest, #56129) [Link] (14 responses)

The original SimCity game had a use-after-free bug, but it happened to work fine on DOS. It broke on Windows, so Microsoft added code to detect if SimCity is running. If that was found to be the case, it would run the allocator in a special mode that allowed use-after-free.

Given some of khim's other comments, this is the kind of thing he seems to expect platform developers to do when dealing with broken code. Except of course he's going to say that said code is not broken because, after all, it has been working for decades, and the problem is clearly you and your toxic attitude.

Well, I have a different opinion. If your code is broken, LLVM (or GCC) developers are under no obligation to accommodate that. The fact that they still do in some ways (with options like -fwrapv or -fno-strict-aliasing) is them being nice.

Footguns

Posted Jul 17, 2021 23:44 UTC (Sat) by Vipketsh (guest, #134480) [Link] (12 responses)

> is them being nice.

I have to wonder .. do you consider the Linux policy of not breaking user space, even buggy ones, them being overly nice ?

Clearly no one would accept Linux changing the semantics of read() every release and substantiate that with "we never agreed to be posix compliant". This is what user's of compilers feel is happening to them (please read below). It's easy to brush off complaints when you are screwing over others, but it's seriously not great when others are screwing you.

I agree that the examples given are not spectacular in that for most of them I don't think one can expect any explicit well defined result. However, I think that there are still reasonable expectations for things the compiler will not do (e.g. start operating on non-rings).

Reality and de-facto standards are also important. For decades compilers compiled code with the semantics of wrapping 2's complement signed arithmetic thereby creating an implicit agreement that that's how signed integers work. Compiler writers then unilaterally broke the agreement without much warning.

In some ways I can understand that only a few people will ever fully comprehend all the fine print in a standard and sometimes user's have the wrong idea and things break. The issue here is that these breakages occur all too frequently and when they do the argument isn't an explanation about how the new behaviour makes sense or issues about what the old behaviour had it's just bluntly "some paper with black splodges says we can".

The situation isn't helped by the fact that many of these surprising issues occur because the compiler is explicitly special casing something (looking for realloc(), null pointer checks, etc.). Clearly someone put in the effort to look for these things and then make deductions based on them. Presumably there is a reason why all this effort was made but that reason never seems to be given. There is some understanding that the reason is probably some performance improvement in which case I think it is reasonable to expect compiler authors to be able to point to a commit log or discussion where the numbers were passed around, but it never happens.

In the end what users believe is that the compiler is miss-compiling their valid code and after some long & painstaking debugging the other party just tells them to get lost. You see why people get upset ?

Footguns

Posted Jul 18, 2021 1:25 UTC (Sun) by HelloWorld (guest, #56129) [Link] (11 responses)

> However, I think that there are still reasonable expectations for things the compiler will not do (e.g. start operating on non-rings).
Let's say you do an out-of-bounds write somewhere in your program. There's a chance that it might overwrite the piece of machine code that was generated from the "return be || !be" statement, and it might replace it with "return 0" (I'm aware of W^X etc, but not all platforms support that). There's no way the compiler can be expected to guard against that. I think a reasonable debate can be had about what exactly should trigger undefined behaviour, and it seems to me that compiler authors agree, as evidenced by the fact that they offer flags like -fwrapv. But I don't think it's reasonable to expect compilers to make any guarantees about what happens after UB has been invoked, not even for the be || !be example.

> Reality and de-facto standards are also important. For decades compilers compiled code with the semantics of wrapping 2's complement signed arithmetic thereby creating an implicit agreement that that's how signed integers work.
Again, SimCity was working fine for many years, despite having use-after-free bugs. Does that mean there's an "implicit agreement" that that's how free should “work”?
I think the standard should be that a behaviour should be preserved (at least optionally) if it has been around for a long time and a reasonable person might have specified it in some form of documentation. Reasonable people might have written a C standard where two's complement representation is specified for signed integer types. But no reasonable person would write a specification for free that says you can still use the data you just freed, because that just doesn't make sense. Now of course people have different ideas about what reasonable people would or wouldn't do, but my point is that “this has been working for years, so it must continue to work” just isn't sufficient by itself.

Footguns

Posted Jul 18, 2021 8:14 UTC (Sun) by Vipketsh (guest, #134480) [Link] (9 responses)

> There's a chance that it might overwrite ...

So, I think this is a key point to understand about C. In C you can and want to have pointers going to any arbitrary piece of memory (e.g. for I/O access) and as you say no standard can hope to define what happens when some data structure, or in more extreme cases, code is arbitrarily overwritten. The reasonable thing to do when analysing C code is to assume such things don't happen, and not to go out of your way to find such cases and generate garbage.

> There's no way the compiler can be expected to guard against that.

Sure, it's unreasonable to expect the compiler to guarantee that your pointers don't overwrite something you didn't expect. It's a completely different matter when the compiler goes out of its way to look for these things and then generating complete garbage, not because it's sane for some surprising reason, but because "it's allowed".

Let's also not forget that in the original example of (be || !be)=false there was no random overwriting of anything. It was reading an un-initialised variable, which is a little easier to reason about -- there is no explicit value one can expect when reading un-initialised variables, at the same time it is also reasonable to expect that the value that one gets does not fall outside of the set of allowed values for that type.

There is also another piece of to this puzzle: only clang assigns values outside the set of allowed values for the type, gcc assigns 0. Yet gcc's performance on the all-important SPEC benchmarks is not lagging miles behind clang so it's clearly not an all important optimisation. So the yet-unanswered question is raised of what advantage does clang's behviour have ? The disadvantages are clear.

> there's an "implicit agreement" that that's how free should “work”?

In some ways, yes, I do think there is implicit agreement here. I also think that user-after-free is not a good example because just like defining what overwriting arbitrary pieces of memory will result in is impossible, so is use-after-free. Still, I think the compiler should not go out of its way to detect use-after-free type situations and then generate garbage in responce.

A different but similar example perhaps would be the case of "The sims", which was known to write past allocated memory areas. It happened to work on Windows 9x. In later versions of windows they changed the allocator and put some internal data structures in those locations resulting in the game not working anymore. In the end Microsoft changed the allocator behaviour such that if you asked for X bytes you got X+Y bytes. Not logical behaviour when looking an allocator but well defined. For me, such changes are a reasonable price to pay for compatibility.

Footguns

Posted Jul 18, 2021 11:34 UTC (Sun) by jem (subscriber, #24231) [Link] (8 responses)

> The disadvantages are clear.

Out of curiousity, what are these clear disadvantages? If be is undefined, then (be || !be) is also undefined. This is "garbage in, garbage out" at work here. You can't create information out of nothing. It's madness to start reasoning about this before fixing the underlying problem first, i.e. make sure the variable has a value before using it.

On the contrary, I would say the compiler did you a favour by giving a hint in the form of an unexpected result. A hint telling you that there is something wrong with the code. Of course it would have been more helpful if the compiler had printed the real error, that the variable is uninitialised. Which is what compilers usually do nowadays, when they are able to detect the it.

Footguns

Posted Jul 18, 2021 12:43 UTC (Sun) by Vipketsh (guest, #134480) [Link] (5 responses)

> what are these clear disadvantages?

It breaks the foundations of the arithmetic all users expect.

My experience with Verilog, that has a number system that is not a ring opposed to (almost) all software, tells me that the number one thing people expect is ring-like behaviour, more so than operating on an infinite set. You can argue that all people are mentally completely deficient when they expect that if they have INT_MAX apples, acquire an additional two and finally eat two of them there are going to be INT_MAX left instead of "poison" number of apples, but perhaps it would be easier to just apply the models that work in the real world and have done so for more than a century ?

> You can't create information out of nothing.

Pretty much the entirety of mathematics is about "creating information out of nothing" and that's the beauty of it. Guess what ? In the given case, the maths says that (be || !be) is true. Not undefined or "poison", but true. In all cases. No exceptions.

You could argue that this isn't a normal system but "LLVM special". It's a valid argument, but you can't have it both ways and keep sane results like LLVM tries in vain: it uses a non-ring but then applies transformations that are valid only on a ring.

> On the contrary, I would say the compiler did you a favour by giving a hint in the form of an unexpected result.

What an overly kind compiler that is. What's next ? I should be donating them a kidney because they were oh-so-kind to insert an arbitrary write in my code, corrupting memory as a way of "warning me of an unexpected result" ?

It's incredibly difficult to figure out what happened when the compiler is doing everything right, and your code is clearly at fault (think: memory corruption). Suggesting that the compiler introducing random behaviour "is helping the user" is preposterous.

Footguns

Posted Jul 18, 2021 13:24 UTC (Sun) by mathstuf (subscriber, #69389) [Link] (4 responses)

I'm OK with the ring argument to an extent, but I don't think one can actually apply it to the C Abstract Machine. My main question is: how far do we go down this road? I understand that `be || !be` is "trivial", but is `i < 2 || is_prime(i) || is_composite(i)` supposed to be something the compiler can reason about? If not, why not? After all, every value in the domain for integer types in C is fine here. To go even further, one could have `i < 2 || is_odd(i) || goldbach_conjecture_holds(i)`. While not known for *all* numbers, this is known for up to 2³²-1 at least (64-bit unsigned seems to be a *bit* past the current proof available; 4e18 versus 1.8e19). Where do we draw the line for such "if we treat the types as holding to the rules of a ring, the value is always N, so replace it with such"?

Of course, this is assuming that `be` and `i` do not change from access to access. However, this is exactly the kind of assumption that is allowed for uninitialized variables. Sure, machines today might have all values in the normal range be the only values supported and they won't change willy-nilly, but the Mill has a NAR result; this could certainly be the representation for uninitialized variables in which case C would either need to inject an initialization for you (what rules exist for this?) or do some complicated runtime NAR tracking to handle the cases given above (because the operations will generally result in NAR propagation).

I think people need to remember that "the hardware" is *not* what C targets. Hasn't been for a long time either. The rules of the C Abstract Machine are not beholden to "what currently prevailing hardware does" and nor should it unless it wants to do one of:

- make uninitialized variables a compile error (like Rust does) and require source code changes to continue using newer compilers;
- pessimize future potentials (such as having to inject initialization into every POD variable on the Mill); or
- declare that hardware today has all of the properties we can expect and enshrine them.

Of course, C (and C++) have painted themselves into a corner by trying to say:

- old code will continue to compile (though not necessarily with the same behaviors :/ ) (this has been broken somewhat by the removal of trigraphs and maybe removing some EBCDIC allowances);
- we don't want to enshrine existing architectures and require future hardware to emulate any decisions made here (e.g., twos complement (yes, C++ has done this, but I don't know the status for C), IEEE floating point formats, etc.);
- ABI compatibility; and
- as close to "zero cost abstractions as possible".

Rust, on the other hand, says "we're willing to give up some future opportunities and supporting older platforms at the expense of better available reasoning about the code at compilation time on the hardware that is widely used today". Sure, if some new floating point format appears, Rust's IEEE expectations are going to have to be emulated where that gets used preferentially. But it means that Rust has chosen "working on extant hardware today as well as possible" over "works on ancient stuff, current stuff, and whatever may exist in the future". Given how much extant language design ends up influencing marketable hardware, I think the former is more useful. But who knows, maybe security will become a big enough problem that capability-based processors will be a thing and Rust will then be in a corner and C able to morph over into defining its behavior on such a machine. It's not a bet I would take, but it's also a possibility.

Footguns

Posted Jul 18, 2021 14:17 UTC (Sun) by khim (subscriber, #9252) [Link]

> Sure, machines today might have all values in the normal range be the only values supported

Period. Full stop. End of discussion. If certain behavior is dictated by hardware then compiler shouldn't invent it's own rules which permit it to optimize destroy programs.

Sure, it may invent some additional rules (e.g. Rust may use the fact that certain types don't use all bit patters to save some memory) — but then it becomes responsibility of the compiler to maintain these additional rules and it must abort an attempt of the user to violate such rules.

It's idiotic to just assume that user is simultaneously super-advanced and knowledgeable and remembers all these hundreds of UBs defined in standard (and not defined by standard too, as we now know) yet, simultaneously, is dumb enough to write code which would do something wrong — unconditionally.

> I think people need to remember that "the hardware" is *not* what C targets.

Why not? The only reason compilers exist and are used is to take programs and execute them on hardware. Why make that task unnecessarily complicated?

> But who knows, maybe security will become a big enough problem that capability-based processors will be a thing and Rust will then be in a corner and C able to morph over into defining its behavior on such a machine.

Dream on. Just don't forget to do a reality check when you would wake up.

> It's not a bet I would take, but it's also a possibility.

No. There are no such possibility. People are not writing code for “abstract C machine”. They are writing code for the existing hardware and then fight the compiler till it works.

I have talked with a guy who participated in the development of compiler for E2k CPU (which does have capability-based processor… (or, rather, it had in the initial design, not sure if they kept it).

Approximately zero non-trivial C programs can be compiled in strict mode. Because in any non-trial program sooner or later you hit a code which assumes that pointer is just a number. Maybe a weird number (like far pointer in Windows 16bit) but still a number.

Similarly you hit code which assumes than numbers are two's complement and so on. Hyrum's Law ensures that you couldn't ever transfer non-trivial C or C++ codebase to a radically new architecture (one of the reasons why all CPUs today are so strikingly similar, BTW).

Safe Rust rules, on the other hand, can happily coexist with capability-driven CPU. And given the fact that Rust developers try to minimize use of unsafe Rust (and it's always clearly marked when used) port of Rust code to the capability-driven CPU is quite feasible.

If we ever would switch to capabilities-driven CPUs then C and C++ wouldn't survive the transition for sure. While Rust might.

> To go even further, one could have i < 2 || is_odd(i) || goldbach_conjecture_holds(i)

What's the issue with that code? Pick any single value and calculate the answer, if you can.

> C would either need to inject an initialization for you (what rules exist for this?) or do some complicated runtime NAR tracking to handle the cases given above (because the operations will generally result in NAR propagation).

As long as mental model “uninitialized variable contains whatever garbage which was found there when memory was allocated” holds… people would accept it. Sure, there are programs which would be broken with these optimizations, but it's very hard to find someone who thinks they should work.

Using standard as a guide which includes current state of affairs is fine, too. But when following the standard gives surprising (to the user) result then changes to the standards should be contemplated, too. I remind you, once more, what C committee said explicitly:

Undefined behavior gives the implementor license not to catch certain program errors that are difficult to diagnose. It also identifies areas of possible conforming language extension: the implementor may augment the language by providing a definition of the officially undefined behavior (emphasis mine).

WTH this recommendation (which would have been widely welcomed by C and C++ developers) was followed so rarely and contradicted so often?

Footguns

Posted Jul 18, 2021 16:49 UTC (Sun) by Vipketsh (guest, #134480) [Link] (2 responses)

Is your argument here that it is unreasonable to expect the compiler to be able to be able to prove properties of expressions and perform optimisations based on those properties ? If that is indeed your argument I agree with you. It's also the reason why in the "p = realloc(q, ..); if (q == p) ..." example I think it is wrong to argue "but, I checked the pointers are equal" because there are a lot of expressions which can be true if and only if p and q are equal, but it is not reasonable to expect the compiler to see that in all cases. In that case I think the aliasing rules are just messed up.

The reason I think the (be || !be)=false result is unreasonable is not because LLVM is unable to prove that it should evaluate to true. Instead, my problem is because LLVM goes out of its way to use a non-ring to evaluate it and I have no idea what the advantage is in doing that. If it just didn't try to do anything special, perhaps even just emitted the expression as-is in machine instructions, it would arrive at the expected result.

Footguns

Posted Jul 18, 2021 17:14 UTC (Sun) by khim (subscriber, #9252) [Link]

> Instead, my problem is because LLVM goes out of its way to use a non-ring to evaluate it and I have no idea what the advantage is in doing that.

“Dead code elimination”. The idea the same with most “optimizations” which break formerly valid code: programmer is utterly and inescapably deeply schizophrenic entity which:

  1. Keeps is mind all hundreds of UBs (even if compiler authors couldn't themselves compile adequate list of them in case of C++) and never, just NEVER violates them.
  2. Produces complete and utter garbage in place of code (probably because his head is taken by aforementioned rules and there are no space for anything else in it) which includes lots of pointless manipulations which process undefined values.

If we are dealing with such an entity then using #1 to combat #2 is entirely reasonable.

Unfortunately in real life programmers are exact opposite: they tend not to produce too much garbage yet sometimes create UBs by mistake. Applying these principles to code written by real people turns compiler writers into their adversaries, but that's OK since these users are not the ones who pay salaries to compiler writers, isn't it?

Footguns

Posted Jul 19, 2021 8:36 UTC (Mon) by smurf (subscriber, #17840) [Link]

> I think it is wrong to argue "but, I checked the pointers are equal" because there are a lot of expressions which can be true if and only if p and q are equal

That's not the problem here, and if it was then the compiler should treat them as potentially aliased.

Instead, it treats p as both valid (the location it points to can be written to) and invalid (it cannot possibly point at the same location as another pointer) at the same time. That's the very antithesis of correctness.

Footguns

Posted Jul 18, 2021 13:08 UTC (Sun) by mpr22 (subscriber, #60784) [Link] (1 responses)

> I would say the compiler did you a favour by giving a hint in the form of an unexpected result.

The compiler doing me a favour would be if it defaulted to throwing a fatal error (not a warning, a fatal error, your code doesn't compile, fix it please) when it is asked to compile code which invokes UB 100% of the time and can be cheaply proven to do so.

Footguns

Posted Jul 19, 2021 9:56 UTC (Mon) by farnz (subscriber, #17727) [Link]

This is something I hope future languages steal from Rust (along with Esteban Kuber's great work on high quality error messages): there is no UB in parts of the code not marked out as potentially containing it.

Now, there's a gotcha with this, in that while it's always an unsafe block in Rust that actually invokes UB, it's possible for the preconditions for that UB to be set up by Safe Rust (e.g. you set an offset into an array in safe code, but deference it in unsafe code), but you always know where UB could possibly be found in a body of Rust code, and you can use module scoping (privacy) to ensure that UB can't leak - with that done, it's possible to check an entire module that contains an unsafe block, and be confident that your human analysis shows no UB.

Footguns

Posted Jul 18, 2021 10:17 UTC (Sun) by Cyberax (✭ supporter ✭, #52523) [Link]

> Now of course people have different ideas about what reasonable people would or wouldn't do, but my point is that “this has been working for years, so it must continue to work” just isn't sufficient by itself.

Another story about compilers: it had taken Microsoft more than 5 years to move their C# and C++ compilers onto a new backend. Not because it was so complex, but because they wanted to maintain bug-for-bug compatibility with the old compiler.

They went to their biggest clients and made sure that huge codebases, like the ones that Adobe has, work perfectly.

This is another reason why Windows platform is still the main desktop environment.

Footguns

Posted Jul 18, 2021 7:19 UTC (Sun) by smurf (subscriber, #17840) [Link]

> Well, I have a different opinion. If your code is broken, LLVM (or GCC) developers are under no obligation to accommodate that.

You're getting off on a tangent. The code in question is not SimCity and their use-after-free bug.

The realloc example we're talking about is not broken in any way, shape or form. It's a valid optimization technique to not update pointers after realloc if you determine that the pointer didn't change. LLVM still breaks. There is no freakin' reason why that can possibly be OK.

If your nice and shiny pointer provenance rules and optimization break valid code, then, sorry, but at least one of those rules or optimizations is broken.

Footguns

Posted Jul 15, 2021 15:43 UTC (Thu) by mathstuf (subscriber, #69389) [Link] (21 responses)

> Why would you expect sane results when you do things that are so obviously not sane?

Then why won't the compiler stop me? That is also a sane behavior in the face of such code, but we can't have that either apparently.

Footguns

Posted Jul 15, 2021 15:58 UTC (Thu) by smurf (subscriber, #17840) [Link] (19 responses)

> Then why won't the compiler stop me? That is also a sane behavior in the face of such code, but we can't have that either apparently.

But that would cause old code to *gasp* Not Compile. No, we can't have that, because fixing this problem requires amending the standard so that programmers can tell the compiler what they mean.

Apparently, though, the compiler is by definition the all-knowing oracle which, by following The Holy Standard, is able to understand all by itself what a program may or may not mean. Thus actually telling it to behave differently is thus Abject Blasphemy, and amending the standard to afford such horrors would thus be Vile Heresy.

… or so it seems to me.

Prove me wrong.

Footguns

Posted Jul 15, 2021 16:09 UTC (Thu) by mathstuf (subscriber, #69389) [Link] (18 responses)

Nothing in the C++ (or C) standard stops implementations from warning about all kinds of valid-but-silly nonsense. We have warnings when you shadow variables (perfectly valid C++), implicit `int` to `bool` conversions (also valid, but a "performance concern" to MSVC), and other things. A warning that "I noticed that this can overflow and have therefore assumed it doesn't happen and removed it" is perfectly valid. Sure, it isn't the compiler *stopping* me, but it is at least letting me know that the grenade I've put into my pocket is missing a pin.

Footguns

Posted Jul 15, 2021 21:22 UTC (Thu) by mrugiero (guest, #153040) [Link] (17 responses)

> Nothing in the C++ (or C) standard stops implementations from warning about all kinds of valid-but-silly nonsense.

Yet nothing in the standards mandate it. How hard could it be to make them warn about it as part of the spec?

Footguns

Posted Jul 16, 2021 0:44 UTC (Fri) by mathstuf (subscriber, #69389) [Link] (16 responses)

I believe the line is that doing so would:

- inundate projects with oodles of warnings which they'll promptly disable anyways; and
- compilers would become much slower in order to track and issue diagnostics for such things.

But you'd have to ask the implementers for specifics if you want more. All I can say is that suggestions to do such things are not taken lightly on the implementer side at ISO C++ meetings.

Personally, I'd be fine with a warning level to such heights if just to show people how little they actually understand about C and C++ behaviors. Instead, we get monster threads like this one one a semiregular basis.

Footguns

Posted Jul 17, 2021 15:41 UTC (Sat) by mrugiero (guest, #153040) [Link] (15 responses)

> - inundate projects with oodles of warnings which they'll promptly disable anyways; and
As long as there's a flag, it's only a minor annoyance.

> - compilers would become much slower in order to track and issue diagnostics for such things.
I thought of this, but I'd rather have a slower build than a broken program. Besides, the flag is enough to mitigate this if I'm OK with my program being completely broken but want faster builds.

> But you'd have to ask the implementers for specifics if you want more. All I can say is that suggestions to do such things are not taken lightly on the implementer side at ISO C++ meetings.
I'm aware. But my point is this shouldn't be up for implementors to decide IMO. If our code is not doing what we think it's doing we need to know, it's as simple as that.

> Personally, I'd be fine with a warning level to such heights if just to show people how little they actually understand about C and C++ behaviors. Instead, we get monster threads like this one one a semiregular basis.
Agreed.

Footguns

Posted Jul 18, 2021 0:00 UTC (Sun) by HelloWorld (guest, #56129) [Link] (14 responses)

> If our code is not doing what we think it's doing we need to know, it's as simple as that.
So you're saying it's simple for the compiler to figure out what you think the code ought to be doing?

Footguns

Posted Jul 18, 2021 3:33 UTC (Sun) by mrugiero (guest, #153040) [Link] (13 responses)

> So you're saying it's simple for the compiler to figure out what you think the code ought to be doing?
No, I'm saying it's simple for the compiler to figure out that what you code means nothing and you obviously meant something, so in that case it isn't doing what you think it's doing :)
That's what happens when you hit UB. Nasal demons and all that. If your program has no meaning, that's not what you want it to mean.

Footguns

Posted Jul 19, 2021 10:26 UTC (Mon) by farnz (subscriber, #17727) [Link] (12 responses)

The problem with UB is not when all ways to interpret the code interpret UB - everyone, even compiler writers, agrees that there should be diagnostics for that.

The problem is when there are multiple ways to interpret the code, such that some interpretations don't lead to UB, but others do. The classic example is:


struct useful {
    int value;
    /* And a lot more bits */
};

void ub(void * forty) {
    struct useful * ptr = (useful*) forty;
    int value = ptr-> value;
    if (!forty) {
        return;
    }
    /* Do stuff with value */
}

There are two interpretations here; either forty is NULL, or it isn't. If forty is NULL, then ptr->value; is UB, and thus the code snippet invokes UB. If forty is not NULL, then ptr->value is not UB, and so the compiler reasons that !forty must be false, always.

And generated code, among other uses of C compilers, often depends on the compiler being able to spot that an expression must be true, and optimising accordingly. As a result, there's plenty of real code that depends on the optimizations that fire when forty cannot be NULL, and the compiler simply works backwards from what it knows - that if forty was NULL, then there would be UB, ergo forty must be known not to be NULL.

In this particular example, the compiler's reasoning chain (and the user's mistake) is fairly obvious, and it wouldn't be challenging to have a warning that says "hey, !forty is always false because ptr->value would be UB otherwise, so this if statement is not meaningful", and have the user spot the error from there, but it gets more challenging with complex code. And neither compiler developers nor standard committees seem willing to say that good C compilers give a friendly warning when they find a codepath that invokes UB - in part because it'd trigger a lot of noise on legacy codebases.

Footguns

Posted Jul 19, 2021 11:20 UTC (Mon) by smurf (subscriber, #17840) [Link] (7 responses)

> There are two interpretations here

Three. The third is that whoever wrote the program mis-ordered these statements. So the compiler might as well rearrange them (the code has UB, so it's allowed to do that), and emit a warning while it's at it.

Footguns

Posted Jul 19, 2021 11:45 UTC (Mon) by farnz (subscriber, #17727) [Link]

That is not an interpretation of the code as written - that's a guess at the authorial intention. And while this is a simple case, chosen because it's painfully obvious what the author meant, but it's also obvious how the compiler gets to "!forty must be false", one of the things that makes UB so painful is that the chain of reasoning the compiler uses from "if this is true, then UB" to "the meaning of this program is nowhere near what the author intended" can be huge, and even span multiple files if you're unlucky (assumption of not-NULL in a header file resulting in the only non-UB case being pointer is not NULL, or unsigned value is less than 64, or more complex things around signed overflow).

Which is why I think that there should be a way to mark areas where UB could happen (Rust uses the unsafe keyword for this), and the compiler should be on the hook for defining all behaviour outside those blocks. If that's not reasonable (e.g. due to legacy code), then as a QoI issue, I'd like compilers to explicitly call out in a warning when they're reasoning backwards from "anything else would be UB".

And yes, I know this is not easy. But it'd help out the people who want to write good modern C or C++ if their compilers could be trusted to alert them when there's a risk of UB resulting in badly behaved programs.

Footguns

Posted Jul 19, 2021 12:46 UTC (Mon) by excors (subscriber, #95769) [Link] (5 responses)

> Three. The third is that whoever wrote the program mis-ordered these statements. So the compiler might as well rearrange them (the code has UB, so it's allowed to do that), and emit a warning while it's at it.

Reordering the statements means it would have to emit instructions to perform the NULL check. If the function is never called with NULL, that's an unnecessary performance cost. It's not a "might as well" situation - it's choosing to sacrifice performance of correct code, in exchange for less surprising behaviour if the code is buggy.

I think farnz's point about "generated code" is particularly relevant for C++ because many libraries (including the standard library) depend heavily on template metaprogramming, which is basically a program that runs at compile-time where the inputs are C++ types and the outputs are C++ functions, so most C++ programs will include a lot of generated code in that sense. And that relies heavily on inlining, constant propagation, dead code elimination, etc, to get good performance - the metaprogram itself is (usually) functional and very recursive and has huge amounts of dead code, but needs to end up compiling the same as a simple imperative function.

Because the library developers know they can rely on the compiler doing that optimisation, they can design the libraries to be generic and type-safe and easy-to-use, while still getting as good performance as specialised hand-tuned code (sometimes purely by relying on clever compiler optimisation, sometimes by explicitly selecting different algorithms based on properties of the type it's being specialised for). And then users of the library can write much safer programs than if they tried to implement it by hand, because the library has been carefully designed and tested and reviewed by experts for years.

If you try to sacrifice performance for safety, then those libraries may become much slower and developers would stop using them, so they'd write bad manual replacements, and then you'd end up losing performance *and* safety.

(That doesn't apply so much to C, which doesn't have that metaprogramming facility (the closest thing is macros, which are terrible). But I guess the issue there is that few people care enough about C to write a dedicated C compiler, they just write a C++ compiler and stick a C frontend on it, and if C programmers refuse to use C++ then they'll share the costs of that C++-focused optimisation but will miss out on most of the high-level safety benefits.)

Footguns

Posted Jul 19, 2021 13:22 UTC (Mon) by mrugiero (guest, #153040) [Link] (4 responses)

> Reordering the statements means it would have to emit instructions to perform the NULL check. If the function is never called with NULL, that's an unnecessary performance cost. It's not a "might as well" situation - it's choosing to sacrifice performance of correct code, in exchange for less surprising behaviour if the code is buggy.
If the programmer put the check there then the programmer told the compiler the pointer could be NULL, so it shouldn't assume the opposite unless it can absolutely prove the programmer was wrong.

> If you try to sacrifice performance for safety, then those libraries may become much slower and developers would stop using them, so they'd write bad manual replacements, and then you'd end up losing performance *and* safety.
The template metaprogramming case is special in the sense the compiler knows if for that particular invocation the argument would be NULL (although you can't pass pointers as template arguments AFAIR, if we assume that it relies on inlining then we know for sure once the check has been performed at the outmost call we can erase it from all of the recursive calls). So you don't really sacrifice that much performance.

Footguns

Posted Jul 19, 2021 13:58 UTC (Mon) by farnz (subscriber, #17727) [Link] (2 responses)

Trivial plain C counterexample, where the check is seen by the compiler, but the programmer did not intend to tell the compiler that the pointer could be NULL:


/* In a header file, for config handling */
struct configs {
/* Lots of pointers to config members here, omitted */
};

#define GET_STR_CONFIG_OR_DEFAULT(config, member, default) \
    (config ? config->member ? config->member : default : default)

#define GET_INT_CONFIG_OR_DEFAULT(config, member, default)
    (config ? config->member ; default)

/* In a C file, using the header file */
void do_amazing_things(void *config) {
    int rating = GET_INT_CONFIG_OR_DEFAULT(config, rating, 0);
    char * expertise = GET_STR_CONFIG_OR_DEFAULT(config, expertise, "novice");

    /* Do the amazing things */
}

In this case, the call to GET_INT_CONFIG_OR_DEFAULT (a macro) has promised that config is not NULL because it does an early return; a compiler that doesn't optimize this will be issuing repeated NULL checks for something that it knows must be non-NULL, or complaining because the user has an unnecessary NULL check in the expansion of GET_STR_CONFIG_OR_DEFAULT.

The code the compiler sees when the preprocessor hands it off is:


struct configs {
/* Lots of pointers to config members here, omitted */
};
/* Rest of headers etc */

void do_amazing_things(void *config) {
    int rating = (config ? config->member : 0);
    char * expertise = (config ? config->member ? "expertise" : "expertise");
}

Which leads you into the question of provenance of your "always true" if statement; in this case, the compiler has to know that it knows config is not NULL because the programmer did something that's an explicit check, not a "if it's NULL, then we have UB" case, otherwise you get a noisy warning because the programmer included an unnecessary NULL check in the macro. But they did that because that way the macro is safe to use even if it's only used once in a function, and if the user does do:


/* In a C file, using the header file */
void do_amazing_things(void *config) {
    int rating = ((struct configs*) config)-gt;rating;
    char * expertise = GET_STR_CONFIG_OR_DEFAULT(config, expertise, "novice");

    /* Do the amazing things */
}

then the compiler will need to warn because its "config is not NULL" assumption came from "if config is NULL, then there is UB in the execution". But first we need compiler authors to get onboard with the idea that whenever UB is invoked to permit an optimization (noting that this can require you to do complex tracking to get from your low-level IR (MachineInstr in the LLVM case, for example) all the way back to C, so that you can confirm that a given decision in codegen is not causing issues because it results in bad signed overflow behaviour (for example).

Footguns

Posted Jul 19, 2021 19:11 UTC (Mon) by mrugiero (guest, #153040) [Link] (1 responses)

> In this case, the call to GET_INT_CONFIG_OR_DEFAULT (a macro) has promised that config is not NULL because it does an early return; a compiler that doesn't optimize this will be issuing repeated NULL checks for something that it knows must be non-NULL, or complaining because the user has an unnecessary NULL check in the expansion of GET_STR_CONFIG_OR_DEFAULT.
The compiler can prove the extra check is unnecessary there. I'm not sure it would be wrong for it to complain, but it's an entirely different case than adding a test after a dereference of something you can't prove to not be NULL.

> Which leads you into the question of provenance of your "always true" if statement; in this case, the compiler has to know that it knows config is not NULL because the programmer did something that's an explicit check, not a "if it's NULL, then we have UB" case, otherwise you get a noisy warning because the programmer included an unnecessary NULL check in the macro. But they did that because that way the macro is safe to use even if it's only used once in a function, and if the user does do:

> /* In a C file, using the header file */
> void do_amazing_things(void *config) {
> int rating = ((struct configs*) config)-gt;rating;
> char * expertise = GET_STR_CONFIG_OR_DEFAULT(config, expertise, "novice");

> /* Do the amazing things */
> }

> then the compiler will need to warn because its "config is not NULL" assumption came from "if config is NULL, then there is UB in the execution". But first we need compiler authors to get onboard with the idea that whenever UB is invoked to permit an optimization (noting that this can require you to do complex tracking to get from your low-level IR (MachineInstr in the LLVM case, for example) all the way back to C, so that you can confirm that a given decision in codegen is not causing issues because it results in bad signed overflow behaviour (for example).

The only thing I said is always true is that you can't dereference something you can't prove not to be NULL and expect it to not be UB; the check reordering is allowed because it was UB before and we can assume at that point what the author wanted. This doesn't mean a warning should not be emitted unless (somehow) specified otherwise. If there's a check before in the same function it's trivial to prove it's no longer NULL after that. Besides, you don't need to know whether an optimization resulted from the UB. Knowing it exists is enough. And AFAICT that can be known before transforming to IR.

Footguns

Posted Jul 20, 2021 11:54 UTC (Tue) by farnz (subscriber, #17727) [Link]

> The only thing I said is always true is that you can't dereference something you can't prove not to be NULL and expect it to not be UB; the check reordering is allowed because it was UB before and we can assume at that point what the author wanted. This doesn't mean a warning should not be emitted unless (somehow) specified otherwise. If there's a check before in the same function it's trivial to prove it's no longer NULL after that. Besides, you don't need to know whether an optimization resulted from the UB. Knowing it exists is enough. And AFAICT that can be known before transforming to IR.

This is the difference between your model, and the C model. In C, you can dereference something that you can't prove not to be NULL, and expect it to not be UB; it's only UB if, on a specific execution, the thing is NULL. If it happens not to be NULL, then there's no UB. This comes down to whole program analysis - as a programmer, you might ensure that you only call ub(config) with non-NULL pointers, and thus it's fine.

So the C compiler is able to reason backwards - if thing *is* NULL, then there is UB. Ergo is must not be NULL, because UB isn't allowed, which permits optimization on the basis that it's not NULL. This is fine if the explicit NULL check is (e.g.) from a macro, or in generated code, or left over from refactoring and not yet removed; you've taken out something that's always false in this context, and just deleted the dead code.

It only becomes a problem where the human is surprised by the deletion of dead code - i.e. where the human thought the check was still important, but it's actually considered always false (or always true) by the compiler. And as deletion of dead code takes place at every level - from AST, through IR, through machine instruction choice - the compiler needs to link back its elimination of dead code and determine whether or not the human would be surprised by this particular elimination.

And that's what makes it a hard problem - we may know that ub(config) is only ever called with a non-NULL pointer, but that needs a whole program analysis which the compiler cannot perform. We may also know this simply because we've refactored so that callers of ub(config) do the NULL check themselves, and the extra NULL check inside the function may be there because a preprocessor (which can be outside the compiler!) has used a general form of code that works regardless of the presence of the NULL checks; why use two macros and force the human to think about whether or not a NULL check has happened already, when you can use one?

Footguns

Posted Jul 19, 2021 14:56 UTC (Mon) by excors (subscriber, #95769) [Link]

> The template metaprogramming case is special in the sense the compiler knows if for that particular invocation the argument would be NULL (although you can't pass pointers as template arguments AFAIR, if we assume that it relies on inlining then we know for sure once the check has been performed at the outmost call we can erase it from all of the recursive calls)

But the compiler may not know that. E.g. you could have code like:

struct S {
    int n;
    // ...
};

template <typename T, typename Pred>
void reset_if(T &items, Pred pred) {
    for (auto &item : items)
        if (pred(item))
            item = nullptr;
}

// Precondition: all items are not nullptr
// Postcondition: all items are even-numbered or are nullptr
void remove_odd(std::vector<std::unique_ptr<S>> &items)
{
    reset_if(items, [](auto &item) { return item->n % 2 != 0; });
}

(i.e. there's a dynamically-allocated array of pointers to S, and the function deallocates all the Ss that match some condition. This is somewhat artificial, but I don't think it's totally implausible as a component of some larger operation). The function's precondition is guaranteed by the programmer but is not visible to the compiler. (Maybe the caller is in a separate translation unit, or a dynamically-loaded library.)

On "item = nullptr", the unique_ptr will deallocate its original contents (to prevent memory leaks). Because unique_ptr is generic and supports custom deallocator functions, and the custom deallocator might not accept nullptr as an argument, the unique_ptr destructor is specified to do something like "if (get() != nullptr) get_deleter()(get());".

In this function, the unique_ptr destructor is only called after the "item->n % 2" check, which is dereferencing the pointer. Then the compiler knows the pointer can't be nullptr, so it can omit the "if (get() != nullptr)" test and save some time and code size. (In this case it's only a couple of instructions, but it's not hard to imagine similar cases where the 'if' is followed by an 'else' with a much larger chunk of code, and eliminating the whole 'else' clause could make a big difference.)

The null check wasn't intentionally added to this code by any programmer, it's just a part of the (relatively) safe abstraction provided by unique_ptr, so it's included in the code that's generated by the template instantiation. In this function the code is used in a way where the check is redundant and the compiler is able to figure that out because it sees the programmer was deliberately dereferencing the pointer, so the abstraction has zero cost and the programmer doesn't have to revert to a less-safe kind of pointer to get the same performance.

Footguns

Posted Jul 19, 2021 13:16 UTC (Mon) by mrugiero (guest, #153040) [Link] (3 responses)

> There are two interpretations here; either forty is NULL, or it isn't. If forty is NULL, then ptr->value; is UB, and thus the code snippet invokes UB. If forty is not NULL, then ptr->value is not UB, and so the compiler reasons that !forty must be false, always.
AFAICT, the logical conclusion here is that every pointer argument can be NULL unless proved otherwise. Since you can't prove that for non-static functions, that's necessarily UB. This is why many compilers have extensions to explicitly tell the compiler an argument can never be NULL.

> In this particular example, the compiler's reasoning chain (and the user's mistake) is fairly obvious, and it wouldn't be challenging to have a warning that says "hey, !forty is always false because ptr->value would be UB otherwise, so this if statement is not meaningful", and have the user spot the error from there, but it gets more challenging with complex code. And neither compiler developers nor standard committees seem willing to say that good C compilers give a friendly warning when they find a codepath that invokes UB - in part because it'd trigger a lot of noise on legacy codebases.
Which was exactly my point. Besides, the point of triggering noise is moot IMO, since you can always add a flag to ignore the warning like with any other warning.

Footguns

Posted Jul 19, 2021 13:27 UTC (Mon) by farnz (subscriber, #17727) [Link] (2 responses)

> > There are two interpretations here; either forty is NULL, or it isn't. If forty is NULL, then ptr->value; is UB, and thus the code snippet invokes UB. If forty is not NULL, then ptr->value is not UB, and so the compiler reasons that !forty must be false, always.

> AFAICT, the logical conclusion here is that every pointer argument can be NULL unless proved otherwise. Since you can't prove that for non-static functions, that's necessarily UB. This is why many compilers have extensions to explicitly tell the compiler an argument can never be NULL.

This is why the function included the check "if (!forty) { return; }"; after that test, forty is provably not NULL, and the compiler can optimize knowing that forty is not NULL. The gotcha is that the user invoked UB before that check by dereferencing ptr, and the compiler used that information to know that forty cannot be NULL after the dereference, because if it is, UB is invoked.

> > In this particular example, the compiler's reasoning chain (and the user's mistake) is fairly obvious, and it wouldn't be challenging to have a warning that says "hey, !forty is always false because ptr->value would be UB otherwise, so this if statement is not meaningful", and have the user spot the error from there, but it gets more challenging with complex code. And neither compiler developers nor standard committees seem willing to say that good C compilers give a friendly warning when they find a codepath that invokes UB - in part because it'd trigger a lot of noise on legacy codebases.

> Which was exactly my point. Besides, the point of triggering noise is moot IMO, since you can always add a flag to ignore the warning like with any other warning.

And my point is that this is entirely a social problem - nobody with the power to say "compilers that use UB to optimize must tell the user what UB the compiler is exploiting" is willing to do so. If compiler writers for C were doing that, even if it was the preserve of the very best C compilers (so GCC and CLang), it'd be enough to reduce the number of developers who get surprised by compiler use of UB.

Footguns

Posted Jul 19, 2021 18:57 UTC (Mon) by mrugiero (guest, #153040) [Link] (1 responses)

> This is why the function included the check "if (!forty) { return; }"; after that test, forty is provably not NULL, and the compiler can optimize knowing that forty is not NULL. The gotcha is that the user invoked UB before that check by dereferencing ptr, and the compiler used that information to know that forty cannot be NULL after the dereference, because if it is, UB is invoked.
Of course. My point was that _on entry_ it isn't provably not NULL, so NULL dereference is a possibility in that line, and thus the conclusion should consistently that the first dereference is UB. My point is that there's no true ambiguity about that.

> And my point is that this is entirely a social problem - nobody with the power to say "compilers that use UB to optimize must tell the user what UB the compiler is exploiting" is willing to do so. If compiler writers for C were doing that, even if it was the preserve of the very best C compilers (so GCC and CLang), it'd be enough to reduce the number of developers who get surprised by compiler use of UB.
So we agree, then?

Footguns

Posted Jul 19, 2021 21:10 UTC (Mon) by farnz (subscriber, #17727) [Link]

And the problem with your point is that C is not very amenable to whole program analysis (for a variety of reasons to do with only having a global and a local static namespace, plus using textual inclusion instead of modules), which means that there's a lot of legacy code where the first dereference is not UB. It's only UB if the program execution can call ub with forty being NULL, which is not guaranteed. If the rest of the code ensures that ub is always called with a valid pointer to configs, then no UB is present.

As I've pointed out in another comment, that check could be the consequence of a macro expansion - in which case, pessimising the output machine code because you've used a macro to access configs isn't a good look - there was no UB before, now you're adding code that verifies something that the human writing the code has already verified is true outside ub, and the use of a macro has been sufficient to introduce extra machine instructions.

This is part of why it's a damn hard social problem - it's possible that ub is only called from code in 3 different C files, and all of those files already check for NULL before calling ub, so in the actual final application, there is no UB. The compiler using the fact that the check can only be true and cause an early return if UB has already happened to optimize ub simply speeds things up at no cost.

Footguns

Posted Jul 15, 2021 16:38 UTC (Thu) by HelloWorld (guest, #56129) [Link]

> Then why won't the compiler stop me?
It does, unless you explicitly tell it not to by casting away const.

Footguns

Posted Jul 11, 2021 23:35 UTC (Sun) by roc (subscriber, #30627) [Link]

> I suspect they're sponsored by the same who sponsor rust because over the last 5 years or so

I hope you're joking, because if you aren't, it's bonkers conspiracy theory stuff.

Footguns

Posted Jul 11, 2021 6:39 UTC (Sun) by wtarreau (subscriber, #51152) [Link] (1 responses)

> Why are C compilers so "awful"? Because the *users* of those compilers *demand* that awfulness.

This is false. The worst fear from both developers and distro maintainers is the release of the next gcc version that will again immediately break the build of 10% of their software and silently break 10 other percent at runtime, that will often be detected by end users before being caught by unit tests or reg tests.

The *only* motivation here is competition between gcc and clang. Binary code correctness is their least concern. The amount of stupid warnings they can throw at the developer's face however, is a well accepted metric, which often forces developers to introduce dangerous workarounds to silence them, to the point of sometimes writing bugs where they were none.

Footguns

Posted Jul 11, 2021 15:08 UTC (Sun) by pizza (subscriber, #46) [Link]

> This is false. The worst fear from both developers and distro maintainers is the release of the next gcc version that will again immediately break the build of 10% of their software and silently break 10 other percent at runtime, that will often be detected by end users before being caught by unit tests or reg tests.

Sure, those are legitimate concerns from a subset of compiler users. But they're not the only users, many of whom are busy chasing [micro]benchmarks and lauding 0.5% performance boosts and/or 2% smaller binaries out of existing codebases. [1] Perhaps more importantly, the latter folks are the ones actually funding compiler development.

> The *only* motivation here is competition between gcc and clang. Binary code correctness is their least concern. The amount of stupid warnings they can throw at the developer's face however, is a well accepted metric, which often forces developers to introduce dangerous workarounds to silence them, to the point of sometimes writing bugs where they were none.

In the 20-odd-years I've been slinging C, the number of "false positive" warnings generated by the compiler represent a fraction of a percent. The overwhelming, overwhelming majority of the other warnings were legit issues caused by the programmer making assumptions that might not be true in practice... if not outright mistakes.

[1] I've been one of those users several times over the course of my career, gladly turning on every "Make the binary as small as possible" black-magic option. The alternative was redesigning the hardware (which in one case was an actual ASIC).

Footguns

Posted Jul 11, 2021 11:36 UTC (Sun) by smurf (subscriber, #17840) [Link] (15 responses)

> In other words, don't tell it to try and optimize your code, and it won't.

That doesn't work, there is lots of code out there that won't even link when optimizations are turned off.

> Why are C compilers so "awful"? Because the *users* of those compilers *demand* that awfulness.

No. Because these optimizations are perfectly valid in languages without C's undefined-behavior "rules". Optimized programs are faster and/or smaller. Uusers want optimizeable programs. When compiler A builds code that's 2% smaller and/or 1.5% faster than compiler B's, users complain to B's authors and demand improvements.

The problem is that, all too often, warnings about undefined behavior fall by the wayside.

The realloc nonsense is a case in point. Disregarding for the moment that the optimizer should recognize that "p" aliases "q", the pointer you pass in becomes invalid whenever realloc returns non-NULL. You're not supposed to use it after that call, end of discussion. Does the compiler complain when you do anyway? no, mainly because the language doesn't even have a way to freakin' *tell* it. Neither can you teach the compiler that a return value can be NULL and thus *must* be checked before being used.

There are lots of other examples of this kind of "why add a way to reject undefined behavior when we could just as well let the program crash and burn instead" thinking by the C/C++ standardization people, which is why it's completely understandable that certain people got fed up with it all and created Rust instead.

.-.

The failure IMHO isn't so much that there are new optimizations here. The failure is that you can't tell the compiler when you know they're invalid, and even when you can and do (e.g. using the realloc example's pointer comparison) some optimizers refuse to listen but still, apparently, conform to the standard.

Just for laughs: this problem was introduced in clang 3.7. It's now at version 12 and the bug is still there. GCC on the other hand does the same optimization (i.e. it doesn't dereference the pointers) but at least it notices that they are aliased. In fact it has noticed since 4.1, which is the earliest version on godbolt.

Cute, clang people. Really cute. Tell me again why I should trust your compiler to generate correct code?

NB: not to single out clang. I just didn't check whether GCC has similar brokenness somewhere else in its codebase.

Footguns

Posted Jul 11, 2021 12:18 UTC (Sun) by khim (subscriber, #9252) [Link]

> NB: not to single out clang. I just didn't check whether GCC has similar brokenness somewhere else in its codebase.

It sure does. If you look on the proposal which explicitly intends to declare formely valid programs invalid because of UB you'll find examples there for GCC, too. And realloc example is broken on ICC and MSVC, so no need to look hard.

In fact MSVC goes father than clang and GCC. If doesn't even look on the name of function! If your function accepts void pointer and size, then it's probably realloc and it's Ok for the compiler to rely on that.

Nifly, huh? Even if your hohoho just returns pointer it receives — according to MSVC that invalidates pointer passed to it.

> which is why it's completely understandable that certain people got fed up with it all and created Rust instead.

Actually that's the reason why Rust tries so very hard to ensure that only code marked with unsafe moniker can cause undefined behavior. But keep in mind that wrong unsafe can “infect” safe Rust and thus actual misbehavior can happen in a very different piece of program, not just inside an unsafe code block.

Initially Rust wasn't as religious about it, but as C/C++ compiler developers tried to ensure that these languages are unfit for any purpose harder and harder… Rust tightened the screws more and more tightly.

Today they promise that any undefined behavior in an unsafe code have to come from unsafe code block and that code block should live outside of standard library.

In fact that's where they put the limit on a stability guarantee: if your old program doesn't work with a new compiler (or couldn't be compiled with a new compiler) it's a bug to be fixed ASAP — except when your program relies on some kind of loophole which may introduce UB into safe Rust. Then, as last resort, they can break such code (but preference goes into “make it compile-time error” instead of crazy C/C++ stance where making code non-compileable is huge sin, but making it broken is “business as usual”).

Footguns

Posted Jul 11, 2021 15:27 UTC (Sun) by pizza (subscriber, #46) [Link] (13 responses)

> The problem is that, all too often, warnings about undefined behavior fall by the wayside.

If by this you mean "folks writing code routinely ignore/disable warnings and complain when things don't work as expected" then I wholeheartedly agree.

Footguns

Posted Jul 11, 2021 15:55 UTC (Sun) by khim (subscriber, #9252) [Link] (10 responses)

But they don't disable warnings because of malice! They disable warning because “they know better”! And, indeed, the original plan explicitly called for compilers writers to augment the language by providing a definition of the officially undefined behavior. To make life of such developers easier.

Instead C and C++ developers have painted themselves into the corner: by repeating mantra “nothing except the standard text matter to us” they made said standard text sacred. Now, when they have found out that some optimizations they actually perform are unsound and don't obey the letter of said standard… attempts to change the standard are rightfully and correctly perceived as an act of sabotage.

Maybe if for last couple of decades compiler developers have actually listened to the users and not used said standard as fig leaf… then maybe, just maybe reaction would have been different. Even then: it's not guaranteed. Backward incompatible changes, especially not detectable at compile time is not something which users would welcome.

But at this point it looks as if the only sane decision would be to switch to some other language. Where developers actually listen to user's complains.

Rust is one such language.

Footguns

Posted Jul 11, 2021 16:09 UTC (Sun) by pizza (subscriber, #46) [Link] (9 responses)

> But they don't disable warnings because of malice! They disable warning because “they know better”!

"Sufficiently advanced stupidity is indistinguishable from malice"

> Instead C and C++ developers have painted themselves into the corner: by repeating mantra “nothing except the standard text matter to us”

Because "The standard" is all that actually matters, because it is the contract that everyone involved relies upon to determine what to expect. If "the standard" is inadequate, then the correct thing to do is *improve the standard*

Otherwise you're just asking for the compiler equivalent of IE6 ("standards be damned, we're the ones with the market share") and OOXML's "autoSpaceLikeWord95" parameter.

> But at this point it looks as if the only sane decision would be to switch to some other language. Where developers actually listen to user's complains. Rust is one such language.

Which users are Rust's stewards listening to? What happens when (if?) its userbase grows large enough that their requests can not be reconciled? Will the "losing users" then have to switch to a new language whose developers "Actually listen to users' complaints"?

Footguns

Posted Jul 11, 2021 17:00 UTC (Sun) by khim (subscriber, #9252) [Link] (6 responses)

> If "the standard" is inadequate, then the correct thing to do is *improve the standard*

Then why haven't compiler developer have done that? Why the heck they made signed integer two's complement without making integer overflow legit?

> Otherwise you're just asking for the compiler equivalent of IE6 ("standards be damned, we're the ones with the market share") and OOXML's "autoSpaceLikeWord95" parameter.

No. I'm asking about some kind of dialogue where users talk to compiler writers and they listen (instead of dismissing any and all concerns with reference to the standard).

Note that story with IE6 ended not with some kind of all-compassing “finished and sacred” HTML5 standard, but with living standard. Which tries to reconcile concerns of different actors.

Nothing like that happened with C/C++ developers. First they ignored any and all concerns about standard craziness. And then, when they found out that standard is problematic for them, too — they decided do change it. And retroactively turn valid programs into invalid.

That is why it was pulled in through “defect report” venue (and not through “changes for the next version” venue).

They want to try to pretend that standard always meant to say what it doesn't explicitly say. And don't plan to offer any compatibility options.

Have they really though compiler users wouldn't notice that bait-and-switch tactic?

> Which users are Rust's stewards listening to?

Please watch (or read) that presentation by Stephen Klabnik. He explains that better than me.

But the short answer is: they try to minimize number of unhappy users. Not by applying some crazy rules buy by listening to a different people and trying to reconcile opposing POV.

They have found out that when you actually listen and explain people very often are willing to change their opinions and compromise which is acceptable for everyone (or almost everyone) involved is possible more often then you may think.

Right now they are brainstorming the asynchronous programming story and, frankly, I'm 99% sure they would reach a much better compromise then what C++ got and instead of something everyone hates equally they would reach to something everyone would accept equally.

> What happens when (if?) its userbase grows large enough that their requests can not be reconciled? Will the "losing users" then have to switch to a new language whose developers "Actually listen to users' complaints"?

We would see, I think. Nobody have the crystal ball and can predict the future. Anything can happen but I just don't foresee the situation which was the norm for last 10-15 years in the C/C++ land where opinions of the wast majority of “normal developers” were ignored “because sacred standard gave us that right” and the “my way or the highway” approach to the treatment of UB was normal.

In spite of the fact that standards committee explicitly said that UB identifies areas of possible conforming language extension it was almost never treated as such.

Instead of looking for a way to remove UB where it made sense (to make language easier to work with) compiler developers always seeked a way to exploit and amplify them (to win these all-important 2-3% on SPEC CPU benchmarks).

That is something I don't see in Rust so far (but then, it only has one official compiler thus benchmark games don't matter… yet).

Consider how that issue with signed overflow was treated. First of all: there are large and detailed document which explains the decision. Second: for the ones who want/need wrapround there are standardized handling of it. And, most important, it was clearly decided that overflow may be handled differently, but it absolutely, under no circumstances can be used to remove code from the program.

It's not a UB.

Now compare to what happens in C/C++ land.

Footguns

Posted Jul 14, 2021 16:21 UTC (Wed) by pizza (subscriber, #46) [Link] (5 responses)

> Now compare to what happens in C/C++ land.

Here's what I see.

Rust is able to be more "responsive" to users' complaints than the C & C++ folks because it is young, immature, and not widely used. [1] I'm not saying that pejoratively; this gives Rust a _lot_ more freedom and flexibility to rapidly evolve and improve, and this is also reflected in the processes that Rust uses to improve itself. Its lack of users (and diversity of use) means there's very little at stake, and the single-implementation monoculture ensures everyone is using the same tooling with changes becoming rapidly available.

However, C & C++ have many independent implementations, and (due in part to a 30 year head start vs Rust) have many orders of magnitude more lines of code, projects, and active coders out there, with the inevitable (and _considerable_) historical baggage that entails. Each of those stakeholders has different requirements and goals (that are, more often than not, inherently confliciting), and collectively represent a _lot_ of investment. This means the standardization efforts operate much more conservatively, slowly, and *formally*, but perversely, that many-independent-implementations state of affairs means that there's _less_ need to actually participate in standardization efforts. (This is why C++ was such an utter interoperability mess for so long..)

But yes, standards groups fall back to their standards documents, which are simultaneously sacrosanct and highly fungible depending on the context and what's being discussed. Welcome to the wonderful world of diverse stakeholders that only occasionally pull in the same direction. If Rust succeeds in becoming anything more than a small niche language, this diversity (and inevitable impact on velocity & responsiveness) will happen to them too... indeed, it's more likely that achieving this diversity is necessary in order for Rust to succeed.

Rust seems to have the basics/core down pretty solidly now; ie the language meets their own goals/needs fairly well. But trying to grow the tent means they have to appeal more broadly -- and beating/berating the folks you're trying to convince isn't exactly endearing. Putting questionable PR aside, the harsh reality is that these other prospective users need a gradual path to adoption, and Rust is still quite awful about integrating into both predominantly non-Rust codebases and into a larger systems as a whole. Sure, they're working on improving both, but again, software & system maintainers tend to be _very_ conservative and have seen plenty of "don't worry, we'll solve these [very hard problems] later" hot-and-shinies come and go.

FWIW, personally, if I were to start a new project today I'd probably try it in Rust, but there's zero advantage to rewriting my existing C projects.

[1] The product at $dayjob that my team will eventually integrate into has over 5.5M lines of in-house C++ code. Once you include stuff shared with other in-house products and 3rd-party code/libraries it goes up considerably further. It's safe to say that my employer easily has tens of millions of lines of C++ code in production today, and I wouldn't be surprised if the actual number is closer to 100 million. And this is just _one_ (admittedly large) organization.

Footguns

Posted Jul 14, 2021 17:19 UTC (Wed) by khim (subscriber, #9252) [Link] (2 responses)

> Rust is able to be more "responsive" to users' complaints than the C & C++ folks because it is young, immature, and not widely used.

True, but what about Java, Python, or, IDK., C#? Python is 30 years old, Java is 26 years old. And amount of code written in both is comparable to C or C++.

Yes, there are tensions between developers of these languages and users of them, there are always are. But nothing like the situation with C and C++ where concerns of users are usually dismissed with magic phrase “you don't know C (or C++), read the standard, please”.

I don't know of any other language where “holy standard gave us the right, you must bow before our might” is the normal attitude.

In fact even when Spidermonkey or V8 developers justify something with reference to standard it's usually with note of sadness and intent to help. You can certainly feel that they genuinely want to help the developer, user of their language, but they have to follow the standard rules, too. So they are looking for the compromise (and often finding it: by showing standard-compliant compromise, adding certain feature or just offering a way to refactor code to make it better).

For them the fact that standard is, in some places, crazy and stupid is a limitation which they still have to follow, but attempts to make life of users easier are equally important.

And if they can fix the issue in a new version of a standard — they do that without leaving pile of footguns behind (except where backward compatibility make them do that).

> This means the standardization efforts operate much more conservatively, slowly, and *formally*, but perversely, that many-independent-implementations state of affairs means that there's _less_ need to actually participate in standardization efforts. (This is why C++ was such an utter interoperability mess for so long..)

Java never had such an issue, despite there being lots of players, too. No, I think the toxic C/C++ compiler developers attitude comes from the mere fact that most C/C++ developers, perversely enough, are irrelevant for them.

They are captive audience. Either they need to use proprietary compiler to develop code for certain hardware platform. Or, they are not even directly relevant at all if compiler developer comes from some university (then the need to publish paper is much more important than the need to send time thinking how real users would use the compiler).

It also doesn't help that C and C++ are last widely used languages which were developed before rise of the Usenet, mailing lists or web sites.

The actual software developers are not their customers — that is the issue. Everything else is secondary.

> Sure, they're working on improving both, but again, software & system maintainers tend to be _very_ conservative and have seen plenty of "don't worry, we'll solve these [very hard problems] later" hot-and-shinies come and go.

That's exactly why I think trying to bring these folks by talking about virtues of Rust is pointless. Ultimately it all would be decided by Google and Microsoft (and other large companies to lesser degree). If their management would like Rust and divert development resources from C++… house of cards may implode very quickly. If not… then Rust would remain strong yet niche player like Haskell for years.

In both cases it makes little sense to try to bring C++ developers to the Rust bandwagon: if Google and Microsoft would continue with C++ then they would be able to use for years, if they would back Rust instead then they would have to come to Rust camp.

It's probably better to bring Javascript, Python or maybe even Java developers: users of these language are not accustomed to the deal with languages composed almost entirely from footguns, but they appreciate speed.

C++ developers would join later… if that would even happen at all.

> FWIW, personally, if I were to start a new project today I'd probably try it in Rust, but there's zero advantage to rewriting my existing C projects.

That sounds like a sensible approach. Yes, sooner or later, without constant vigilance, these C projects would die because new generations of compilers would find a way to compile them onto nothing with a very powerful prover that would prove that something triggers “undefined behavior” in them, but while they work… why spend efforts to fix something that is not broken?

> Rust is still quite awful about integrating into both predominantly non-Rust codebases and into a larger systems as a whole.

I'm 99% sure Google and Microsoft would find a way to solve that issue. Maybe by creation of Rust++ (similar to how Apple solved that problem with Objective C++ years ago — language which makes zero sense on it's own, but allows one to write a glue code which connects Objective C code to C++ code).

If they would decide it's worthwhile for them to switch from C++ to Rust, of course. I don't think they would bother to both push C++ forward and write something like that.

Footguns

Posted Jul 14, 2021 18:45 UTC (Wed) by pizza (subscriber, #46) [Link] (1 responses)

> Java never had such an issue, despite there being lots of players, too.

Yes and no -- Java was completely proprietary until 2007, and to this day is still ultimately under the control/ownership of a single (very litigious) company.

Footguns

Posted Jul 14, 2021 19:17 UTC (Wed) by khim (subscriber, #9252) [Link]

But why would that force it's developers to hear complains of the users? Wouldn't that make reponse “you have violated the spec, thus your program is wrong, just go and fix it” more attractive, not less?

Can you show me any other language at all where response to some complaint from users (and they are numerous for any language) starts and finishes with reference the the standard, reference manual, or some other documentation?

Not “if we do X then we would have this, if we do Y then we would have that, and we if we would do Z then we would have something else and standard says we should do Y thus that's what we do”, but flat out refusal to even hypothetically contemplate the notion that what the standard says may not be a good idea to do at all? Where standard is turned into holy gospel instead of being perceived as one (albeit, sure, very serious) argument among many?

I just couldn't recall such an attitude among developers of any other language. Consider shifts: Java and Rust pick “modulo size of operand” choice while Go picks “mathematically defined shift” (where 1 << 100 is 0). Both choices have pro and cons, both make some sense, but C compiler developers pick “if we ever catch you doing that we would punish you” which have only one justification: “standard allows us to do that thus we will do that”.

Can you show me one place where developers of any other language are doing that? Knowingly make the life of their users hell just because their “holy gospel” say they have the right to do that — while pointedly and explicitly ignoring and and all other arguments?

I'm not saying that it never happens. I just haven't seen that. So if you have such examples — can you show me them to make our discussion more concrete?

Footguns

Posted Jul 14, 2021 18:03 UTC (Wed) by Cyberax (✭ supporter ✭, #52523) [Link] (1 responses)

> FWIW, personally, if I were to start a new project today I'd probably try it in Rust, but there's zero advantage to rewriting my existing C projects.

It might make sense to eventually rewrite legacy codebases (like Unix command line tools) just to make them more approachable for new people.

Footguns

Posted Jul 14, 2021 18:28 UTC (Wed) by khim (subscriber, #9252) [Link]

It may be fun project for someone who may want to learn Rust, but it don't think right now it would make them more approachable for new people.

Rust currently much more popular, than, e.g., Haskell, but still there are many times less developers than C or C++.

Footguns

Posted Jul 12, 2021 9:19 UTC (Mon) by Wol (subscriber, #4433) [Link] (1 responses)

> > Instead C and C++ developers have painted themselves into the corner: by repeating mantra “nothing except the standard text matter to us”

> Because "The standard" is all that actually matters, because it is the contract that everyone involved relies upon to determine what to expect. If "the standard" is inadequate, then the correct thing to do is *improve the standard*

You're missing Khim's point. Whether he is right or not is irrelevant here - what has allegedly happened is that the compiler writers have BROKEN THE STANDARD and, instead of trying to fix the compiler, are trying to fix the standard instead.

Oh - and as for the standard being inadequate, the correct fix is to *remove* UB, not convert what was defined behaviour into UB, which again is Khim's point.

Cheers,
Wol

Footguns

Posted Jul 12, 2021 10:39 UTC (Mon) by khim (subscriber, #9252) [Link]

What you are saying is correct, only there are no allegedly there.

The sequence of events:

  1. First compiler writers for decades use standard as “big club” to lord over the users. When users (including very prominent users like Linus) claimed that standard is inadequate their response is always “dura lex, sed lex” — and all the (hundreds thousands? millions?) of C/C++ developers must just bow and accept (sometimes they offer some flags as concessions like -fwrapv/-ftrapv, but AFAIK -ftrapv was broken for years with GCC and I'm not even sure it's fixed now).
  2. At some point they have found out that they don't have enough undefined behaviors in the standard to punish the users optimize well… and they needed them because they already miscompiled certain standard-compliant code.
  3. They went to the standards committee and presented their question (remember, we are talking about breaking well-defined, according to the standard, C programs here!) as certain defect report… note the dates.
  4. There was “much discussion” and eventually, reluctantly, C committee “came to a number of conclusions as to what it would be desirable for the Standard to mean” (emphasis mine). Note: even after, most likely, threatening to just ignore the standard and do their own thing, they haven't got the concession about actual meaning of the standard… only about “what it would be desirable for the Standard to mean”
  5. At that point story have started to become bizzare. After the resolution of the standard committee two C standards (C11 and C18) were created and both without incorporation of these changes. Why? Because of unsoundness. Standards committee (and remember, it's more thorough than C++ standards committee, it actually lists all these all-important “undefined behaviors” in attachment) rejected numerous proposals because of that.
  6. In fact, when Rust developers (who are, as we know, obsessing not about benchmarks, but about correctness) started looking on that issue (and they had to because currently Rust compiler is based on LLVM, GCC backend was added just few days ago) they have found out that they can turn completely valid C program into pile of goo (it doesn't happen in real LLVM usage because optimization phases are applied in certain order).

So the end result:

  1. For last 20 years compiler authors claimed that C and C++ developers have to follow the standard (because, see, it's the only contract between compiler and users… nothing else matters).
  2. For last 20 years compiler authors refused to follow the same standard (because, see, we have the fig leaf of DR260 resolution and it's “only technical issue” of actually changing the standard now).

P.S. I would have been more sympathetic to the woes of compiler writer is they would have been more sympathetic to woes of developers. And requests of “why shift by 32 turns my program into pile of goo?” (even if after said shift I do x & 0 which, you know, in normal math always produces zero) answer was not “standard gave us right to assume that, go away” but something more reasonable (e.g. some kind of intrinsic which I can use for that). But after decades of both insisting that users have to follow standard to the letter and apparent refusal of developers to follow it… it's hard to see what kind of dialogue is possible.

Footguns

Posted Jul 11, 2021 17:03 UTC (Sun) by smurf (subscriber, #17840) [Link] (1 responses)

No, that's not what I mean.

My point is that C / C++ doesn't have any features to declare whether, or to what extent, any given code is "safe" or "unsafe". Like "this value might be aliased" (one of the zillion possible meanings of "volatile" …) or "this here is a pointer; that there is an array of size 42" or "tell me if this integer operation overflows".

Thus the compiler can either assume the worst (which disables whole classes of optimizations: traditional compilers) or transitively propagate UBs to whichever remotely plausible result (for some value of "plausible" that often corresponds to no real computer ever built) produces the best-optimizable code (current clang and/or gcc).

Most of these problems DO NOT generate warnings at either compile or run time and there is NO way to catch them without extensive tooling (e.g. convert integers into a struct/class and overloading all basic integer operations), if at all (you simply can't teach the C compiler to not just let you use a pointer like an array) - and even when you can, and do, the compiler people seem to be free to ignore you, as witnessed by the aforementioned realloc nonsense.

The upshot is that compilers do not warn about (many, but not all of) these problems because your compiler run would be inundated with ludicrous warnings about perfectly safe code, both your own and in header files, which the compiler cannot distinguish from unsafe usage. In many cases, there's no way to tell, and the people responsible for the language refuse to add ways to fix the problem.

This won't go away even if the compiler could prove that 95% of these cases are safe because the remaining 5% are still too much. Thus nobody would enable these warnings, thus there's no reason a sane compiler coder would add them in the first place.

Footguns

Posted Jul 11, 2021 21:13 UTC (Sun) by pizza (subscriber, #46) [Link]

> My point is that C / C++ doesn't have any features to declare whether, or to what extent, any given code is "safe" or "unsafe". Like "this value might be aliased" (one of the zillion possible meanings of "volatile" …) or "this here is a pointer; that there is an array of size 42" or "tell me if this integer operation overflows".

Ah, okay. Thanks for the clarification. FWIW I think I'm in agreement with you.

Unfortunately, most of my professional C slinging has been with code that is inherently "unsafe". I do wish the C standards folks would try to improve bare-metal usecases instead of trying to make C more like (the awful parts [1] of) C++.

...On the other hand, I recently had to write a userspace DMA driver.. in Python. Talk about the worst of all possible worlds...

[1] which IMO is "most of it"

Footguns

Posted Jul 11, 2021 16:00 UTC (Sun) by Wol (subscriber, #4433) [Link] (1 responses)

> Yet clang would happily turn it into a function which returns false if you give it some variable which is not initialized.

> This goes so far beyond the abilities of someone to reason about the program it's not funny.

This is the problem with tri-value logic. Look at how SQL handles NULL, for example. By definition, " NULL || !NULL = NULL ".

Although " b || !b " should translate into " known or not known " which one would expect to be true, depending on your definition of "not known", or NULL, or uninitialised ...

Cheers,
Wol

Footguns

Posted Jul 11, 2021 16:10 UTC (Sun) by khim (subscriber, #9252) [Link]

> Although " b || !b " should translate into " known or not known " which one would expect to be true, depending on your definition of "not known", or NULL, or uninitialised ...

It's C, not SQL. And valid int doesn't have a value which can lead to this three-way logic (float does, BTW, I wouldn't be much surprised to see the exact same example with floats: (f == 0.0) || (f != 0.0) can be false in a program without any UB).

They had to specifically add said trilogic to the code and introduce special “poison” (the name is telling, isn't it?) value to the set of “normal” int values to achieve that effect.

This doesn't look like a “mere accident” to me, more like an act of sabotage.

P.S. GCC does such optimizations a bit differently. Instead of treating undefined value as special “poison” value with trilogic it just assigns arbitrary value to it and then does the usual constant propagation and other such optimizations. This produces almost the same speedup without making user angry.

realloc

Posted Jul 12, 2021 12:37 UTC (Mon) by tialaramex (subscriber, #21167) [Link] (12 responses)

> Let's go to that infamous realloc example. Can you show me what kind of sanitizer should I use to catch error in it?

I genuinely don't know what sanitizer would best help you avoid getting this wrong. I had to stare at this code for some time to even discern what its author might have expected it to do beyond "Not work".

> Can you even explain why compiler was allowed to provide the output it does?

I can try.

After calling malloc p is a pointer to some uninitialized memory big enough to store an int (or it's NULL, but we'll ignore this scenario). After calling realloc p is still definitely a pointer BUT we need to examine q to know whether p still points _to_ anything. If q is NULL, p still points to that same memory (and of course this NULL pointer in q isn't pointing at anything), however if q is not NULL, p has been free'd and now doesn't point to anything (even though its bit pattern might seem to suggest otherwise) while q is now pointing to uninitialized memory big enough to store an int.

Then the code tries to compare p and q to see if they're "equal", if p and q are both NULL that's fine (and true) although what's about to happen next is a bad idea. If either of them isn't NULL, this comparison is invalid because they definitely aren't pointing at the same object.

Having concluded that p and q are "equal", it tries to store different values to these pointers. The compiler is under no obligation to actually implement this by writing values to RAM at addresses stored in p and q, and as it is sure that they aren't pointing at the same object (one of them isn't pointing at anything at all), it needn't worry whether in the programmer's imagination this should be overwriting the same value.

This code repeatedly dereferences a pointer that was free()d. It is not surprising that it doesn't always do what you wanted.

> Why does it even say that it may have the same value as a pointer to the old object if I couldn't use that fact for anything?

The copy of K&R I own doesn't say this, but the Linux manual pages do. I imagine they were hoping to prevent confusion, by assuring you that it's possible you'll get the "same" value back and not only "different" values and so you shouldn't try to conclude anything from that. Alas as so often they've instead made things worse by persuading you that you could try to compare these values when in fact you can't do that.

realloc

Posted Jul 12, 2021 17:21 UTC (Mon) by khim (subscriber, #9252) [Link] (11 responses)

> The copy of K&R I own doesn't say this, but the Linux manual pages do.

C standard says that. It should be enough. Besides compiler developers have said long ago they don't care about anything else.

> I imagine they were hoping to prevent confusion, by assuring you that it's possible you'll get the "same" value back and not only "different" values and so you shouldn't try to conclude anything from that.

I don't think so. Let's look on the original description in the C89:

The realloc function changes the size of the object pointed to by ptr to the size specified by size. The contents of the object shall be unchanged up to the lesser of the new and old sizes. If the new size is larger, the value of the newly allocated portion of the object is indeterminate. If ptr is a null pointer, the realloc function behaves like the malloc function for the specified size. Otherwise, if ptr does not match a pointer earlier returned by the calloc, malloc, or realloc function, or if the space has been deallocated by a call to the free or realloc function, the behavior is undefined. If the space cannot be allocated, the object pointed to by ptr is unchanged. If size is zero and ptr is not a null pointer, the object it points to is freed.

The realloc function returns either a null pointer or a pointer to the possibly moved allocated space.

No funny business with objects which are deallocted then allocated in the same place. And phrasing “possibly moved allocated space” definitely sounds as if “object was left untouched” is normal case while “object was moved” as an exception. Which matches the behavior of early allocators, BTW.

And phrase The realloc function returns a pointer to the new object (which may have the same value as a pointer to the old object), or a null pointer if the new object has not been allocated is just a translation of that “easy to understand English” to modern “incomprehensible standartize English”.

> After calling realloc p is still definitely a pointer BUT we need to examine q to know whether p still points _to_ anything.

No we don't. You are not supposed to even look on pointer which no longer points to valid object: attempt to use the value of a pointer that refers to space deallocated by a call to the free or realloc function isused triggers not just any random simple “bounded undefined behavior” but actually triggers “unbounded undefined behavior”. Yet standard explicitly says that value passed to realloc and returned from realloc may be the same. And yes, it's not just “pass NULL, get NULL back” — that would mean entirely different thing than “possibly moved allocated space”.

Also. Pointer provenance is not a thing and that's how pointer comparison works, according to C18:

Two pointers compare equal if and only if both are null pointers, both are pointers to the same object(including a pointer to an object and a subobject at its beginning) or function, both are pointers to one past the last element of the same array object, or one is a pointer to one past the end of one array object and the other is a pointer to the start of a different array object that happens to immediately follow the first array object in the address space.

List is an exhaustive. There are no provisions for two pointers being equal except that tiny loophole about one past the end of one array object. Some compiler authors tried to say to me that it's exactly what happens but that's clear attempt to just invent some plausible justification for harmful and pointless compiler behavior.

> even though its bit pattern might seem to suggest otherwise

Sorry, but no. Standard currently includes exactly and precisely one case where pointers with the same bit patterns may be treated differently: when you have two adjacent arrays and one pointer is pointer one past the end of one array object and another is pointer to the start of a different array object that happens to immediately follow the first array object in the address space.

That's it. No other cases are allowed.

> I genuinely don't know what sanitizer would best help you avoid getting this wrong.

The correct answer: none of them. Sanitizers only catch tiny subset of undefined behaviors — and ubsan, in particular, mostly catches UBs which shouldn't be UBs in the first place.

realloc

Posted Jul 12, 2021 17:43 UTC (Mon) by smurf (subscriber, #17840) [Link] (1 responses)

> The realloc function …

and its definition in the standard is irrelevant here. The simple reason for this is that there is no way to even *tell* the compiler that the pointer passed into realloc is invalid if (and only if) realloc returns NULL. Thus it has no business whatsoever blithely ignoring the pointer comparison; you could call a "foobar" function that does exactly the same thing.

Would you deny that it's a valid optimization to not update pointers to an object if it happens not to have been moved? same thing.

The comparison after realloc tells the compiler that p and q are aliased to the same location. It bloody well should listen to that if it is supposed to not break existing code. Doing that is not rocket science. Anyway, please explain how the fact that a piece of code affects program flow but not the optimizer can possibly be anything but a bug. It's not as if the C standard forbids aliased pointers. In fact there's the "restrict" keyword which tells it that there's no danger of aliasing, so why does it assume so in that keyword's absence?

The claim that comparing these two pointers can possibly be illegal makes no sense whatsoever IMHO, and I very much doubt that that was the intent of the C standards people. They might be misguided but they're not *that* stupid.

realloc

Posted Jul 12, 2021 18:39 UTC (Mon) by khim (subscriber, #9252) [Link]

> The simple reason for this is that there is no way to even *tell* the compiler that the pointer passed into realloc is invalid if (and only if) realloc returns NULL.

It's done in the compiler sources. The same way as with memcpy. You can even disable it the exact same way, by specifying -fno-builtin-realloc. And then, of course, everything works.

> Thus it has no business whatsoever blithely ignoring the pointer comparison; you could call a "foobar" function that does exactly the same thing.

That's MSVC. Don't mix them. Clang, at least, looks on the function name…

> Would you deny that it's a valid optimization to not update pointers to an object if it happens not to have been moved?

Indeed. But you would be naive you C compiler developers agreed. When I talked about this I was assured that it's absolutely not possible to do that. And if object is actually moved you can not traverse it and update insider pointers, too.

Instead you have to convert all insider pointers into indexes, store these indexes somewhere (but you can use unions to save space so could just copy then into uintptr_t which resides in the exact same place) and then turn indexes back into pointers (properly adjusted now).

And no, it's not a joke.

> It bloody well should listen to that if it is supposed to not break existing code.

Irrelevant (according to compiler developers). Existing code must be fixed as described above. The fact that it may force it to become slower (and much slower in some cases) is irrelevant, again. Unless it's part of some well-known benchmark, of course.

> It's not as if the C standard forbids aliased pointers.

C standard says something which is, as Linus would would say, is “total and utter crap”. On one hand it says this:

Two pointers compare equal if and only if both are null pointers, both are pointers to the same object (including a pointer to an object and a subobject at its beginning) or function,both are pointers to one past the last element of the same array object, or one is a pointer to one past the end of one array object and the other is a pointer to the start of a different array object that happens to immediately follow the first array object in the address space¹⁰⁹).

But if you notice there's a footnote ¹⁰⁹). It says this:

Two objects may be adjacent in memory because they are adjacent elements of a larger array oradjacent members of a structure with no padding between them, or because the implementation choseto place them so, even though they are unrelated. If prior invalid pointer operations (such as accesses outside array bounds) produced undefined behavior, subsequent comparisons also produce undefined behavior.

If you read it carefully you will realize that since accessing one-past-end-element is forbidden it also means that what the main body of the standard says is just not possible… but then why is it there?

The answer: C compiler developers got an approval from C standards committee to add what they call “pointer provenance” to the C standard but since in 15 years they still were unable to invent sane rules which people would be able to understand… they only managed to add that one tiny footnote.

And they were unable to put anything else to the standard because C committee (unlike C++ committee) actually cares about correctness and just doesn't want to add complete an utter garbage to the standard (you can read about why it's complete and utter garbage here). > The claim that comparing these two pointers can possibly be illegal makes no sense whatsoever IMHO, and I very much doubt that that was the intent of the C standards people. They might be misguided but they're not *that* stupid.

True. They resisted more-or-less succesfully with both C11 and C18. But C/C++ compiler developers are that stupid. This now they are trying again.

And what is their reasoning, pray tell? Why, of course: existing compilers already rely on the properties and thus standard have to be changed!

I would have been somewhat sympathetic if the exact same prompt by C/C++ users WRT other controversial parts of the standard would not have been meet with derision and prompts to burn the heretics rewrite all the programs.

realloc

Posted Jul 12, 2021 18:40 UTC (Mon) by excors (subscriber, #95769) [Link] (1 responses)

> And phrase "The realloc function returns a pointer to the new object (which may have the same value as a pointer to the old object), or a null pointer if the new object has not been allocated" is just a translation of that “easy to understand English” to modern “incomprehensible standartize English”.

I think the root of the problem is that the "easy to understand" version of the C standard was ambiguous and contradictory. People writing C programs interpreted it one way (or one of several ways, depending on which paragraphs they read). People writing C compilers interpreted it a different way. That was a failure of the language specification and needed to be fixed.

To fix the specification, they needed to come up with something closer to a formal specification of the language's semantics. In the ambiguous cases there was a choice of specifying the behaviour that users expected, vs specifying the behaviour that compiler developers expected. But I suspect all users' mental models of the language's semantics are not logically consistent - they'll say "example A should obviously output X" and "the compiler should obviously be able to optimise example B into assembly code Y" even though it's impossible for both to be true at once - because they've never had to think about the gnarly edge cases. That's not an adequate basis for designing a specification.

Compiler developers' mental models were a lot more complex, but also a lot closer to being logically consistent and complete, because they had already thought about most of those edge cases and invented new concepts and terminology so they could implement it in a way that balanced users' expectations of correctness and performance. Maybe it wasn't the best balance, but at least it was based on a coherent set of rules that could be reasoned about. The language specification can take those concepts and rules and formalise them, so it can provide a single correct answer to the question of what a piece of code should do, and that answer is not incompatible with the optimisations that users expect their compiler to do. So it's inevitable that the specification grew into being a compiler developer's interpretation of the language, full of weird concepts and terminology that make it almost incomprehensible to regular users, because that's the only way to formalise something that grew informally. It's not good, but it's better than leaving it ambiguous.

realloc

Posted Jul 12, 2021 19:22 UTC (Mon) by khim (subscriber, #9252) [Link]

> Compiler developers' mental models were a lot more complex, but also a lot closer to being logically consistent and complete, because they had already thought about most of those edge cases and invented new concepts and terminology so they could implement it in a way that balanced users' expectations of correctness and performance.

No. It was neither consistent nor complete. That's why they weren't able to add pointer provenance neither to C11 nor to C18. The latest attempt is here. Please note the dates.

> Maybe it wasn't the best balance, but at least it was based on a coherent set of rules that could be reasoned about.

Nope. Not even close. Kinda-sorta coherent models were developed (here is one attempt) — but they don't match what the compilers are actually doing (the new model requires a handful of problematic IR-level optimizations to be removed, but it also supports the addition of new optimizations that were not previously legal is pretty telling, isn't it?) and, more importantly, it's not clear why they are supposed to be applicable to C89 or even C99 programs.

> So it's inevitable that the specification grew into being a compiler developer's interpretation of the language

It haven't done that yet. Pointer provenance is not a thing — even in C18 version of C standard.

> It's not good, but it's better than leaving it ambiguous.

Good for whom? Compiler writers? Maybe. It's bad for C users, definitely, but since they couldn't present anything better they had only one option: accept the standard or live without a compiler.

But now it looks as if accepting standard is not enough. That is the problem.

When the story was presented as “Semi-portable C” vs “Standard C” this sounded like a sensible compromise: sure standard maybe harsh is some places, but hey, it's a standard!

Now, then we know that compiler developers don't feel themselves restricted by requirements of the standard the question arises: why then C users demands to change the standard are ignored? Hmm?

realloc

Posted Jul 12, 2021 21:44 UTC (Mon) by tialaramex (subscriber, #21167) [Link] (4 responses)

The insistence that "Pointer provenance is not a thing" appears to be a false claim about reality. As you have noticed, programs written with your belief do not necessarily work. I think you've mentioned Defect Report #260 previously. Just as C programmers are often too clever for their own good, sometimes the standards committee is likewise and that's reflected in #260. This also happened with the Memory Model, C++ 17 "temporarily discouraged" use of consume ordering because eh, it seemed like a good idea when it was invented and now it does not.

realloc

Posted Jul 12, 2021 22:25 UTC (Mon) by khim (subscriber, #9252) [Link] (3 responses)

> The insistence that "Pointer provenance is not a thing" appears to be a false claim about reality.

What kind of reality?

> As you have noticed, programs written with your belief do not necessarily work.

Yes. Compilers are broken. And now we even know there broken not as an accident. So?

> Just as C programmers are often too clever for their own good, sometimes the standards committee is likewise and that's reflected in #260.

Indeed. Instead of throwing away premature and unfinished ideas about how the so-called “pointer provenance” have to work they acquiesced to compiler writers demands (or, more likely, an ultimatum, but I wasn't there and couldn't say for sure). Yet instead of saying that standard is wrong they just “came to a number of conclusions as to what it would be desirable for the Standard to mean”. IOW: they proposed that compiler developer would start developing some sane set of rules for the C users to follow.

Instead of doing that compiler developers went on to invent unproven, buggy and half-specified schemes which were, as they claim, permitted by DR260. Indeed even latest proposal from last year explicitly says DR260 CR has never been incorporated in the standard text and lament about the sad fact that DR260 only explicitly permits track the origins of a bit-pattern and [...] may also treat pointers based on different origins as distinct even though they are bitwise identical

Which basically means: output from that realloc example is still buggy. Even if you would incorporate DR260 resolution into the standard — you still couldn't get output 1 2 in question. The most you can get is elimination of comparison and the whole code which does operations after that branch.

Now, you may try to argue that compiler was clever enough to notice that the only situation where p is equal to q and both are valid… nope, no cigar

> This also happened with the Memory Model, C++ 17 "temporarily discouraged" use of consume ordering because eh, it seemed like a good idea when it was invented and now it does not.

Yes, but at least there some memory model was actually accepted and added to the text of standard. Nothing like that happened with DR260.

> it seemed like a good idea when it was invented and now it does not.

And I would argue that “pointer provenance” was a very bad idea and shouldn't brought to the realms of C as unconditional property. Previous proposal at least tried to acknowledged that and proposed two modes, test macros and so on.

But it was rejected. Most likely because it was “too much work” to implement.

So… it's too much work for the compiler writers to implement -fno-provenance switch yet reviewing and rewriting billions lines of code to make them compliant with new “optimizations” (which break code which was acceptable for decades) is not “too much work”? WTH?

realloc

Posted Jul 13, 2021 13:13 UTC (Tue) by tialaramex (subscriber, #21167) [Link] (2 responses)

> What kind of reality?

This kind. Where your program doesn't do what you expected.

C and C++ are ISO standards. But, ISO standards are just written documents. One of the insights of the IETF is appropriate here. If there's a standards document which says X, but everybody does Y, then X is not in fact the standard. But worse than that, the standards document might (hopefully inadvertently) say that 2+2=5, and even if everybody tries very hard to obey that standard they can't.

For some of the ISO standards the difference between what the text says, what it is understood to mean, and what is practical, is negligible. ISO 216 A-series paper sizes are simple enough that this works.

But C++ is very far from that. So, the C++ standards document says words, but in some cases those words turn out to be incoherent nonsense (like 2+2=5) as happened for the Memory Model. In other cases, as here with pointers, the words imply that C++ is a language whose correct implementation has terrible performance. But nobody wants terrible performance (sometimes in this sort of forum they'll _say_ they want terrible performance, but then immediately they demand a way to "opt out" and they never switch it off) so what you'll actually get is not that language.

For years Java had to pretend there was a special "less strict" interpretation of how floating point numbers work distinct from how they're documented in the actual language standard, which was optional and might be (read: was) switched on for some (read: Intel architecture) platforms. Eventually the terrible Intel x87 FPU was obsolete and Java removed this "feature" entirely. It was a necessary evil, until it wasn't, and then it was gladly killed.

Anyway, not only do you have the problem that C++ as-it-is-compiled is not the written standards document, because rooms full of people translating your C++ into assembly language would be a horror show -- but worse, the optimizers, which you can't live without, do not actually optimise C++. They have an Intermediate Representation and optimize that.

One of the things that makes some people unhappy about Rust is that it doesn't have a written standard. If you've got an unjustifiable faith in the power and correctness of standards documents this can feel like a big obstacle. But in truth Rust's bigger obstacle isn't the lack of a standards document for Rust the programming language, but for the LLVM IR. Everybody is agreed that Rust has different semantics from C++ and so it needs to express those in the LLVM IR. Famously for example infinite loops aren't a thing in C++ (forward progress is mandatory, sometimes the compiler can't be sure if there is progress and so the running program is actually an infinite loop, but if it knew that it would elide the loop entirely) so Clang needn't express "this is an infinite loop" because there aren't any in C++ whereas in Rust they're a thing, so rustc needs to express that and have LLVM produce correct code for an infinite loop. But, since the IR is not formally standardised, there is no analysis which says the optimisations actually work on the IR, only that they seem to work for C++. Sometimes a C++ programmer will optimise the IR in a way that deletes Rust's infinite loop. Oops.

realloc

Posted Jul 13, 2021 14:41 UTC (Tue) by khim (subscriber, #9252) [Link]

> If there's a standards document which says X, but everybody does Y, then X is not in fact the standard. But worse than that, the standards document might (hopefully inadvertently) say that 2+2=5, and even if everybody tries very hard to obey that standard they can't.

Except that's not what I hear when I bring the question of overflowing shift or nullptr arithmetic.

When I bring this up and ask for some sanity (like: I don't care what i << 32 would be as long as (i << 32) & 0 is still 0) the answer is always the same: “holy standard” proclaimed that's an UB, thus go away and not bother us with triflities, fix your program instead (it's usually more polite, but idea is always the same).

I'm more than willing to discuss things like this with Rust developers because they:

  1. Explain their decisions in plain English.
  2. Offer me an alternative which I may use instead.

I would have been willing to discuss that with C/C++ developers if they wouldn't have brought sayings from “holy standard” into discussions about how should I deal with mmap or shift-by-32 but treated these questions like IETF (or Rust developers) treat.

At times it felt like I'm talking with devout Jews who are believing that all the answers can be found in Torah. It looked somewhat acceptable (if tiring after same time) if they were actually devout believers and actually tried to follow their “holy scripture”.

But when they started talking about pointer provenance and brought DR260 which is 2004 year decision not incorporated into their “holy scripture” in the course of last 15 years (and two published standards)… I have realized the depth of hypocrisy. They were knowingly breaking standard-compliant programs for more than decade while simultaneously preaching standard as the source of truth.

> But C++ is very far from that.

Maybe, but that's not what C/C++ compiler developers say usually. You can't have you cake and eat it too. Well… you can try but when you would be, eventually, caught, you would lose what little trust you had.

> For years Java had to pretend there was a special "less strict" interpretation of how floating point numbers work distinct from how they're documented in the actual language standard, which was optional and might be (read: was) switched on for some (read: Intel architecture) platforms. Eventually the terrible Intel x87 FPU was obsolete and Java removed this "feature" entirely. It was a necessary evil, until it wasn't, and then it was gladly killed.

But note that both Strict or Nonstrict Floating-Point Arithmetic was described in books and present from the beginning. It was nothing like what C/C++ compiler developers are trying to do with their “we don't even yet know themsleves the rules in year 2020 but let's pretend they were already there in C in 1989 and demand that C users obey them”.

> If you've got an unjustifiable faith in the power and correctness of standards documents this can feel like a big obstacle.

I don't. But C/C++ compiler developers do. Except when it doesn't suit them. That is the problem.

Basically: “if compiler user violated the standard and program was miscompiled then it's fault of said user and there would be no lenience” simultaneously “if compiler violated the standard and program was miscompiled then it's fault of the standard and standard (and not the compiler) would be fixed” stance is flat out unacceptable.

> But in truth Rust's bigger obstacle isn't the lack of a standards document for Rust the programming language, but for the LLVM IR.

Well… LLVM IR simultaneously with the need for optimizations. Rust without optimizations is not viable which means that for the foreseeable future it would have to consider limitations of LLVM. That's serious problem but as long as Rust developers are trustworthy and don't adopt the C/C++ developers “it's my way or the highway” stance it's manageable.

> Sometimes a C++ programmer will optimise the IR in a way that deletes Rust's infinite loop. Oops.

Oops indeed. But I think if Rust would adopt the stance similar to what Java had as explicitly say: “here is what may happen because we rely on the untrustworthy LLVM” then people would accept that.

Maybe some time down the road Rust would be big enough to afford completely separately compiler and these warts could be fixed for real, but noone expects them to do the impossible. Because they in turn don't demand the impossible from compiler users.

In fact Rust developers tell that explicitly: we need better language specs. They admit the fact that unsafe Rust is underspecified and they couldn't actually explain how and what can be done in it safely.

But for them it's a problem. They don't pretend that everything's peachy, Rust users would follow the rules even we couldn't even write these rules themselves.

On the other hand C/C++ compiler developers made standard text sacred by constant insistence that we should ignore everything and anything when we talk about C/C++ programs (“POSIX, x86/ARM/RISC-V architecture specs and so on are all irrellevant and must be ignored, if you want to change even one jot your have to change the standard first” was their persistent public stance for years).

realloc

Posted Jul 13, 2021 23:12 UTC (Tue) by foom (subscriber, #14868) [Link]

FWIW, the handling of infinite loops in LLVM IR was codified late last year with the addition of the "mustprogress" function and loop attribute in LLVM IR, whereby languages which want to require forward progress must explicitly opt into the requirement. ("forward progress" being defined by the C and C++ standards, e.g. https://eel.is/c++draft/intro.progress for C++).

Pointer provenance

Posted Jul 21, 2021 19:32 UTC (Wed) by mathstuf (subscriber, #69389) [Link] (1 responses)

I'll note these two papers for the C++ committee which describe and propose resolutions to the provenance issue (for C++):

P1726R5 (describing the problem)
P2414R0 (proposed solutions)

I believe they are publicly available, but in an abundance of caution, I'll leave them unlinked in case they are not (but those with access should be able to find them given these IDs).

Pointer provenance

Posted Jul 22, 2021 2:14 UTC (Thu) by foom (subscriber, #14868) [Link]

Footguns

Posted Jul 15, 2021 21:04 UTC (Thu) by mrugiero (guest, #153040) [Link] (12 responses)

> Why does it even say that it may have the same value as a pointer to the old object if I couldn't use that fact for anything?

One pointer is valid and the other isn't because of the semantics of reallocating memory. I think that part is reasonable, pointers are unrelated and one, from the point of view of the user, has been freed. However, the runtime can use the fact that their value may be the same: malloc will often have arenas of fixed size regions (depending on implementation, of course); your allocated memory may come from an area bigger than the size you requested or, as in your example, be the same size. You may also be trying to shrink, so the contents of the new region can fit the old region anyway. Explicitly allowing the value of the pointer to be the same is what allows this, avoiding looking for another chunk of memory and copying the contents of your previous buffer to a new one. And in this case the performance win could be significant, a lot of processing goes just into copies in some workloads.

Footguns

Posted Jul 15, 2021 22:32 UTC (Thu) by khim (subscriber, #9252) [Link] (5 responses)

> I think that part is reasonable, pointers are unrelated and one, from the point of view of the user, has been freed.

Except there are no such thing in C as “pointers that are equal yet one is valid and the other is not”. Except for one case where you have no right to even attempt to compare them.

Here is the full exhaustive list from latest draft (identical to C18):

Two pointers compare equal if and only if both are null pointers, both are pointers to the same object(including a pointer to an object and a subobject at its beginning) or function, both are pointers to one past the last element of the same array object, or one is a pointer to one past the end of one array object and the other is a pointer to the start of a different array object that happens to immediately follow the first array object in the address space¹¹¹)

Some Clang developers even tried to claim that after call to realloc “old pointer” have become pointer to “one past the end of the array”, but no, C18 removed all doubts but adding that note:

Two objects may be adjacent in memory because they are adjacent elements of a larger array or adjacent membersof a structure with no padding between them, or because the implementation chose to place them so, even though they are unrelated. If prior invalid pointer operations (such as accesses outside array bounds) produced undefined behavior, subsequent comparisons also produce undefined behavior.
> However, the runtime can use the fact that their value may be the same

I don't care about what runtime does. It's not really important here. I only care about what standard says:

The realloc function returns a pointer to the new object (which may have the same value as a pointer to the old object), or a null pointer if the new object has not been allocated.

See? The fact that I can compare these two pointers and they can be identical is spelled there explicitly. It's part of the standard text, not something I may want to see there. And then, when standard talks about pointer equality it explains what that does mean: it means that they either point to the same object or, alternatively, both of them are pointers to one past the end. One additional possibility is not really allowed because it would trigger UB which would make explicit permission to compare these two pointers invalid.

Footguns

Posted Jul 17, 2021 15:54 UTC (Sat) by mrugiero (guest, #153040) [Link] (4 responses)

> Except there are no such thing in C as “pointers that are equal yet one is valid and the other is not”. Except for one case where you have no right to even attempt to compare them.
I'm not talking about how C defines them on the letter, I'm talking about what's reasonable. You realloc something, your chunk of memory is not the same from the POV of your application. I'm talking about the high level in the quoted part. It can be argued that a pointer just points to memory and is thus just the number, but I don't think it's entirely clear that it has to be just the number.

> Some Clang developers even tried to claim that after call to realloc “old pointer” have become pointer to “one past the end of the array”, but no, C18 removed all doubts but adding that note:
Yeah, that sounds just like mental gymnastics. Just admit you're making an exception for performance, there's nothing in the semantics of realloc that make you think somehow it becomes "one past the end of the array".

Two objects may be adjacent in memory because they are adjacent elements of a larger array or adjacent membersof a structure with no padding between them, or because the implementation chose to place them so, even though they are unrelated. If prior invalid pointer operations (such as accesses outside array bounds) produced undefined behavior, subsequent comparisons also produce undefined behavior.

> I don't care about what runtime does. It's not really important here. I only care about what standard says:

> The realloc function returns a pointer to the new object (which may have the same value as a pointer to the old object), or a null pointer if the new object has not been allocated.

> See? The fact that I can compare these two pointers and they can be identical is spelled there explicitly. It's part of the standard text, not something I may want to see there. And then, when standard talks about pointer equality it explains what that does mean: it means that they either point to the same object or, alternatively, both of them are pointers to one past the end. One additional possibility is not really allowed because it would trigger UB which would make explicit permission to compare these two pointers invalid.

I see. My understanding of your claim was precisely that the standard said something about the lines even tho the numeric value (emphasis in numeric) may be the same, the pointers were not equal. That would allow to make that distinction. But the fact the value can be the same is most likely because that allows the runtime to allocate extra space the first time and reuse the same chunk. Most often than not, the standard defines things thinking about how it'll help implementors and users (maybe not so much recently). But yeah, it says "value", not "numeric value", which wouldn't imply they are equal. Like, (uintptr_t)old == (uintptr_t)new but old != new.

Footguns

Posted Jul 17, 2021 16:09 UTC (Sat) by khim (subscriber, #9252) [Link] (3 responses)

> You realloc something, your chunk of memory is not the same from the POV of your application.

Why is not not the same? On the contrary: it is the same. I have verified that.

> I'm talking about the high level in the quoted part.

And I'm talking about explicit mention about the possibility of in pointer being the same as out pointer. Why is it there? What can one do with that information? How can I use it?

I assume that standard writers weren't insane and haven't been adding some notes just to make standard bigger and more complex. So what's the reason to include that note as part of the interface?

> My understanding of your claim was precisely that the standard said something about the lines even tho the numeric value (emphasis in numeric) may be the same, the pointers were not equal.

But these are just wet dreams of C/C++ compiler developers. Right now standard doesn't allow for the possibility of two pointers being equal and comparable and yet them being different.

One concession which standard did in C11 was to declare some pointer comparisons as being UB (e.g. you are not supposed to compare one-past-the-end-pointer to “normal” pointer… that comparison is UB).

> Just admit you're making an exception for performance, there's nothing in the semantics of realloc that make you think somehow it becomes "one past the end of the array".

Except to claim this they would need to show at least one real-world example where it have improved performance. So far nothing was shown thus we may safely assume all that code was added specifically to break otherwise good programs and there are no other reason.

Footguns

Posted Jul 18, 2021 3:32 UTC (Sun) by mrugiero (guest, #153040) [Link] (2 responses)

> Why is not not the same? On the contrary: it is the same. I have verified that.
For a start, because for your application they don't (usually) have the same size.

> And I'm talking about explicit mention about the possibility of in pointer being the same as out pointer. Why is it there? What can one do with that information? How can I use it?
> I assume that standard writers weren't insane and haven't been adding some notes just to make standard bigger and more complex. So what's the reason to include that note as part of the interface?
Again, because that allows runtime writers to reduce the cost of calling realloc, that's why it's mentioned. But you're right in that clang breaks this rule, I think I mentioned it in the quoted comment.

> But these are just wet dreams of C/C++ compiler developers. Right now standard doesn't allow for the possibility of two pointers being equal and comparable and yet them being different.
Yep, I got that.

> One concession which standard did in C11 was to declare some pointer comparisons as being UB (e.g. you are not supposed to compare one-past-the-end-pointer to “normal” pointer… that comparison is UB).
I always thought that was there for C89 already. The more you know.

> Except to claim this they would need to show at least one real-world example where it have improved performance. So far nothing was shown thus we may safely assume all that code was added specifically to break otherwise good programs and there are no other reason.
Indeed. But saying that is already an improvement compared to just intentionally misinterpreting the standard. At least it's a lie that the infinite sea of possibilities makes impossible to really disprove.
Regarding intention, I addressed in a different comment that there's a lot of other possibilities that can explain bad decisions.

Sorry for answering everything even when I'm just acknowledging I agree with you now that I understand better what you say. It just seemed rude not to.

Footguns

Posted Jul 19, 2021 16:55 UTC (Mon) by Wol (subscriber, #4433) [Link]

> > Why is not not the same? On the contrary: it is the same. I have verified that.

> For a start, because for your application they don't (usually) have the same size.

????

Not particularly elegant programming, but it's quite possible that a programmer (a) knows how much space he needs but (b) does not know how much space he's got. So he does a realloc for "safety's sake". The data portion of a linked list, for example. If the majority of your data elements are the same size that could easily result in a lot of reallocs of the same size.

(If I want to throw away the current value in the list, and replace it with a new one, a safety realloc is much quicker than parsing the value to find out whether I need to realloc ...)

Cheers,
Wol

Footguns

Posted Jul 19, 2021 17:38 UTC (Mon) by khim (subscriber, #9252) [Link]

> I always thought that was there for C89 already. The more you know.

Nope. It was only added in C11. And that was done sloppily: main body of text still says that says that “pointers are equal only if they point to the same object but since one-past-the-end pointer doesn't point to any object it may be identical to the other pointer which points to actual object”… then footnote (added in C11 only) turns that logic on it's ear and says “hey, but you are not supposed to compare pointer which doesn't point to anything to another pointer which points to something”.

I think you are confused about different place where standard says about pointers ordering. Indeed, pointer arithmetic and < or > pointer comparisons are only defined for pointers which point to the elements of the same array, but equality/unequality couldn't work like that or else you would make many complex data structures (like Linus list with root node instead of nullptr) invalid.

Actually, if you think about it, C89 definition contradicts even MS-DOS memory model where 0x0040:0x0000 and 0x0000:0x0400 pointers point to the exact same memory yet are not identical (but then MS-DOS compilers included huge memory model where they were conformant and other memory models were easily understood by programmers even if they were not 100% C89 conformant).

Footguns

Posted Jul 15, 2021 22:58 UTC (Thu) by khim (subscriber, #9252) [Link] (5 responses)

> And in this case the performance win could be significant, a lot of processing goes just into copies in some workloads.

That's another interesting question: what kind of workload can that “optimization” make faster? What application? What benchmark?

Yes, I think you may try to invent some kind of benchmark which would become faster, but then, it's much easier to invent something which would become slower if you do crazy turn-pointers-into-indexes-then-call-realloc-then-turn-them-back-into-pointers dance, and I quite literally know exactly zero programs which become faster or smaller. Clang developers were unable to present anything either, they were too busy inventing excuses.

Basically AFAICS this change does exactly and precisely two things:

  1. It miscompiles some [rare] real-world programs and makes them incorrect.
  2. Changes which it imposes on C developers almost always make programs slower and never, not for any real-world application make them faster.

Thankfully this particular “optimization” can be easily disable in clang with -fno-builtin-realloc thus it's effect is easy to benchmark.

I sincerely hope it wasn't done with an explicit goal to make C developers life miserable (this would imply C compiler developers are real sadists which is disturbing discovery if true), but I couldn't see any other reason for that “optimization” to exist. Maybe I don't understand something.

Footguns

Posted Jul 17, 2021 16:03 UTC (Sat) by mrugiero (guest, #153040) [Link] (4 responses)

> That's another interesting question: what kind of workload can that “optimization” make faster? What application? What benchmark?
realloc heavy loads. You append quite a lot to an array. realloc can be clever and allocate exponentially for bigger buffers. That is much faster. I know that much by experience, by having to mitigate a performance bug where someone made an array of key-values rather than make a hashmap. I didn't have the time to fix it properly, so I both appended where I knew the key wasn't previously defined and used exponential alloc.

> Yes, I think you may try to invent some kind of benchmark which would become faster, but then, it's much easier to invent something which would become slower if you do crazy turn-pointers-into-indexes-then-call-realloc-then-turn-them-back-into-pointers dance, and I quite literally know exactly zero programs which become faster or smaller. Clang developers were unable to present anything either, they were too busy inventing excuses.
No, because provenance doesn't generally propagate to runtime. It's something the compiler uses to try to be more clever than it should, apparently, not some kind of instrumentation.

> It miscompiles some [rare] real-world programs and makes them incorrect.
We agree that's the current use case, yes. But that doesn't have a lot to do with the concept of provenance. Also, I have yet to see an example where you would want to make that comparison. Yes, if you do, it misbehaves. Why did you do it in the first place?
> Changes which it imposes on C developers almost always make programs slower and never, not for any real-world application make them faster.
Casting is literally free on runtime. It's an annoyance, yes, but it doesn't affect runtime in any reasonable implementation.

> Thankfully this particular “optimization” can be easily disable in clang with -fno-builtin-realloc thus it's effect is easy to benchmark.
I see the same code in both sides. So no problem?

> I sincerely hope it wasn't done with an explicit goal to make C developers life miserable (this would imply C compiler developers are real sadists which is disturbing discovery if true), but I couldn't see any other reason for that “optimization” to exist. Maybe I don't understand something.
This is just paranoia. You had good points before this, I don't intend this as an ad hominem. But you should probably keep it in check, from a person with a mental illness to one who may or may not have one.

Footguns

Posted Jul 17, 2021 17:36 UTC (Sat) by khim (subscriber, #9252) [Link] (3 responses)

I think we are talking past each other. When I'm asking about “optimization” I'm not talking about realloc implementation.

I know when realloc can leave things untouched and why. That's not the interesting case.

The interesting case is an “optimization” enabled with -fbuiltin-realloc. The one which forbids user to compare p and q and says that you can't use pointers to the “old” realloced object. What does that one improve?

> It's something the compiler uses to try to be more clever than it should, apparently, not some kind of instrumentation.

Precisely. And now I can't just do a simple q == p check to see if object is unmoved. I have to, basically, always do p = q assignment (if q is not nullptr) but what's more important: I can't keep pointers to “old” realloced object anywhere. If I have some other pointers (besides p) and, especially, if I have “internal” pointers (which point to the inside of that object) then I have to turn them all into indexes and then traverse everything after realloc again and turn these indexes back to pointers!

Now, I can easily imagine where such “compiler appease dance” can introduce slowdowns (even if realloc is clever and allocates exponentially for bigger buffers, etc).

But where the heck that dance imposed on me by C compiler developers helps? What kind of program? What benchmark? Can you show me anything at all where -fbuiltin-realloc is faster than -fno-builtin-realloc?

> But that doesn't have a lot to do with the concept of provenance.

It has everything to do with the concept of provenance. What -fbuiltin-realloc option does is tells the compiler: “if someone does q = realloc(p, …) then you can assume that p and q have different provenance, they could never alias (not even after explicit p == q check) and you can optimize on basis of that”.

The one thing (and very big thing for me): WTH can you use that assertion for? What kind of code should we observe for that rule to speed up anything?

> Casting is literally free on runtime. It's an annoyance, yes, but it doesn't affect runtime in any reasonable implementation.

Casting itself is free. But because new “optimized” rules demand that you find and convert all pointers before you call realloc you would need additional code which would traverse additional data structures and scans all these pointers.

And after realloc you have to traverse all these data structures again and turn indexes back into pointers.

Not only that adds additional code which complicates logic, there are no guarantee that this code would be optimized away by the compiler (and in some cases, when external functions are involved, it couldn't be removed).

> I see the same code in both sides.

It's not the same. Version with -fbuiltin-realloc does this:

 mov esi,0x1
 mov edx,0x2
 xor eax,eax
 call 401030 <printf@plt>

Version with -fno-builtin-realloc does this:

 mov esi,0x2
 mov edx,0x2
 xor eax,eax
 call 401030 <printf@plt>
So yeah, code is very similar except -fbuiltin-realloc one is broken while -fno-builtin-realloc works.

But lots of people claim it's “an optimization”. It's supposed to “improve” something. So… what, where and how does it improve?

> This is just paranoia.

If that's a paranoia then surely it would be easy for you to show something which benefits from -fbuiltin-realloc. Anything at all, I'm not that picky, if you don't have a real-world app I can even accept some contrived artificial example for the start.

Footguns

Posted Jul 18, 2021 3:24 UTC (Sun) by mrugiero (guest, #153040) [Link]

> I think we are talking past each other.
Oh no, I was just explaining where my confusion came from, but we were on the same page by that comment :)

> The interesting case is an “optimization” enabled with -fbuiltin-realloc. The one which forbids user to compare p and q and says that you can't use pointers to the “old” realloced object. What does that one improve?
And we agreed it doesn't improve anything.

> Precisely. And now I can't just do a simple q == p check to see if object is unmoved. I have to, basically, always do p = q assignment (if q is not nullptr) but what's more important: I can't keep pointers to “old” realloced object anywhere. If I have some other pointers (besides p) and, especially, if I have “internal” pointers (which point to the inside of that object) then I have to turn them all into indexes and then traverse everything after realloc again and turn these indexes back to pointers!
You have a great point. I didn't think about the internal pointers that get invalidated. The check saves you that iteration, but now you can't.

> But where the heck that dance imposed on me by C compiler developers helps? What kind of program? What benchmark? Can you show me anything at all where -fbuiltin-realloc is faster than -fno-builtin-realloc?
No, I'm not defending whatever nonsense they introduced.

> It has everything to do with the concept of provenance. What -fbuiltin-realloc option does is tells the compiler: “if someone does q = realloc(p, …) then you can assume that p and q have different provenance, they could never alias (not even after explicit p == q check) and you can optimize on basis of that”.
I picked the wrong words. What I meant is that, while the concept provenance is what allows this behavior, and provenance by itself makes sense, it's still a stupid thing to do. It doesn't mandate it, and you know, the fact that you can do something doesn't mean it's a good idea.

> The one thing (and very big thing for me): WTH can you use that assertion for? What kind of code should we observe for that rule to speed up anything?
I can't think of any examples that wouldn't require an idiot to make them useful, I give you that.

> Casting itself is free. But because new “optimized” rules demand that you find and convert all pointers before you call realloc you would need additional code which would traverse additional data structures and scans all these pointers.
> And after realloc you have to traverse all these data structures again and turn indexes back into pointers.
Agreed. Shitty idea.

> But lots of people claim it's “an optimization”. It's supposed to “improve” something. So… what, where and how does it improve?
It definitely doesn't optimize anything there if it just changes a constant :/

> If that's a paranoia then surely it would be easy for you to show something which benefits from -fbuiltin-realloc. Anything at all, I'm not that picky, if you don't have a real-world app I can even accept some contrived artificial example for the start.
Not really. There's no need for evil to make bad choices. You can have stupidity, arrogance, stubbornness, misguidance, and countless other reasons that may lead to that. That's why I say it's paranoia. In this case, I'd go with "we tried to improve this little number because we needed a report to look better" kind of misguidance. They had an artificial benchmark that somehow got a benefit (possibly giving a broken result that they didn't check) and called it a day. Under lack of evidence of the contrary I'll always assume there was no bad intention. Specially for something that doesn't seem to have stakes that high such as promoting another language. I simply don't see the benefit.

Footguns

Posted Jul 18, 2021 11:27 UTC (Sun) by excors (subscriber, #95769) [Link] (1 responses)

> The interesting case is an “optimization” enabled with -fbuiltin-realloc. The one which forbids user to compare p and q and says that you can't use pointers to the “old” realloced object. What does that one improve?

If I'm interpreting it correctly, this example. The -fno-builtin-realloc version increases the main loop from 4 instructions to 6 instructions, which is a significant performance difference. That code is:

struct Context { int initial; };

int *test(Context *ctx, int *p, int n) {
    int *q = (int *)realloc(p, n * sizeof(int));
    for (int i = 0; i < n; ++i)
        q[i] = ctx->initial + 1;
    return q;
}

which I think is not particularly artificial or unrealistic.

As far as the compiler knows, this might be called with p == ctx. Then realloc returns a pointer which (with -fno-builtin-realloc, or with a builtin that doesn't have the rule about invalidating pointers to the old object) might be equal to p and pointing to the same object, in which case the write to q[i] might validly change the value of ctx->initial, therefore it has to dereference ctx on every iteration.

With builtin realloc, the compiler knows that q is always a pointer to a newly-allocated object that is distinct from any object pointed to by previously-existing pointers, even if q happens to be numerically equal to the old p, so there's no aliasing between q and ctx and it's safe to hoist the "ctx->initial + 1" out of the loop. ("safe" in the sense that it won't affect behaviour unless the program is illegally accessing the object pointed to by p, in which case the program is buggy.)

(I guess in theory the compiler could do this optimisation without the "invalidating old pointers" rule, with the reasoning that realloc might deallocate p (and the programmer can never be sure that it won't, even if n matches the size of the original allocation), therefore it's invalid for the programmer to call this function with p == ctx && n != 0 because that will dereference ctx which might have been deallocated, so the compiler can assume n == 0 || p != ctx, and the loop body only executes if n != 0, so the loop body can be compiled assuming p != ctx, in which case q != ctx (because either q == p or q is a new numerically-distinct pointer, by the basic definition of realloc). But that sounds infeasibly complicated to implement, whereas "the pointer returned by realloc never aliases any pointer that already exists" sounds pretty simple.)

Footguns

Posted Jul 18, 2021 12:09 UTC (Sun) by khim (subscriber, #9252) [Link]

That's pretty nice example. But it shows all the issues discussed in the well-known rant perfectly.

First: it doesn't require you to make surprising and crazy assumption that p and q point to distinct objects. So it's not quite the same optimization as I'm not talking about.

Second: it can be easily fixed by just adding one restrict - and that works even for a compiler which doesn't know anything about realloc.

> But that sounds infeasibly complicated to implement, whereas "the pointer returned by realloc never aliases any pointer that already exists" sounds pretty simple.)

Sounds “infeasibly complicated”… to whom? To someone who doesn't want to compile standard-conforming programs correctly? To someone who feels C and C++ develpers are slaves of the compiler and would have to follow all the warts of it no matter what?

Adding 10 (20? 100?) lines of code was “infeasibly complicated”, but forcing all users of realloc to review and fix their code is not?

That's precisely an attitude which makes C and C++ unsuitable for any purpose.

That's already a very-very troubling and hard to accept attitude. But at least when there was that “holy scripture”, that standard, which everyone was supposed to obey… it looked doable.

Now, when we know compiler developers wouldn't stop their work on sabotaging perfectly working programs (yes, I know: usually it's considered polite to say something like compiler writers really like the freedom that aggressive undefined behavior gives them to optimize, and are reluctant to cede any ground that might impact performance but I'm rude guy and when I see that something is black I call it black) we need to start thinking about escape plan.

Not necessarily Rust. It may be Ada, C# or even, gasp, Java or JavaScript.

But we have to start working on migration. Now. Till we still have time.

It's time to declare C and C++ a liability and start working on making sure they would join PL/I in the annals of history. Maybe then people would learn something.

Footguns

Posted Jul 15, 2021 9:37 UTC (Thu) by cortana (subscriber, #24596) [Link] (2 responses)

What's the correct way to do the assert(x + 100 > x) check?

assert(x <= INT_MAX - 100)?

(I'd not even chance it and reach for __builtin_add_overflow but of course that might not be available in your favourite implementation...)

Footguns

Posted Jul 15, 2021 10:14 UTC (Thu) by khim (subscriber, #9252) [Link] (1 responses)

Yes, assert(x < INT_MAX - 100) is the one pushed by C compiler developers, but since unsigned overflow is not a thing (usigned arithmetic is defined as modular arithmetic by the standard thus it may never overflow)… and since conversion between signed and unsigned was always implementation-defined behavior, not undefined behavior… and since in C++ is mandatory to use two's complement now… starting from C++20 in theory and in all existing compilers in practice you can use… drumroll… assert((int)((unsigned)x + 100U) > x);. Easy, ne?

It works exactly the same as assert(x + 100 > x); (with -fwrapv) the only difference it that it's ugly and you wouldn't learn about it in books.

That's the problem with C/C++ compiler developers: it lies not with the fact that it's impossible to write correct C/C++ code. But it's hard, and, worst of all, C/C++ compiler developers employ extremely strong SEP field and never even consider the need, to, you know, write real program somehow, important. It's always “go read the standard”, “our way or the highway” style discussion.

And __builtin_add_overflow shows that perfectly: yes, it's very nice solution. The only issue: gcc 2.95 from last century already breaks these checks, yet first version of GCC which got __builtin_add_overflow was version 5.0 (and clang got that one even later and no because they think C developers deserve it, but because they wanted to be more GCC compatible).

Footguns

Posted Jul 15, 2021 14:08 UTC (Thu) by cortana (subscriber, #24596) [Link]

Thanks. And wow that expression with the casts is unpleasant... I think I'll stick with the INT_MAX version!


Copyright © 2025, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds