|
|
Subscribe / Log in / New account

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 3, 2023 11:51 UTC (Fri) by farnz (subscriber, #17727)
In reply to: Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack) by vadim
Parent article: Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

On the other hand, you can always apply those optimizations by hand (in both languages), and you can ignore UB (in both languages), relying on the programmer to "hold the tool properly".

It's just that C++'s default presumption is that it's OK to tell the programmer to "hold the tool properly" (see also -fwrapv as an example, where if that were the default, some programs would be a bit slower, but there'd be a lot less UB out there, and it's usually easy to fix the cases where -fwrapv slows down the output code), while Rust's default is that it's OK to sacrifice a little performance in order to avoid having to tell the programmer to "hold the tool properly".

This difference, BTW, is why I don't hold out much hope for a "safe C++"; if the people involved in C++ cared, then something like -fwrapv would be trivial to enable by default (with an opt-out for older code), and they'd be removing the "no diagnostic required" language from the C++ standard, so that everything where the compiler's interpretation is not defined at least requires a warning.


to post comments

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 3, 2023 12:26 UTC (Fri) by vadim (subscriber, #35271) [Link] (80 responses)

Yeah, but it's not necessarily that clear-cut.

If you're writing something you can't guarantee will be built with -fwrapv (eg, it's a header only library), then you'll have to code defensively. And I believe the UB-proof implementation will be bigger and slower than a straightforward implementation + -fwrapv.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 3, 2023 12:45 UTC (Fri) by Wol (subscriber, #4433) [Link] (64 responses)

But why do you need to enable -fwrapv everywhere?

As I understand it, it says that arithmetic overflow will wrap around? Which is what happens anyway with 2's complement?

So they simply spec that the default for arithmetic overflow is "whatever the chip does", which means fwrapv is now the DEFINED behaviour on x86_64 and similar, which gives you no performance hit for any existing UB-free code.

Okay, it does break the compiler that optimises detected UB to a no-op, but that's tough.

Doesn't break anything that isn't already broken. Converts UB into whatever any sane rational programmer would expect. What's wrong with that?

Cheers,
Wol

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 3, 2023 13:42 UTC (Fri) by farnz (subscriber, #17727) [Link] (12 responses)

Because the hardware does not wrap around in all cases. Specifically, the hardware wraps around if the register size and the variable size are the same; but to fully meet the definition for -fwrapv for 8, 16 and 32 bit integers on x86_64, you have to follow mixed-bitness arithmetic operations (e.g. arithmetic on a 32 bit integer being used to index an array, which is a 64 bit pointer) with sign extension, because, per the implicit casting rules of the language, you must do the arithmetic in the wider type, then sign-extend as you convert back down to the narrower type.

If you also forbade mixed-type arithmetic without explicit conversions (so "int x = 1; long y =2; return x + y;" is now illegal, because x is int, y is long, and you need to tell the compiler which type to convert them to for the arithmetic), then you'd not have the performance problem. But that's a huge pile of pain fixing all sorts of code that does things like "for(int i = 0; i < len; ++i) { array[i] = func(i) }" (since i in this context has to be a size_t, and size_t is an unsigned integer, not a signed one).

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 5, 2023 17:16 UTC (Sun) by anton (subscriber, #25547) [Link] (11 responses)

It depends on the way the result is used (e.g., when you pass it to a function, it depends on the ABI) whether you have to sign-extend the result. And it depends on the architecture. E.g., AMD64 (aka x86_64) has a 32-bit addition operation, but it always zero-extends; it has an 8-bit and 16-bit addition which leave the rest of the 32 bits alone. RV64GC has a 32-bit addition which sign-extends, and not 8-bit or 16-bit addition, and no zero-extending 32-bit addition, so you may be seeing zero-extending instructions when, e.g., performing array indexing with unsigned ints. ARM A64 has addressing modes that sign-extend or zero-extend a value in a register, so you can avoid explicit sign- or zero extension of 32-bit values for use in addressing.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 6, 2023 10:26 UTC (Mon) by farnz (subscriber, #17727) [Link] (10 responses)

Right, but this means that "what the hardware does" is also ill-defined; not only is it different on different hardware (which is fair enough), it's also different on the same hardware depending on the use of the result in the C abstract machine.

It would be far simpler to just fully define the behaviour, as -fwrapv does, and accept that in some cases, there's a performance cost that can only be addressed by fixing your code. But that would mean losing on the SPECint 2006 benchmark, which is less acceptable than having a language full of UB…

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 7, 2023 18:44 UTC (Tue) by anton (subscriber, #25547) [Link] (9 responses)

"What the hardware does" is a idea, but indeed not a specification (I guess that's why UB advocates love to beat on strawmen that include this statement).

However, you can define one translation from an operation-type combination in the C abstract machine to machine code. If the results are used in some way, conditioning the data for that use is part of that use. One interesting case is p+i: Thanks to the I32LP64 mistake, on 64-bit platforms p is 64 bits wide, while i can be 32 bits wide, but most hardware does not have instructions that performs a scaled add of a sign-extended (or, if i is unsigned, zero-extended) 32-bit value and a 64-bit. So one would translate this operation to a sign/zero-extend instruction, a multiply by the size of *p, and an add instruction.

And then you can optimize: If the instruction producing (signed) i already produced a sign-extended result, you can leave the sign extension away; or you may be able to combine the instruction that produces i with the sign/zero extending instruction and replace it with some combined instruction. And for that you can see how the various features of the architectures mentioned above play out.

As for -fwrapv or more generally -fno-strict-overflow, all architectures in wide use have been able to support that for many decades, so yes, that would certainly be something that can be done across the board and making it the default and putting that behaviour into the C standard is certainly a good idea. C compiler writers worrying about performance can then warn about loop variable types that require a sign extension or zero extension on every loop iteration.

BTW, on machines with 32-bit ints, there is no need to sign-extend 8-bit or 16-bit additions, because the way that C is defined, all additions happen on ints (i.e., 32 bits) or wider. So you convert your 8-bit or 16-bit operands to ints first, and then add them.

Standardization on a fully-defined behaviour is unlikely for cases where architectural differences are more substantial, e.g., shifts. You can then define -fwrapv-like flags, and that might be a good porting help, but in the absence of that, having consistent behaviour on a specific platform would already be helpful.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 7, 2023 19:17 UTC (Tue) by farnz (subscriber, #17727) [Link] (6 responses)

But therein lies the core of the problem: -fwrapv costs some subsets of SPECint 2006 around 5% to 10% performance numbers. Which means, in turn, that people have already refused to turn on -fwrapv by default, since they're depending on the performance boost they get from the compiler treating "int" as "unsigned", rather than promising sign extension and wraparound.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 8, 2023 10:10 UTC (Wed) by anton (subscriber, #25547) [Link] (5 responses)

It's actually only one program in SPECint2006 where it costs 7.2% with gcc (as reported by Wang et al.); and the performance improvement of the default -fno-wrapv can alternatively be achieved by changing the type of one variable in that program.

You write about people and culture, I write about compiler maintainers and attitude. And my hypothesis is that this attitude is due to the compiler maintainers (in particular those working on optimization) evaluating their work by running their compilers on benchmarks, and then seeing how an optimization affects the performance. The less guarantees they give, the more they can optimize, so they embrace UB. And they produced advocacy for their position.

Admittedly, there are also other motivations for some people to embrace UB:

  • Language lawyering, because UB gives them something that distinguishes them from ordinary programmers.
  • Elitism: The harder C programming is (and the more UB, the harder it is), the more they feel part of an elite. Everyone who does not want it that hard should switch to a nerfed language like Java, or better get out of programming at all.
  • Compiler supremacy: The compiler always knows best and magically optimizes programs beyond what human programmers can do. So if, by not defining behaviour, the compiler reduces the ways in which a programmer can express something, that's only for the best: The compiler will more than make up for any potential slowdown from that by being able to optimize better. After all (and this is where the compiler writer advocacy kicks in), UB is the source of optimization, and without having as much UB in C as there is, you might as well use -O0.
  • It's free: There are those who have not experienced that the compiler changed the behaviour of their program based on the assumption that the program does not exercise UB (or have not noticed it yet, in cases where the compiler optimizes away a bounds check or the code that erases a secret). They can fall for the compiler writer's advocacy that UB gives them performance for free. The cost of having to "sanitize" (hunting and eliminating UB in) their programs is not yet obvious to them.
There are, however, also other positions, advocated by many (including me), so I think that the C compiler writer's position on UB is not "the C culture" (it may be "the C++ culture", I don't know about that). In particular, I think (and have evidence for it) that humans are superior at optimizing programs, and that, if the goal is performance, programmer's time is better spent at performing such optimizations (by, e.g., changing the type of one variable, but also more involved transformations) than at "sanitizing" the program.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 8, 2023 10:57 UTC (Wed) by farnz (subscriber, #17727) [Link] (4 responses)

What I see, however, when I look at what the C standards committee is doing, is that the people who drive C and C++ the languages forwards are dominated by those who are embracing UB; this is largely because "make it UB, and you can define it if you really want to" is the committee's go-to answer to handling conflicts between compiler vendors; if there are at least two good ways to define behaviour (e.g. arithmetic overflow, where the human-friendly definitions are "error", "saturate", and "wrap"), and two compiler vendors refuse to agree since the definition either way results in the output code being less optimal on one of the two compilers, the committee punts on the decision.

And it's not just the compiler maintainers and the standards committees at fault here; both GCC and Clang provide flags to provide human-friendly defined behaviours for things that in the standard are UB (-fwrapv, -ftrapv, -fno-delete-null-pointer-checks, -fno-lifetime-dse, -fno-strict-aliasing and more). Users of these compilers could insist on using these flags, and simply state that if you don't use the flags that define previously undefined behaviour, then you're Holding It Wrong, but they don't.

Perhaps if you got (say) Debian and Fedora to change their default CFLAGS and CXXFLAGS to define behaviours that in standard C and C++ are undefined, I'd believe that you were anything more than a minority view - but the dominant variants of both C and C++ cultures don't do that.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 8, 2023 11:45 UTC (Wed) by Wol (subscriber, #4433) [Link] (1 responses)

> this is largely because "make it UB, and you can define it if you really want to" is the committee's go-to answer to handling conflicts between compiler vendors;

Arghhhh ....

The goto answer SHOULD be "make it implementation defined, and define a flag that is on by default". If other compilers don't choose to support that flag, that's down to them - it's an implementation-defined flag.

(And given I get the impression this SPECInt thingy uses UB, surely the compiler writers should simply optimise the program to the null statement and say "here, we can run this benchmark in no time flat!" :-)

Cheers,
Wol

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 8, 2023 12:38 UTC (Wed) by farnz (subscriber, #17727) [Link]

The C standard (and the C++ standard) do not define flags that change the meaning of a program, because they are trying to define a single language; if something is implementation defined, then the implementation gets to say exactly what that means for this implementation, including whether or not the implementation's definition can be changed by compiler flags.

There's four varieties of incompletely standardised behaviour for a standards-compliant program that do not require diagnostics in all cases (I'm ignoring problems that require a diagnostic, since if you ignore warnings from your compiler, that's on you):

  1. "ill-formed program". This is a program which simply has no meaning at all in the standard. For example, I am a fish. I am a fish. I am a fish is an ill-formed program (albeit one that requires a diagnostic). Ill-formed programs can always be optimized down to the null statement.
  2. "undefined behaviour". This is a case where, due to input data or program constructs, the behaviour of the operation is undefined, and the consequence of that undefinedness is that the entire program has no meaning attributed to it by the standard. A compiler can do anything it likes once you've made use of UB, but in order to do this (e.g. compile down to the null statement), the compiler first has to show that you're using UB; e.g. you've executed a statement with undefined behaviour, such as arithmetic overflow in C99. If you do something that has UB in some cases but not others, the compiler can assume that the UB cases don't happen.
  3. "unspecified behaviour". The behaviour of the program upon encountering unspecified behaviour is not set by the standard, but by the implementation. The implementation does not have to document what the behaviour will be, nor remain consistent between versions. Unspecified behaviour can be constrained by the standard to a set of possible options; for example, C++03 says that statics initialized via code are initialized in an unspecified order, but for each initializer, all statics must either be fully-initialized or zero-initialized at the point the initializer runs. This means that code like int a() { return 1; }; int b = a(); int c = b + a(); can set c to either 1 (b was zero-initialized) or 2 (b was fully-initialized), but not to any other value. However, because this is unspecified, the behaviour can change every time you run the resulting executable.
  4. "implementation-defined behaviour". The behaviour of the program upon encountering unspecified behaviour is not set by the standard, but by the implementation. The implementation must document what behaviour it chooses; like unspecified behaviour, the allowed options may be set by the standard.

And it's been the case in the past that programs have compiled down to the null statement because they always execute UB; the problem with the SPECint 2006 benchmark in question is that it's conditionally UB in the C standard language, and thus the compiler must produce the right result as long as UB does not happen, but can do anything if UB happens.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 8, 2023 18:55 UTC (Wed) by anton (subscriber, #25547) [Link] (1 responses)

What I see, however, when I look at what the C standards committee is doing, is that the people who drive C and C++ the languages forwards are dominated by those who are embracing UB; this is largely because "make it UB, and you can define it if you really want to" is the committee's go-to answer to handling conflicts between compiler vendors
Yes, not defining something where existing practice diverges is the usual case in standardization. That's ok if you are aware that a standard is only a partial specification; programs written for compiler A would stop working (or require a flag to use counter-standard behaviour) if the conflicting behaviour of compiler B was standardized. However, if that is the reason for non-standardization, a compiler vendor has a specific behaviour in mind that the programs of its customers actually rely on. For a case (e.g., signed overflow) where compiler vendors actually say that they consider the programs that do this buggy and reject bug reports about such programs, compiler vendors do not have this excuse.

We certainly use all such flags that we can find. Not all of the following flags are for defining what the C standard leaves undefined, but for gcc-10 we use: -fno-gcse -fcaller-saves -fno-defer-pop -fno-inline -fwrapv -fno-strict-aliasing -fno-cse-follow-jumps -fno-reorder-blocks -fno-reorder-blocks-and-partition -fno-toplevel-reorder -falign-labels=1 -falign-loops=1 -falign-jumps=1 -fno-delete-null-pointer-checks -fcf-protection=none -fno-tree-vectorize -pthread -fno-defer-pop -fcaller-saves

I think both views are minority views, because most C programmers are relatively unaware of the issue. That's because the maintainers of gcc (and probably clang, too) preach one position, but, from what I read, practice something much closer to my position: What I read is that they check whether a new release actually builds all the Debian packages that use gcc with the release candidate and whether these packages then pass some tests (probably their self-tests). I assume that they then fix those cases where the package then does not work (otherwise, why should they do this checking? Also, Debian and other Linux distributions are unlikely to accept a gcc version that breaks many packages). This covers a lot of actual usage (including a lot of UB). However, it will probably not catch cases where a bounds check is optimized away, because the tests are not very likely to test for that.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 9, 2023 11:23 UTC (Thu) by farnz (subscriber, #17727) [Link]

Right, but you're a rarity. I see that big visible users of C (Debian, Fedora, Ubuntu, SLES, RHEL, FreeRTOS, ThreadX and more) don't set default flags to define more behaviour in C, while at the committee level John Regehr (part of the CompCert verified compiler project) could not get agreement on the idea that the ~200 UBs in the C standard should all either be defined or require a diagnostic from the compiler. And compiler authors aren't pushing on the "all UB should either be defined or diagnosed" idea, either.

So, for practical purposes, C users don't care enough to insist that their compilers define behaviours that the standard leaves undefined, nor do they care enough to insist that compilers must provide diagnostics when their code could execute UB (and thus is, at best, only conditionally valid). The standard committee doesn't care, either; and compiler authors aren't interested in providing diagnostics for all cases where a program contains UB.

From my point of view, this is a "C culture overall is fine with UB" situation - people have tried to get the C standard to define more behaviours, and the in charge of the C standard said no. People have managed to get compilers to define a limited set of behaviours that the standard leaves undefined, and most C users simply ignore that option - heck, if C users cared, it would be possible to go through the Fedora Change Process, or the Debian General Resolution process to have those flags set on by default for entire distributions, overruling the compiler maintainers. Given the complete lack of interest in either top-down (start at the standards body and work down) or bottom-up (get compiler writers to introduce flags, set them by default and push for everyone to set them by default) fixes to the C language definition, what else should I conclude?

And note that in the comments to this article, we have someone who agrees that too much of C is UB saying that they'll not simply insist that people use the extant compiler flag and rely on the semantics that are created by that - which is basically C culture's problem in a nutshell; we have a solution to part of the problem, but we're going to complain instead of trying to push the world to a "better" place.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 7, 2023 22:27 UTC (Tue) by Wol (subscriber, #4433) [Link] (1 responses)

> "What the hardware does" is a idea, but indeed not a specification (I guess that's why UB advocates love to beat on strawmen that include this statement).

> However, you can define one translation from an operation-type combination in the C abstract machine to machine code. If the results are used in some way, conditioning the data for that use is part of that use.

No it's nice and simple ... presumably in your example the processor has an op-code to carry out the operation. So instead of UB, we now have Implementation Defined - the compiler chooses an op-code, and now we have Hardware Defined.

If the compiler writers choose idiotic op-codes, more fool them. But the behaviour of your code is now predictable, given a fixed compiler and hardware. Of course "same compiler and hardware" has to be defined to mean all versions of the compiler and all revisions of the architecture.

"What the hardware does" means the compiler writers have to pick an implementation, AND STICK WITH IT. (Of course, a flag to force a different implementation is fine.)

Cheers,
Wol

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 8, 2023 10:51 UTC (Wed) by farnz (subscriber, #17727) [Link]

presumably in your example the processor has an op-code to carry out the operation

Not usually, no. The processor typically has between 0 and 3 opcodes that you can use to implement any low-level operation, with different behaviours; if it has zero opcodes, then there are multiple choices for the sequence of opcodes you use to implement the C abstract machine, each with different behaviours.

And inherently, if you're asking the compiler writers to pick an option and stick to it forever, you're also saying that you don't want the optimizer to ever do a better job than it does in the current version; the entire point of optimizing is to choose different opcodes for a given C program, such that the resulting machine code program is faster.

This differs to things like -fwrapv, and -funreachable-traps, since those options define the behaviour of the source code where the standard says it's UB, and promise you that whatever opcodes they end up picking, they'll still meet this definition of behaviour; but a negative consequence of that is that there are programs where the new definition costs you a register or more opcodes. Now, you can almost certainly fix those programs to not have the performance bug; but that's a tradeoff that people choose not to make.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 3, 2023 15:53 UTC (Fri) by khim (subscriber, #9252) [Link] (50 responses)

> What's wrong with that?

The wrong with that is such thing:

> whatever any sane rational programmer would expect

SIMPLY DOESN'T EXIST

Consider something like this: (2 >> 33) + (4 >> 257). Different CPUs would give the following answers:

  1. The answer may be 2 (ARM), or
  2. The answer may be 3 (80386), or
  3. The answer may be 0 (x86-64 if fully vectorized), or
  4. The answer may be 1 (x86-64 if partially vectorized), or
  5. The answer may be 2 (x86-64 if partially vectorized)

Out of these possibilities what's the whatever “any sane rational programmer would expect”? #1, #2, or #3? And if compiler would produce #4 or #5… is it still “sane” or not?

Note that half-vectorized code breaks that all-important anton's “C int bit-rotation idiom(x<<n) | (x>>(32-n)) while faitfully converting each operand to precisely one assembler instructions and then “doing what the hardware is doing”! Is it still “a portable assembler” or not, at this point, hmm?

The only way to achieve any semblance of “sanity” is to discuss these things, write them into a language spec and then ignore “what the hardware is doing” from that point on.

Because hardware is different and inconsistent. Vector instructions are doing things differently from scalar intructions on x86-64 and RISC-V permits different behavior on different cores of the same big-LITTLE CPU!

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 3, 2023 16:13 UTC (Fri) by vadim (subscriber, #35271) [Link] (46 responses)

> Out of these possibilities what's the whatever “any sane rational programmer would expect”?

Personally I find all of them acceptable.

Yes, it varies between CPUs and instructions used. But to me what's important is that it executes as code, I can step by step through it in GDB, and easily figure out where I'm going wrong and fix it.

> The only way to achieve any semblance of “sanity” is to discuss these things, write them into a language spec and then ignore “what the hardware is doing” from that point on.

In that case the language should declare a single correct interpretation and emulate it as necessary on every CPU.

My stance is that I find basically 2 approaches to be acceptable:

1. Do like Java and ignore the hardware. Declare that eg, when shifting only the lowest 5 bits matter, and if it costs performance to ensure that on the underlying CPU, too bad, you take the hit.
2. Translate it as-is, and whatever happens on this CPU, happens

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 3, 2023 16:38 UTC (Fri) by khim (subscriber, #9252) [Link] (31 responses)

> Personally I find all of them acceptable.

Then you are Ok with the fact that two of them break that C rotation idiom, right? Why is it Ok to break in that way but not in some other way?

> But to me what's important is that it executes as code, I can step by step through it in GDB, and easily figure out where I'm going wrong and fix it.

Then -O0 should be your choice. Why is it not acceptable for you?

> Translate it as-is, and whatever happens on this CPU, happens

That sends us back to -O0 solution, isn't it?

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 3, 2023 17:25 UTC (Fri) by vadim (subscriber, #35271) [Link] (30 responses)

> Then you are Ok with the fact that two of them break that C rotation idiom, right?

Ideally, I'd like #3 or #2.

#3 has the most human appeal. #2 makes sense from the technical standpoint of "only the lowest 5 bits matter". I'm not sure where ARM's #1 comes from exactly.

IMO, #4 and #5 shouldn't happen without a dedicated compiler argument in the style of -funsafe-math-optimizations that implies some accuracy is being sacrificed somewhere. Optimization shouldn't change visible behavior without explicitly opting into it.

> Why is it Ok to break in that way but not in some other way?

I don't particularly like it, but it's still better than "this is UB and will be silently compiled to nothing". I asked for a shift, so I expect to get a shift, exact equivalent, or a compiler error.

> Then -O0 should be your choice. Why is it not acceptable for you?

It's indeed how I build my code in development by default. What I don't like is the behavior changing with higher optimization levels. I'm fine with optimizations, so long the code is guaranteed to arrive at exactly the same result as with -O0, unless some explicit opt-in is given.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 4, 2023 17:42 UTC (Sat) by khim (subscriber, #9252) [Link] (19 responses)

> I'm not sure where ARM's #1 comes from exactly.

ARM just takes low byte of argument and then treats it like human would.

> Optimization shouldn't change visible behavior without explicitly opting into it.

In language like C this would leave list of possible optimizations empty.

Simply because any change to generated code may be classified as “change in visible behavior” for a program that takes address of function and then disassebles it.

> I asked for a shift, so I expect to get a shift, exact equivalent, or a compiler error.

What that phrase even means? What is I asked for shift, what is I get a shift, what is exact equivalent?

You have, basically, replaced hard task (implementing complicated but, presumably, unambigious spec) with impossible task (implementing bunch of things which different people interpret differently). How is that an improvement?

> I'm fine with optimizations, so long the code is guaranteed to arrive at exactly the same result as with -O0, unless some explicit opt-in is given.

IOW: you are fine with optimizations that break someone's else code, but not Ok with optimizations that break your code. O_PONIES, O_PONIES and more O_PONIES

> What I don't like is the behavior changing with higher optimization levels.

If they change the behavior of correct C or C++ program then it's bug that should be fixed. But that's not what you are talking about, right?

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 4, 2023 20:41 UTC (Sat) by Wol (subscriber, #4433) [Link] (9 responses)

> > I asked for a shift, so I expect to get a shift, exact equivalent, or a compiler error.

> What that phrase even means? What is I asked for shift, what is I get a shift, what is exact equivalent?

Okay, so we define left shift mathematically as move all the bits left one space. A "specification compliant" program will get the same as "times 2" because a specification-compliant program won't left-shift something too big. In other words, the spec should DEFAULT to assuming the program is well-formed, and the result will be exactly the same as a guy doing it with pen and paper.

At which point the question is "What happens if the hardware can't do what's asked of it" or "what happens if the programmer does something stupid" (eg 256*256 in unsigned byte arithmetic)?

We're not really even asking for C/C++ to define what happens for a malformed program. What we need is for the standard / compiler to give us sufficient information to work out what will happen if things go wrong. "Does << mean left shift with rotate, or left shift and drop?" Will x << 32 on an int32 give you x or 0?

And like fwrapv, are you confident enough to let the static analyser assume a well-formed program, or are you assuming malicious input intended to cause trouble?

And actually, this brings into clarity the reason why the current compiler-writer obsession with optimisation is HARMFUL to computer security! It's pretty obvious that the combination of compiler writers assuming that all input is well-formed (whether to the compiler, or a program compiled by the compiler), and crackers searching for ever more ingenious ways to feed malformed input into programs, is going to lead to disaster.

At the end of the day, to be secure ANY language needs to define what SHOULD happen. And given that programming is *allegedly* (should I really feel forced to use that word?) maths, defining what should happen should be easy. At which point you then have to define what happens in the corner cases, eg overflow, wraparound, etc etc.

Without a CONCERTED and SERIOUS effort to remove as much UB as possible from the language (and to implement the principle of "least surprise"), the use of C or C++ should be a major security red flag.

Cheers,
Wol

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 4, 2023 21:23 UTC (Sat) by smurf (subscriber, #17840) [Link]

> At the end of the day, to be secure ANY language needs to define what SHOULD happen

That's not enough. It also needs to define what SHOULD NOT happen, and be able (language design problem) and empowered (here's that cultural problem again) to halt the compilation and/or runtime if/when it does.

Arithmetic overflows and similar problems are easy to protect against. They're local. You can just wrap your possibly-overflowing in protective coating. GCC has a bunch of intrinsics that tell you when an operation overflows. Also, adding two numbers is unlikely to modify a random bit somewhere else.

The large class of memory leak / double free / aliasing / "array" overrun / … errors which C/C++ is (in)famous for, and which comprise the overwhelming majority of exploits on programs written in it, is a rather different kettle of fish.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 5, 2023 9:55 UTC (Sun) by khim (subscriber, #9252) [Link] (7 responses)

> We're not really even asking for C/C++ to define what happens for a malformed program.

Seriously?

> What we need is for the standard / compiler to give us sufficient information to work out what will happen if things go wrong.

Ah. So you replace hard problem with impossible problem and call that an improvement?

Ultimately the whole discussion is classic half-full vs half-empty discussion.

Let's start with the facts:

  1. There are hundreds of UBs in C and C++
    Some of these are very obscure and no one remembers all of them.
  2. The wast majority of these can easily be converted to full-implemented definition or implementation-depenedent definition
    Examples are integer overflow, lack of newline at the end of file (yes, that's UB in C!), etc.
  3. But small minority of these are “hard” UB, the ones which couldn't be easily defined, the most you can do is some kind of UB detector (like Rust's Miri and various clang/gcc sanitizers) which detected them (and then you rewrite the code instead of trying to reason about how that code with UB would work)
    For example “an attempt to use valiable outside of it's scope like I showed here

And what makes C/C++ unfixable is dishonesty on both sides.

O_PONIES lovers (including you, I guess) concentrate on fact #2 and demand that compliers treat all UBs in a predictable way. That's impossible because of fact #3.

Compiler developers concentrate on fact #3 and then demand that all UBs were avoided because small subset of these couldn't be ever handled by optimizing compiler at all. That's dishonest but is the only option in the presence of people who refuse fact #3.

The only solution which can lead somewhere is the following:

  1. Kick-out/remove from the community guys who ignore the fact #3.
    They can never be satisfied and as long as the code from such people is in circulation you may never trust that your programs would behave adequately.
  2. Accept that fact #1 is unacceptable and invent some plan of reducing number of UBs.
    Because today no one, not even compiler developers, may remember all these UBs that are peppering language specifications and standard library specifications.
  3. Open the dialogue between compiler developers and compiler users about how #2 UB should be treated.

And so far I don't see the most important step #1 being even acknowledged by C/C++ communities, let alone being addressed.

In Rust it works, as I have shown already. Steve may call it “a sad day for Rust”, but in reality it was absolutely necessary “rite of passage”. Because social problems can only be solved by social measures and refusal to accept existence of fact #3 is social issue.

> At the end of the day, to be secure ANY language needs to define what SHOULD happen.

That's impossible in low-level language which allows one to insert arbitrary assembler code (and both C/C++ and Rust make it possible). And C# or Java or JavaScript or Python or… literally anything would, by that definition, be unsafe if it allows loading shared libraries which may include arbitrary assembler code.

On the contrary: to achieve any semblance security you need to kick out people who insist on the definition of everything. That's impossible. What you have to do is ensure that there are certain rules your “crazy code” (that part that is arbitrary assembler code, if nothing else) have to obey and then, on top of that, you can build other things.

Demanding that compiler developers would give you ponies is not constructive. Even if asking them to reduce number of UBs is constructive. But you may never reduce that number to zero and you may never get predictability from programs that hit an UB case.

> And given that programming is *allegedly* (should I really feel forced to use that word?) maths, defining what should happen should be easy.

Why? How? It's precisely because programming is math certain things are impossible. Rice's theorem, Halting theorem and many other things restrict decidability.

It's precisely because of math that we know fact #3. Certain UBs (not all of them, of course!) literally can not contained. Any modification of program with some “bad” UBs can break it. Including, of course, any optimization, too.

If you want to have both “unrestricted assembler code” and optimizations, then “predictability in face of UB” is not an option.

> At which point you then have to define what happens in the corner cases, eg overflow, wraparound, etc etc.

These are easy to define. What's impossible to define are things related to lifetimes. That's why Rust feels like a breath of fresh air: instead to making developer of handling all lifetimes manually it makes compiler to reason about the majority of them. Only unsafe have to deal with UB (and, of course, “unrestricted assembler code” is unsafe for obvious reason).

> Without a CONCERTED and SERIOUS effort to remove as much UB as possible from the language (and to implement the principle of "least surprise"), the use of C or C++ should be a major security red flag.

These things are complementary: the less UB you leave the more “surprises” you may expect from the remaining ones.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 5, 2023 17:38 UTC (Sun) by Wol (subscriber, #4433) [Link] (6 responses)

> > At the end of the day, to be secure ANY language needs to define what SHOULD happen.

> That's impossible in low-level language which allows one to insert arbitrary assembler code (and both C/C++ and Rust make it possible). And C# or Java or JavaScript or Python or… literally anything would, by that definition, be unsafe if it allows loading shared libraries which may include arbitrary assembler code.

And this is where your logic goes over the top stupid. IF IT AIN'T C, THEN THE C GUARANTEES CANNOT HOLD. !!! !!!

Dial yourself back a bit, and let's keep the discussion about what a SANE language should be doing. Firstly, it should not be saying stuff about what happens over things out of its control!

Which is why something as simple as saying "256 * 256 will give you 65K unless you trigger overflow. If overflow occurs, it's not C's problem, you get what the hardware gives you". Or "multiplying two int16s will give you internal int32. If you don't give it a big enough int to store the result, you get what you get". That at least gives you some ability to reason.

Yes Godel says you can't define a completely logical mathematical model, but if you take THIS approach, where C / C++ *actually* *takes* *responsibility* for things *within* *its* *power*, that will get rid of a lot of UB.

And it is NOT the COMPILER'S JOB to determine what happens if the programmer asks for something illogical. Take use-after-free. Why is the C compiler trying to dictate what happens when the programmer does that? Either you declare something like that "out of scope", or if you detect it the compiler prints an error.

This is the problem with fwrapv, for example. And with loads of UB, actually. The compiler is allowed to take advantage of UB to do stuff that 99% of people don't want !!! If you even just said "any optimisations that are enabled by UB must be explicitly switched on, not enabled by default", that would probably make C/C++ a safer language at a stroke! Even if *some* stuff slips through the net that shouldn't.

Cheers,
Wol

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 5, 2023 19:11 UTC (Sun) by khim (subscriber, #9252) [Link] (4 responses)

> Firstly, it should not be saying stuff about what happens over things out of its control!

How would that work? If you call assembler function then it may do literally anything with any part of the code and data at any point in the future. How can compiler and compiler writers guarantee ANYTHING AT ALL if the requirement is to ensure that code work with adversary which changes everything and anything in your program randomly?

> Which is why something as simple as saying "256 * 256 will give you 65K unless you trigger overflow.

You can not do that if you wouldn't limit parts of the assembler in your program. Heck, on most platforms that assembler part may install “random flipper” which would, after starting it, randomly flip bits in the accessible code and data areas. How can you predict result of 256 * 256 in the presence of such “helper”? How can you predict anything at all?

> Yes Godel says you can't define a completely logical mathematical model, but if you take THIS approach, where C / C++ *actually* *takes* *responsibility* for things *within* *its* *power*, that will get rid of a lot of UB.

Sure. But what's the point? If some random guy or gal insists that any UB must be handled by SANE language predictably, even “random bit flipper” in your process, then your only choice is to sigh and start tedious process of reviewing or removing code written by such a person, what other choice is there? Such person clearly doesn't understand how program development work, if s/he wrote anything that actually works, by accident, you may never be sure it wouldn't stop working at random time in the future.

Yes, lots of C/C++ UBs may be redeclared as not UBs. But that would never satisfy O_PONIES lovers which insist of entirely different treatment of UB and as long as they are part of you community… any semblance of safety is impossible.

> If overflow occurs, it's not C's problem, you get what the hardware gives you

What's the point of that stupidity? If you want predictability then it's much better to just fully specify the result. If you want portability then UB is better because it makes you code usable on wider range of systems.

What the hardware gives you is pretty much useless: it gives you neither predictability nor portability, why even bother?

> And it is NOT the COMPILER'S JOB to determine what happens if the programmer asks for something illogical. Take use-after-free. Why is the C compiler trying to dictate what happens when the programmer does that?

Because that the only way to apply as if rule. Optimizations should preserve behavior of the program. But if your program doesn't have a predictable behavior then it's impossible to answer the question of whether optimization would preserve it or not.

> Either you declare something like that "out of scope", or if you detect it the compiler prints an error.

Once more: consider the following code:

void foo(int x) {
  int y = x;
}

This piece doesn't include UB, doesn't do anything strange, it only does one simple (if useless) assignment.

But if you remove that assignment you may break some other piece of code!

Cases like these make optimizations of programs that may trigger UB pretty much impossible. Critically: it makes it impossible to even optimize these parts of the code that don't trigger UB.

> The compiler is allowed to take advantage of UB to do stuff that 99% of people don't want !!!

Why are you so sure? How can you know that that 99% of people don't want these optimizations? On the contrary: when an attempt was made to find out what optimizations people actually want then almost every UB was found to be liked to significant percentage of developer.

> If you even just said "any optimisations that are enabled by UB must be explicitly switched on, not enabled by default", that would probably make C/C++ a safer language at a stroke!

You mean something like CompCert C? It's not too much popular. If people actually wanted what you preach then they would have switched to it in droves.

In reality… I think there are more Rust users than CompCert C users.

Without a single language definition you no longer have a community, you have bunch of silos and each one not too big.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 5, 2023 20:12 UTC (Sun) by jem (subscriber, #24231) [Link] (3 responses)

Once more: consider the following code:

void foo(int x) {
  int y = x;
}

This piece doesn't include UB, doesn't do anything strange, it only does one simple (if useless) assignment.

But if you remove that assignment you may break some other piece of code!

The code that relies on this assignment does exhibit undefined behaviour: it uses the value of an uninitialised variable. I can't understand why anyone would want to write code like this, which assumes that a function local variable occupies the same address as a variable in a previously called function that has already returned. I don't see the usefulness of this, and even the most junior programmer should realise this is an extremely risky piece of code. This has got nothing to do with the compiler taking advantage of UB to optimise code. Just normal compilers optimisations like deleting the unused variable results in a non-working program.

One could even argue that your program is broken even if it was written in pure assembly language. The stack slot the popped variable occupied is not part of the stack anymore, and I can imagine target machines where the stack grows and shrinks automatically. On such a machine the physical page could be unmapped as soon as the hardware detects that the stack has shrunk past the page border.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 5, 2023 21:50 UTC (Sun) by khim (subscriber, #9252) [Link] (2 responses)

> I can't understand why anyone would want to write code like this, which assumes that a function local variable occupies the same address as a variable in a previously called function that has already returned.

Computer can't understand that, either. Because it couldn't understand anything at all. It doesn't have conscience or common sense.

> I don't see the usefulness of this, and even the most junior programmer should realise this is an extremely risky piece of code.

So what? Remember the context here: If you follow the discipline above, all optimization levels behave the same way, so why talk about -O0?

If you so pompously proclaim that there are some magic discipline which allows you to optimize any code without relaying on the absence of UB then you have to explain how it works on this example (and on other, even more convoluted examples, too).

Or, if you don't plan to optimize any working code then you have to explain why this piece of code is not deserve all optimization levels behave the same way treatment without adding things like I don't see the usefulness of this or I can't understand why anyone would want to write code like this. If you can not do that then your proclamations about how there are “benigh optimizations that don't rely on the absence of UB” are exposed as lies.

> This has got nothing to do with the compiler taking advantage of UB to optimise code.

This had got everything to do with compiler taking advantage of absence of UB to optimize code.

You either say that code which hits UB at runtime may not preserve behavior after optimization or, alternatively, be prepared how to optimize any code that predictably works without optimizations. And any means any here, not just “the code I may want to write”.

> One could even argue that your program is broken even if it was written in pure assembly language.

It works, sorry.

> The stack slot the popped variable occupied is not part of the stack anymore, and I can imagine target machines where the stack grows and shrinks automatically.

Sure, but x86-64 doesn't behave that way.

> On such a machine the physical page could be unmapped as soon as the hardware detects that the stack has shrunk past the page border.

How is that relevant? On x86-64 red zone is large enough to keep values from being overwritten. And O_PONIES lovers always talk about how they have the right to take advantage of any and all quirks of the target platform and yet compiler have to process such programs anyway… I'm not using anything beyond that here.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 6, 2023 7:49 UTC (Mon) by jem (subscriber, #24231) [Link] (1 responses)

>This had got everything to do with compiler taking advantage of absence of UB to optimize code.

Ok, then why are you using code with UB to prove your point? I am talking about the "other piece of [C] code".

Anyway, this discussion isn't leading anywhere, so I am out.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 6, 2023 8:03 UTC (Mon) by mb (subscriber, #50428) [Link]

> Ok, then why are you using code with UB to prove your point? I am talking about the "other piece of [C] code".

The code is "programmed to the machine" and the compiler is expected to just emit instructions and the behavior shall be whatever the machine's stack would do.
The compiler breaks this by optimizing away the stack store, because the whole program is undefined in the language machine model.

"Programming to the hardware" is fundamentally broken.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 6, 2023 7:12 UTC (Mon) by smurf (subscriber, #17840) [Link]

> The compiler is allowed to take advantage of UB to do stuff that 99% of people don't want !!!

You're coming at this from the wrong end of the road.

Everybody (*) wants their code to be as small and/or as fast as possible.
Every optimization whatsoever that a compiler performs can be proven to be perfectly valid *if* there are no UB conditions around.

Compiling for UB-free code allows the optimizer to skip checks and to ignore conditions that correct, non-UB-ish code simply doesn't need or have in the first place. It's not the optimizer steps' jobs to determine whether their transformations introduce errors under UB conditions, because they can't read your mind and can't know what you WANT to happen (and thus, what constitutes an error). The code doesn't tell them. It can't – if it could, it wouldn't be UB in the first place.

The problem is that C++ has no checks(**) to assure that your code does not exhibit UB, no culture to introduce such checks, and a governance that's not interested in changing the culture. The -fwrapv-is-not-the-default-because-of-a-trivially-fixed-SPECint-benchmark controversy is just the tip of the tip of the iceberg.

* OK, 99%; the remaining 1% are stepping through their functions while debugging.

** This would require code annotations equivalent to Rust's mutability and lifetime stuff – which are very hard, if not impossible, to introduce into a language that's *already* a mess of conflicting annotations and ambiguous grammar. Not to mention the code that's supposed to act on them.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 5, 2023 22:49 UTC (Sun) by vadim (subscriber, #35271) [Link] (8 responses)

> In language like C this would leave list of possible optimizations empty.

Nonsense

> Simply because any change to generated code may be classified as “change in visible behavior” for a program that takes address of function and then disassebles it.

Visible as in affecting the program's output. Eg, no normal optimization option should result in a sequence of mathematical operations produce different results. Eg, there should be no way for -O3 to calculate a value for Pi different from -O0.

Obviously, program size, execution speed and so on are not invariants to be preserved.

> What that phrase even means? What is I asked for shift, what is I get a shift, what is exact equivalent?

The mathematical equivalent. Eg, a multiplication or division that returns the same numerical result. So by that standard, vectorization shouldn't be done if there's any way the result could vary, without explicitly opting into that possibility.

> IOW: you are fine with optimizations that break someone's else code, but not Ok with optimizations that break your code.

I just recognize the rare usefulness of special purpose options. But they must be explicitly opt-in and separate from normal optimization.

> If they change the behavior of correct C or C++ program then it's bug that should be fixed. But that's not what you are talking about, right?

Yes, guarantees can't be made regarding incorrect programs. However the language spec should resist with all might any attempts to add additional ways in which a program may be successfully compiled and yet still be incorrect, and to try to reduce the amount of such things in future releases.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 5, 2023 23:20 UTC (Sun) by khim (subscriber, #9252) [Link]

> Visible as in affecting the program's output.

And what prevents one from taking address of some random function and changing output depending on how that function body is encoded?

> Obviously, program size, execution speed and so on are not invariants to be preserved.

Obviously… to whom? Obviously… why? In the absence of UB what prevents one from writing the program whose output depends on the program size, execution speed and so on?

> Eg, a multiplication or division that returns the same numerical result.

But shift returns different result on the same platform, depending on which instruction was choosen. And as @anton helpfully tells us this is even true for simple operations like additions and/or subtractions.

> So by that standard, vectorization shouldn't be done if there's any way the result could vary, without explicitly opting into that possibility.

By that standard nothing whatsoever can be done to anything.

> But they must be explicitly opt-in and separate from normal optimization.

Except we have yet to establish what “normal optimization” even mean. And if any exist in principle.

> However the language spec should resist with all might any attempts to add additional ways in which a program may be successfully compiled and yet still be incorrect, and to try to reduce the amount of such things in future releases.

That part only becomes sensible after existence of unrestricted UB (as in: UB which is “crazy enough” that any program which hits it may produce any random output at all).

After you establish that such UB exists you may “separate the wheat from the chaff”: programs which hit “unrestricted UB” may be miscompiled (since they don't have any predetermined behavior to begin with), programs which don't trigger it must be compiled on the as if basis.

And list of unrestricted UBs becomes subject of negotiations of the language team which includes both compiler writers and compiler users.

If you don't accept the existence of unrestricted UB then discussing about when compiler may or may not do to change the output of program becomes pointless: compiler which would satisfy O_PONIES lovers requirements couldn't exist thus why would anyone spend time to try to create one?

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 6, 2023 6:54 UTC (Mon) by mb (subscriber, #50428) [Link] (6 responses)

> Obviously, program size, execution speed and so on are not invariants to be preserved.

Why is that obvious?

My my multi threaded PI calculation program running on an embedded system uses carefully crafted timing loops to avoid data race UB. Your speed optimization breaks my perfectly working program!

In another part of the program my carefully written program inspects the generated machine code, which I carefully wrote the C code "to the machine" for. It does decisions based on what the compiler generated. Your optimized code is smaller and breaks my code!

You must realize that "sane" is not a trait that you can use in optimizers.
You either have to define the behavior in terms of the language's virtual machine model, you leave it UB.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 6, 2023 15:44 UTC (Mon) by khim (subscriber, #9252) [Link]

Note that from Atari 2600 to Playstation 2 counting cycles and doing syncronization that was was the norm (in fact Atari 2600 uses cheap CPU version which doesn't have interrupts thus it's the only way to do syncronization there).

And if people who did that would be presented by “Portable Assembler” idea they would, naturally, expect something like that.

And inspection of function body is something that iced includes in it's tutorial… do you think it's done because no one does that?

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 7, 2023 9:27 UTC (Tue) by vadim (subscriber, #35271) [Link] (4 responses)

Because by specifying the "optimize" flag you're explicitly asking the compiler to try and make it faster, and therefore change the timing.

And even without optimization I don't think timing can be guaranteed in C or C++. At any time a compiler could be updated to learn of a new instruction, or fix a code generation bug.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 7, 2023 10:24 UTC (Tue) by mb (subscriber, #50428) [Link] (3 responses)

>Because by specifying the "optimize" flag

So we're back to: You must specify -O0, if you "program to the hardware".

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 7, 2023 19:12 UTC (Tue) by vadim (subscriber, #35271) [Link] (2 responses)

No, I don't think even that will do if you're counting cycles, because even with -O0 the possibility exists that the compiler will make different decisions about what instructions to use depending on bug fixes/implementation. I don't think there's for instance any guarantee that GCC and Clang will both produce the same binary with -O0 in anything but the most trivial cases.

So if you're counting cycles you should probably be actually coding it in assembler.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 7, 2023 19:17 UTC (Tue) by mb (subscriber, #50428) [Link] (1 responses)

So you are saying that you can't "program to the hardware" in C at all?
I fully agree.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 8, 2023 13:16 UTC (Wed) by vadim (subscriber, #35271) [Link]

You can to a limited extent.

Eg, under DOS you can write C code that sets interrupt handlers, or does low level control of the floppy drive. The ability to do such low level things is precisely why C gets used to write operating systems.

Like I said elsewhere, "portable assembler" is in my view a very metaphorical description, because obviously there can't be such a thing in the absolute sense. Proper assembler reflects the CPU's architecture, and a single language can't accurately depict the wildly different designs that exist. However it can get there part of the way given some compromises.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 4, 2023 21:09 UTC (Sat) by smurf (subscriber, #17840) [Link] (9 responses)

> I'm fine with optimizations, so long the code is guaranteed to arrive at exactly the same result as with -O0

How should the compiler know which result the -O0-compiled code should generate? It doesn't know about your data input and your preconditions. It doesn't know what you want to achieve. It can't know whether that struct whose address you're passing to your function is going to be modified by the seemingly-unrelated function you're calling, or whether two arrays (sorry – memory ranges you use as arrays) overlap, or (if they do) whether that's a bug or your intent, or … and so on, and so forth.

Current C/C++ optimizers are simple-minded: *Their* precondition for the guarantee you want isn't quite O_PONIES, but that your input doesn't cause any UB.

The problem is, they do not help with ensuring that this precondition (or rather, very large set of preconditions) is actually met. "UB, no warning necessary" idiocy in the standard(s) is one strong indicator that this problem is unlikely to be resolved any time soon. There are others – as should be obvious from other answers in this thread.

NB: Surprise: Quite a few of these documented-as-UB conditions don't even need an optimizer to trigger. I'll leave finding examples for this assertion as a fairly-trivial exercise for the esteemed reader.

Stroustrup's Plan does not address the "UB no warning" problem, or the "include file" problem, or … I could go on.

Compilers are not mind readers.

Rust has aliasing prohibitions etc. that actually mean something (mostly-)concrete, not "more magic" – like 'restrict' and 'volatile' do.

Grafting the concept of sharing-XOR-mutability, or lifetime annotations, or something else that might achieve similar results of UB prevention, onto C/C++ at this point is wishful thinking. That's not just because of the culture, or the heap of old code you'd need to adapt. There's also the include file mess, which pretty much guarantees that you can't mix "old" and "new" code and have the result mean anything. There's the fact that C++ has grown to an unwieldy mess that doesn't even have a context-free unambiguous grammar any more (assuming that it ever did …), thus by this time it's no longer possible IMHO to add something this fundamental and have it MEAN something.

I could go on, but it's late.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 5, 2023 10:12 UTC (Sun) by khim (subscriber, #9252) [Link]

> There's also the include file mess, which pretty much guarantees that you can't mix "old" and "new" code and have the result mean anything.

C compilers had the ability to mix code with different meanings for decades.

> There's the fact that C++ has grown to an unwieldy mess that doesn't even have a context-free unambiguous grammar any more (assuming that it ever did …)

Even the very first edition of The C++ Programming Language mentioned ambiguity that causes the most vexing parse. I think you are talking about undecidability. That achevement is newer, it needs templates, but it's in C++98, already.

That's why GCC stopped trying to bend bison to it's will and switched to completely ad-hoc parser.

> by this time it's no longer possible IMHO to add something this fundamental and have it MEAN something.

Why not? All these things are fixable in the Ship of Theseus fashion.

But for that you need the community that is willing to change. And C/C++ community doesn't fit the bill.

That is the critical failure of that plan, everything else is fixable.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 5, 2023 18:17 UTC (Sun) by anton (subscriber, #25547) [Link] (7 responses)

How should the compiler know which result the -O0-compiled code should generate?
The compiler writer decides one the behaviour for the platform once, and sticks with it. E.g., if the compiler writer decides to compile + to behave like an add instruction on the Alpha, and * into a mul instruction, it is fine to optimize l1+l2*8 to use an s8add instruction, because this instruction behaves the same way as a sequence of mul (by 8) and add. Once you have decided on the add behaviour, it is not ok to use an addv instruction for +, because that behaves differently (it traps on signed overflow).

As for -O0-compiled: If you follow the discipline above, all optimization levels behave the same way, so why talk about -O0? Plus, in compilers that assume that programs don't exercise UB, -O0 does not help: I have seen gcc assume that signed overflow does not happen with -O0.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 6, 2023 6:46 UTC (Mon) by smurf (subscriber, #17840) [Link] (6 responses)

> The compiler writer decides one the behaviour for the platform once, and sticks with it

Oh, how nice, you now have platform-dependent code, thus zero guarantees that your "x86-64" code will run on tomorrow's ARM64 or MIPS platforms. And, crucially, no way to find out – because C++ standard is perfectly OK with the compiler not telling you.

Also, compiler writers don't "decide on the behavior". They decide on a series of optimization steps which are assumed to be correct transformations – assuming that there are no UB conditions.

How the heck should they know that introducing or expanding one such step affects your UB code? it's explicitly out of scope.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 6, 2023 8:34 UTC (Mon) by anton (subscriber, #25547) [Link] (5 responses)

Oh, how nice, you now have platform-dependent code, thus zero guarantees that your "x86-64" code will run on tomorrow's ARM64 or MIPS platforms.
Correct. In that respect this discipline does not improve on the current situation. But the improvement is that existing, tested programs still work on the next version of the compiler, unlike in the current situation.

While portability is a worthy goal, and Java shows that it can be achieved, the friendly C story shows that it is one bridge too far for C.

Compiler writers certainly decide on the behaviour, including for the UB cases. E.g., on the Alpha the gcc maintainers decided to compile signed + to add, not to addv; both behave the same way for code that does not exercise UB. They also make a decision when they implement an "optimization" that, e.g., "optimizes" x-1>=x to false. The problem with such "optimizations" is that 1) this behaviour is not aligned with the behaviour of x-1 in the usual case and 2) in cases that should be equivalent (e.g., when the x-1 happens in a different compilation unit than the comparison) the behaviour is different.

How should the compiler writers know? They generally don't, therefore in the usual case they should preserve the behaviour on optimizations. For exceptions read section 5 of this paper.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 6, 2023 12:12 UTC (Mon) by smurf (subscriber, #17840) [Link] (4 responses)

> the improvement is that existing, tested programs still work on the next version of the compiler, unlike in the current situation.

Random changes in the optimizer can still break your code.

"Random" in the sense that they seem to be unrelated to the code in question. Modern compilers contain a lot of optimizer passes and rules/patterns they apply to your code. Given the mountain of UBs in C++ it's not reasonable to demand that changes in these rules and patterns never affect the result when any of them is violated.

It's not reasonable to demand that the next version of a compiler should not come with any improvements to its optimizer.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 8, 2023 9:03 UTC (Wed) by anton (subscriber, #25547) [Link] (3 responses)

A correct optimization must not change the behaviour, not that of the transformed code, and not that of seemingly unrelated code. Even UB fans accept this, they only modify the definition of behaviour to exclude UB. So anyone working on an optimizer has to reason about how an optimization affects behaviour.

And the fact that C (and maybe also C++, but I don't follow that) compiler writers offer fine-grained control over the behaviour they exclude, with flags like -fwrapv, -fwrapv-pointer, -fno-delete-null-pointer-checks, -fno-strict-aliasing etc. shows that they are very confident that they can control which kind of behaviour is changed by which optimization.

It's not reasonable to demand that the next version of a compiler should not come with any improvements to its optimizer.
That's a straw man argument.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 8, 2023 9:34 UTC (Wed) by smurf (subscriber, #17840) [Link] (2 responses)

> anyone working on an optimizer has to reason about how an optimization affects behaviour

… unless that behaviour is related to UB, in which case most bets are off.

Sure you can control some optimizer aspects with some flags, but (a) that's too coarse-grained (b) there's heaps of UB conditions that are not related to the optimizer and thus cannot be governed by any flags (c) replacing some random code (random in the sense of "if it's UB the compiler can do anything it damn well pleases") with some slightly less random code doesn't fix the underlying problems (d) there's plenty of UB that isn't related to the optimizer.

Contrast all of this that with Rust's definition of soundness, which basically states that if you don't use the "unsafe" keyword you cannot cause any UB behavior, period end of discussion.

C/C++ is lightyears away from that.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 8, 2023 10:53 UTC (Wed) by farnz (subscriber, #17727) [Link] (1 responses)

I think you're being a bit harsh on those flags - they define behaviours that are UB in the standard, and require the compiler to act as-if those behaviours are fully defined in the documented manner.

The problem with the flags is that people aren't willing to take any hit, no matter how minor, in order to have those flags on everywhere, preferring to stick to standard C, not that there exist flags that reduce the amount of UB you can run across.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 8, 2023 11:49 UTC (Wed) by khim (subscriber, #9252) [Link]

The problem are not the people who specify or not specify these flags, but people who don't even think about these flags and UBs related to these flags.

They apply “common sense” and refuse to divide problems in parts. They mash everything (millions of lines of code) into one huge pile and then try to reason about that. Here's perfect example: the gcc maintainers decided to recognize this idiom in order to pessimize it.

How that crazy conclusion was reached? Easy: ignore the CPU model that gcc uses, ignore the process that GCC optimizer uses, imagine something entirely unrelated to what happens in reality, then complain about object of your imagination that it doesn't work as you expect.

It's not possible to achieve safety if you do that! If you refuse to accept reality and complain about something that doesn't exist then it's not possible to do anything to satisfy you. It's as simple as that.

P.S. It's like a TV repairer who refuses to ever look on what's happening inside and just tries to “fix” things by punching TV from different directions. Decades ago when TV included half-dozen tubes this even worked and some such guys even knew how to “fix” things by punching them lightly and harshly. But after TVs have become more complicated they stopped being able to do their magic. And modern TVs don't react to punches at all. Similar thing happened to compilers. Same result: A new scientific truth does not triumph by convincing its opponents and making them see the light, but rather because its opponents eventually die and a new generation grows up that is familiar with it. And the best way to achieve that is to use some other language: new generation ,may learn Ada or Rust just as easily as they may learn C++ (easier, arguably) and there are no need for opponents to physically die off, if they would just stop producing new code the end result would be approximately the same.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 7, 2023 7:14 UTC (Tue) by mathstuf (subscriber, #69389) [Link] (13 responses)

> 2. Translate it as-is, and whatever happens on this CPU, happens

If it is translated as-is, it means that an optimizer is not allowed to touch it. An "as-is" rule is far more restrictive on code transformations than an "as-if" rule. Of course, this also assumes that targets even have operations for the relevant abstract operation (e.g., consider floating point-lacking hardware).

Here's a question: C doesn't have a way to specify "fused multiply and add" at all. Should C offer a library intrinsic to access such instructions? Require inline assembly? If a processor supports `popcount`, what do you want me to do to my source to access it besides something like `x && x & (x - 1) == 0` becoming `popcount x == 1`. After all, I wrote those bitwise operations, I'd expect to see them in the assembly in your world, no?

> In that case the language should declare a single correct interpretation and emulate it as necessary on every CPU.

Sure, except that we have noises about *bounds* checking being too intrusive and expensive. What makes you think that every `(var >> nbitshift)` expression being guarded with some check/sanitizing code would be acceptable?

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 7, 2023 9:08 UTC (Tue) by anton (subscriber, #25547) [Link]

C doesn't have a way to specify "fused multiply and add" at all. Should C offer a library intrinsic to access such instructions?
Looking at the output of man fma, it reports that the functions fma(), fmaf(), and fmal() are conforming to C99.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 7, 2023 10:52 UTC (Tue) by farnz (subscriber, #17727) [Link] (11 responses)

Forget bounds checking being too expensive; we have noises about -fwrapv being too expensive, and "all" that does is say that signed integer overflow/underflow is defined in terms of wrapping a 2s complement representation. If your code is already safe against UB, then this flag is a no-op; it can only cause performance issues if you could have signed integer overflow causing issues in your code.

If you can't get agreement on something that trivial, where the performance cost (while real, as shown by SPECint 2006) can be deal with by relatively simple refactoring to make things that should be unsigned into actual unsigned types instead of using int for everything, what hope is there for fixing other forms of UB?

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 7, 2023 17:35 UTC (Tue) by adobriyan (subscriber, #30858) [Link] (10 responses)

> we have noises about -fwrapv being too expensive

-fwrapv gains/losses are trivial to measure in theory:
Gentoo allows full distro recompile with seemingly arbitrary compiler flags.
I was using "-march=native" more or less since the moment it was introduced.

> If you can't get agreement on something that trivial, where the performance cost (while real, as shown by SPECint 2006) can be deal with by relatively simple refactoring to make things that should be unsigned into actual unsigned types instead of using int for everything, what hope is there for fixing other forms of UB?

Making types unsigned is not simple, the opportunities for introducing new bugs are limitless.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 7, 2023 17:43 UTC (Tue) by farnz (subscriber, #17727) [Link] (9 responses)

-fwrapv slows some sub-tests in SPECint 2006 by around 5% to 10% as compared to -fno-wrapv. This is unacceptably large, even though in the cases where it regresses, someone's already done the analysis to confirm that it regresses because it used int for array indexing instead of size_t.

And note that, by the nature of -fwrapv, every case where it regresses performance is one where the source code is already buggy, because it depends on UB being interpreted in a way that suits the programmer's intent, and not in a different (but still legal) way. It cannot change anything where the program's behaviour was fully defined without -fwrapv, since all -fwrapv actually does is say "these cases, which used to be Undefined Behaviour, now have the following defined semantics". But that was already a legal way to interpret the code before the flag changed the semantics of the language, since UB is defined as "if you execute something that contains UB, then the entire meaning of the program is undefined and the compiler can attribute any meaning it likes to the source code".

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 7, 2023 18:34 UTC (Tue) by smurf (subscriber, #17840) [Link] (2 responses)

> This is unacceptably large

… if you value "no performance regression even on UB-buggy code" higher than "sane(r) semantics and less UB".

As long as people who think along these lines are in charge of the C/C++ standards, Stroustrup’s plan (or indeed any plan to transform the language(s) into something safe(r)) has no chance whatsoever to get adopted.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 9, 2023 14:43 UTC (Thu) by pizza (subscriber, #46) [Link]

> As long as people who think along these lines are in charge of the C/C++ standards,

That's a little disingenuous; it's not that they're "in charge of C/C++" it's that there's a large contingent of *users* of C/C++ that ARE VERY VERY VOCAL about performance regressions in _existing_ code.

There's a *lot* of existing C/C++ code out in the wild, representing a *lot* of users. And many of those users are pulling the C/C++ standards in mutally-incompatible ways.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 16, 2023 20:14 UTC (Thu) by mathstuf (subscriber, #69389) [Link]

The plan (AFAICT) seems to be along the lines of defining rules that code must obey in order to satisfy profiles. Once the rules are known, language and library constructs can be analyzed to see if they adhere to those rules and called out by the compiler from there. While it will take time for code to be under a set of profiles that gets Rust-like safety, I think it is probably the most reasonable plan given the various constraints involved. But with this, one can start putting code behind "we checked for property X and want the compiler to enforce it from here on out" until you can start to say it project-wide and then start flipping the switch to say "we know we cannot adhere to property X for this function" getting something like Rust's `unsafe` "callout" that something special is going on here.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 9, 2023 23:15 UTC (Thu) by foom (subscriber, #14868) [Link] (5 responses)

> And note that, by the nature of -fwrapv, every case where it regresses performance is one where the source code is already buggy

Nope.

The flag only affects the _results_ if the program previously exhibited UB, but, it removes flexibility from the optimizer by requiring the result be wrapped. This may require additional conditions or less efficient code. If the more-optimal version didn't produce the correct result when the value wrapped, it cannot be used any more.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 10, 2023 13:55 UTC (Fri) by Wol (subscriber, #4433) [Link] (4 responses)

Which is why my take is that - ON A 2S COMPLEMENT PROCESSOR - -fwrapv should be defined as the default. So by default, you get the expected behaviour.

Then they can compile SPECInt with a flag that switches off fwrapv to give the old behaviour and say "you want speed? Here you are! But it's safe by default".

So UB has now become hardware- or implementation-defined behaviour but the old behaviour is still available if you want it.

Cheers,
Wol

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 10, 2023 14:00 UTC (Fri) by farnz (subscriber, #17727) [Link]

Now go and convince the Clang, GCC, Fedora or Debian maintainers that this should be the default state. That's the hard part - getting anyone whose decisions will influence the C standards body to declare that they want less UB, even at the expense of a few % of speed on some benchmarks.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 13, 2023 13:13 UTC (Mon) by paulj (subscriber, #341) [Link] (2 responses)

Is there anything that's been produced in the last 10 years that is /not/ twos-complement? (If "nothing" - in the last 20?)

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 13, 2023 17:14 UTC (Mon) by kreijack (guest, #43513) [Link]

> Is there anything that's been produced in the last 10 years that is /not/ twos-complement? (If "nothing" - in the last 20?)

My understanding is that the ISO c++20 standard already mandates the two's complement:

If you look at https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2218.htm, you can find more information; even an analysis about which processor is/was not "two's complement".

And also it seems that C23 also is following the same path.

Anyway I think that the right question is "which architecture" supported by GCC (or CLANG...) is/isn't two's complement.

https://en.wikipedia.org/wiki/C23_(C_standard_revision)#cite_note-N2412-62
https://en.wikipedia.org/wiki/C%2B%2B20#cite_note-32
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/...

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 14, 2023 14:30 UTC (Tue) by mathstuf (subscriber, #69389) [Link]

For "basic" operations, probably very few. What about vectorized operations? Are they consistently twos-complement? How about GPU and other specialized hardware?

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 3, 2023 16:16 UTC (Fri) by Wol (subscriber, #4433) [Link] (2 responses)

> The only way to achieve any semblance of “sanity” is to discuss these things, write them into a language spec and then ignore “what the hardware is doing” from that point on.

OR you say "all those options are possible, there are flags to specify which one you expect, and if you don't specify you are explicitly choosing whatever the hardware happens to give you".

Personally, I think I'd expect option 3 - right-shifting 2 33 times looks like 0 to me. If I have a flag to say "that is what I expect", then the compiler can either do what I asked for, or tell me "you're on the wrong hardware". Or take ten times as long to do it. All those options comply with "least surprise". Maybe not quite the last one, but tough!

If I don't tell it what I want, and I'm on a partially vectorised chip, more fool me!

But the compiler is not free to reduce my program to the null statement because I didn't realise I was in the wrong reality!

Cheers,
Wol

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 3, 2023 17:02 UTC (Fri) by khim (subscriber, #9252) [Link]

> OR you say "all those options are possible, there are flags to specify which one you expect, and if you don't specify you are explicitly choosing whatever the hardware happens to give you".

Nope, that doesn't work. That's precisely what C had before C90 and it was a nightmare. Writing any non-trivial code which would work on more than one compiler (and often on just one version of one compiler) wasn't possible.

The problem here is that people don't want any rules.

What they actually want is that “simple” property: if my program stops working then I should be able to incrementally change it to find out where.

And they refuse to accept that this “simple” property is not possible to guarantee.

It's no wonder that developers of lowest part of the stack accepted Rust first: they know that hardware today is in state where it's not actually possible to support that properly… and if hardware couldn't provide it then compiler couldn't do that, either.

But there are still old-school developers who still remember times when that was actually possible. They don't want to change anything, they want to return these “good old times”… and they refuse to think about what made it possible to debug things reliably in these “good old times”.

And no, that wasn't because compilers were worse back then or because language was simple. It was possible to debug things simply because they were so simple: number of transistors in CPUs were measured in thousands, memory was measured in kilobytes… it was just simply impractical to implement sophisticated algorithms which can generate unpredictable results if applied few times.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 7, 2023 7:07 UTC (Tue) by mathstuf (subscriber, #69389) [Link]

> Personally, I think I'd expect option 3 - right-shifting 2 33 times looks like 0 to me. If I have a flag to say "that is what I expect", then the compiler can either do what I asked for, or tell me "you're on the wrong hardware". Or take ten times as long to do it. All those options comply with "least surprise". Maybe not quite the last one, but tough!

Note that optimization passes tend not to be aware of the literal input source or, necessarily, the target. Without that knowledge, it would mean that any optimization around a shift with a variable on the right is impossible to do because it could be doing What Was Intended™ and assuming any given behavior may interfere with that.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 3, 2023 14:10 UTC (Fri) by farnz (subscriber, #17727) [Link] (14 responses)

Yes, but in a language that cared about safety, given that -fwrapv is a clear win for safety, a wash in performance on most benchmarks, and a slight but easily fixed loss (under 10%) in some benchmarks, the language community would decide that if you run with -fno-wrapv, you're doing something you know is dangerous, and therefore we don't need to care about coding defensively for that case - you've explicitly asked for pain.

Instead, because there's a performance regression for a set of users who could refactor to recover the full performance if they cared, the language remains stuck with a situation where you have to be careful to not overflow your signed integers, just in case.

And that's the problem tradeoff - you can insist on -fwrapv, and accept that people who are careless lose up to 10% performance as compared to people who take care to use the right types, but that there's no UB, or you can say that -fno-wrapv is the default, and insist that people make sure that there's no UB manually, even if that makes their implementation bigger and slower than an implementation that relies on the changes in the -fwrapv dialect of C++. If the standards bodies and the compiler authors said "screw the 10% performance cost on badly written benchmark code like the examples in SPECint 2006, we'd prefer to define more behaviour", then I'd think that there's a chance of the C++ community choosing to get rid of UB. But with a well-defined change to remove a commonly misunderstood chunk of UB, which regresses performance slightly in a way that's easy to fix if you're affected, the community chooses to keep UB (no diagnostic required) instead of removing the UB, or at least requiring a diagnostic - and that makes me sceptical of any attempt to make the language contain less UB.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 3, 2023 15:51 UTC (Fri) by vadim (subscriber, #35271) [Link] (12 responses)

Oh, that's not quite what I mean.

What I mean that declaring a thing UB is in some cases a net loss. Eg, take testing for underflow.

If we want to test whether signed "x" is as small as it can get, we could use "if (x-1>x)". But since that's UB, what we have to write instead is "if(x==LONG_MIN)". And that actually turns out to take more bytes of code, because LONG_MIN is a 64 bit constant.

So it's not entirely true that UB's existence is a positive from the point of view of optimization. Sometimes having to tiptoe around it means you have to write worse code.

And even if -fwrapv exists, maybe you're writing this in a header in a library and therefore can't expect that flag to be used, and therefore must use the more roundabout implementation.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 3, 2023 16:18 UTC (Fri) by khim (subscriber, #9252) [Link] (6 responses)

> But since that's UB, what we have to write instead is "if(x==LONG_MIN)"

No. You can write something like if ((long)((unsigned long)x-1UL)>x). This code doesn't have UB and doesn't need LONG_MAX constant.

> What I mean that declaring a thing UB is in some cases a net loss.

Sure, but that's discussion for the time where one defined rules of the game. For example some people sat that defining left and right shifts as they are defined in Rust (runtime error in debug mode, wrapping on release mode) was a mistake.

But as long as these are the rules of the game everyone have to follow.

> So it's not entirely true that UB's existence is a positive from the point of view of optimization.

Without UB optimizations are simply not possible. Proof: you still haven't told us what gives someone the right to remove useless code from that foo function here except for that rule about correct program (they shouldn't contain UB).

> Sometimes having to tiptoe around it means you have to write worse code.

Writing code with UB is like crossing the street on red: you may save 1 minute 100 times, but sooner or later you'll be hit by semi and spend a lot of time in hospital (if you would survive at all).

The end result is net loss. Don't do that. > And even if -fwrapv exists, maybe you're writing this in a header in a library and therefore can't expect that flag to be used, and therefore must use the more roundabout implementation.

It's not too much roundabout since there are no overflow for unsigned numbers. But, ultimately, it's not important: while I agree that list of UBs in C/C++ is, frankly, insane, that's not the main problem. The main problem are people which refuse to play by rules.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 5, 2023 17:49 UTC (Sun) by anton (subscriber, #25547) [Link] (5 responses)

You can write something like if ((long)((unsigned long)x-1UL)>x). This code doesn't have UB and doesn't need LONG_MAX constant.
I have tried something like this, and some version of gcc (IIRC in the 3.x days, but I have not tried it later) "optimized" it to false. So I have switched to comparing with LONG_MIN, and last I looked (gcc-10.3) gcc did not optimize it into small code (even though that would be a proper optimization, not an "optimization" that assumes that the program does not exercise undefined behaviour). So in this case a UB "optimization" lead to inefficiency; maybe it was a bug in the compiler, but that does not help, I still have to work around it; and the source of the bug (if it is one) is the idea that you can "optimize" by assuming that UB is never exercised.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 5, 2023 18:28 UTC (Sun) by khim (subscriber, #9252) [Link] (4 responses)

> last I looked (gcc-10.3) gcc did not optimize it into small code (even though that would be a proper optimization, not an "optimization" that assumes that the program does not exercise undefined behaviour)

That's very strange. For me it does optimize that to small code — if you request small code.

Otherwise it produces faster code, why shouldn't it do that?

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 6, 2023 9:06 UTC (Mon) by anton (subscriber, #25547) [Link] (3 responses)

As is usual for advocates of undefined behaviour, you make claims about "faster code" without presenting any evidence. This is especially unbelievable in the present case. The two code sequences are:
movabs $0x8000000000000000,%rcx
cmp    %rcx,%rdx
je     ...
(15 bytes) and
cmp    $0x1,%rdx
jo     ...
(6 bytes) or (if rdx is dead afterwards)
dec    %rdx
jo     ...
(5 bytes). What makes you think that the longer sequence is faster?

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 6, 2023 10:44 UTC (Mon) by farnz (subscriber, #17727) [Link] (2 responses)

If I run the code through llvm-mca, which uses machine models to determine the cycle time of a given sequence of code, I see that the longer sequence generated by GCC is expected to take 8 cycles when running out of L1$, while the shorter sequence is expected to take 10 cycles when running out of L1$. The longer sequence executes 3 µops, while the shorter one executes 4 µops.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 6, 2023 18:49 UTC (Mon) by anton (subscriber, #25547) [Link] (1 responses)

Interesting tool. What it gives me for "100 iterations" (default) of the three sequences above is the following number of cycles:
  1   2    3
 79  54  104 llvm-mca-11 --mcpu=tigerlake
 80  54  104 llvm-mca-11 --mcpu=znver2 seq3.s
154 104  104 llvm-mca-11 --mcpu=tremont
154 104  104 llvm-mca-11 --mcpu=silvermont
Silvermont is the slowest core I could get (I asked for bonnell, but llvm-mca rejected my request), tigerlake and znver2 (presumably Zen2) and tremont are the most recent ones available. Judging from the output, it gives me Skylake-X data for tigerlake, and Silvermont data for Tremont.

Anyway, with all these machine models sequence 2 above is faster than sequence 1. Sequence 3 is slower than sequence 2 on some machine models.

I'll have to measure on real hardware to see how well these models reflect reality.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 6, 2023 23:09 UTC (Mon) by anton (subscriber, #25547) [Link]

And here's the data on real hardware. I also include sequence 4 that is a more literal translation of the C code (gcc-10 -O -fwrapv produces this sequence):
	leaq	-1(%rdx), %rax
	cmpq	%rdx, %rax
	jg	...
I put 100 copies of these sequences in a row, but they check 4 different registers rather than just one (to avoid making the data dependence on the result of the dec instruction a bottleneck for this microbenchmark). The results are in cycles per sequence.
 1    2    3    4
1.02 0.54 0.54 0.55 Rocket Lake
0.54 0.54 0.52 0.53 Zen3
1.19 1.03 1.03 1.03 Tremont
2.05 1.06 2.05 1.55 Silvermont
2.42 1.43 1.27 3.09 Bonnell
So, concerning our original question, sequence 2 is at least as fast as sequence 1, and actually faster on 4 out of 5 microarchitecture, sometimes by a factor of almost 2. Even sequence 4, which is what gcc10 produces is faster on 4 out of 5 microarchitectures and is slower only on Bonnell, which has been supplanted by Silvermont in 2013. Sequence 3 is also at least as fast as sequence 1, and faster on 4 out of 5 microarchitectures. So the gcc maintainers decided to recognize this idiom in order to pessimize it. Strange.

Code can be found here.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 3, 2023 17:01 UTC (Fri) by farnz (subscriber, #17727) [Link]

The problem is that it's a net win as long as nobody needs to test the edges of UB. If you're at the edges, then you're at risk of finding out just where those edges are and losing out. For the specific case you've mentioned, though, you can get back to the optimal code in GCC and Clang with the __builtin_sub_overflow extension, which gives you overflow/underflow detection on arbitrary integral types.

As to the rest of it, that's precisely my point; because there's a couple of components of SPECint 2006 that regress with -fwrapv, you have to pay the pain of not being able to expect that people will use that flag or keep all the pieces. And that's a cultural thing - if C++ had a safety culture, you'd be able to expect that people would use -fwrapv, and ignore the people who don't do that.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 4, 2023 7:37 UTC (Sat) by adobriyan (subscriber, #30858) [Link] (3 responses)

> If we want to test whether signed "x" is as small as it can get, we could use "if (x-1>x)".
> But since that's UB, what we have to write instead is "if(x==LONG_MIN)".
> And that actually turns out to take more bytes of code, because LONG_MIN is a 64 bit constant.

From maintainability point of view it should be always x == LONG_MIN.

x-1>x requires fresh register which may be used somewhere, it execution shortest path by 1 instruction,
which cannot be processed in parallel with some other instructions.

Finally, gcc will optimise (x - 1 < x) with -fwrapv to x == LONG_MIN.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 5, 2023 17:59 UTC (Sun) by anton (subscriber, #25547) [Link] (2 responses)

x-1>x requires fresh register which may be used somewhere
Just as LONG_MIN.
Finally, gcc will optimise (x - 1 > x) with -fwrapv to x == LONG_MIN.
That's a pessimisation then. It did not pessimize it in this way when I last tried it.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 5, 2023 18:19 UTC (Sun) by adobriyan (subscriber, #30858) [Link] (1 responses)

> > Finally, gcc will optimise (x - 1 > x) with -fwrapv to x == LONG_MIN.
> That's a pessimisation then. It did not pessimize it in this way when I last tried it.

It is still micro-optimisation because "mov r64, LONG_MIN" doesn't have to wait for x, so cmp can be executed asap,
but "lea r64, [r64 - 1]" or equivalent must wait for x.

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 5, 2023 18:38 UTC (Sun) by anton (subscriber, #25547) [Link]

In my usage the result of the comparison is used by a highly predictable branch, so the supposed advantage vanishes on an OoO CPU (and it's unclear whether it exists on an in-order CPU). The size disadvantage, however, stays. And if gcc really recognizes this idiom, gcc's maintainers are extra-clueless if (on AMD64) they compile it into
mov r9, LONG_MIN
cmp r9, r10
je ...
rather than
dec r10
jo ...
which is much shorter and only one macro-instruction.

I won't go as far as suspecting wilful pessimization by the gcc maintainers ("you turned off our glorious -fno-wrapv optimizations, we will teach you by pessimising your code into the same code you would have gotten with -fno-wrapv").

Bjarne Stroustrup’s Plan for Bringing Safety to C++ (The New Stack)

Posted Nov 3, 2023 16:01 UTC (Fri) by khim (subscriber, #9252) [Link]

> the language community would decide that if you run with -fno-wrapv, you're doing something you know is dangerous, and therefore we don't need to care about coding defensively for that case - you've explicitly asked for pain.

For that to happen there need to be a language community and you have to kick out guys who are talking about “portable assembler” thingie.

It's like sports games: people may argue about whether it's Ok to shot one free throw or two free throws if someone tries to run with a ball, but if they wouldn't kick out guys who insist that they are capable of running with a ball and thus it should be permitted then playing game is just impossible.

Because that is, ultimately, the @vadim and @anton position: “who told you one can not run with a ball? I tried it and it works just fine”.

If you couldn't kick such guys from the game and are not allowed to refuse to play with them then it's pointless to talk about the rules.


Copyright © 2025, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds