Date: Sat, 26 Aug 2023 21:04:03 -0700
On Saturday, 26 August 2023 17:21:02 PDT Jason McKesson via Std-Proposals
wrote:
> There's that phrase again: "obviously intended".
This is the crux of the problem: if it were possible to know what the
developer "obviously intended", we wouldn't have undefined behaviour. The
compilers would always compile to what the developer "obviously intended".
In the absence of mind-reading, there has to be a contract between developer
and compiler. That's the standard: it defines that when the developer writes
this, that happens. But it also sets certain boundaries, where the standard
explicitly says "I won't make any determinations about what shall happen if
you do this".
What you're arguing for is that the standard should define all behaviours, at
least under a certain language mode. You're not the first to argue this --
search the mailing list for the term "optimisationists" and you'll find posts
by another person whose name escapes me now who was arguing that our trying to
extract the most performance out of the computers was hurting our ability to
have safe code.
I hear you both, I appreciate the problem and I do think we need to revise
some of the recent changes, such as the start_lifetime_as(). You can see my
post on the subject on why that one in particular should never have existed.
But arguing for no UB at all is too steep of a hill to climb. In particular,
you're arguing effectively for another language inside of C++. (Maybe you
should give Rust a try)
I'll give you two examples of where the standard explicitly leaves UB because
it allows for "don't pay for what you don't need". And neither are signed
integer overflow:
1) shift beyond the width of a type
This is left undefined because processors have different behaviours when doing
shifting, usually because shifting used to be very slow (1 cycle per shift
count).. Some processors will mask off the number of bits in the count, so
shifting a 32-bit integer by 32 is the same as shifting by 0. Other processors
may not mask at all and would then shift the input out of the register,
resulting in zero. Some others may not mask by the type's width but use the
same machinery as a bigger type, so shifting by 32 would return 0, but
shifting by 64 would be a no-op.
x86 is actually all of the above. The SHL instruction on 8- and 16-bit
registers uses 5 bits of the shift count, so shifting by 16 shifts everything
out of those. But when you use 32- and 64-bit registers, it uses the type's
width, so shifting by 64 is a no-op.
2) converting an out-of-range floating point number to integer
This is also left undefined because different processors will do different
things. Some may return the saturated maximum and minimum, some others may
return a value in range that matches the original modulo the integer's width,
some others may return a sentinel value indicating overflow (x86/SSE is the
latter), whereas some others may set a flag indicating overflow and return
garbage. And then add to this FP emulation in software.
The latter discussion happened recently in the IETF CBOR mailing list, which
made me look up what Rust does. It does define what the result shall be
(saturated values, and zero for NaN), which is safe but requires more code.
Compare: https://rust.godbolt.org/z/8h6Mhf885
If your argument is "define what is today UB", then it requires more code and a
ton of time by standard writers and compiler implementers. If your argument is
"generate assembly with no assumptions", then the behaviour isn't portable. It
might not be even inside of a single processor architecture, q.v. fused
multiply adds, the extended precision x87 stack, and the DPPS/DPPD
instructions differing in behaviour between Atom and Big Core lines.
wrote:
> There's that phrase again: "obviously intended".
This is the crux of the problem: if it were possible to know what the
developer "obviously intended", we wouldn't have undefined behaviour. The
compilers would always compile to what the developer "obviously intended".
In the absence of mind-reading, there has to be a contract between developer
and compiler. That's the standard: it defines that when the developer writes
this, that happens. But it also sets certain boundaries, where the standard
explicitly says "I won't make any determinations about what shall happen if
you do this".
What you're arguing for is that the standard should define all behaviours, at
least under a certain language mode. You're not the first to argue this --
search the mailing list for the term "optimisationists" and you'll find posts
by another person whose name escapes me now who was arguing that our trying to
extract the most performance out of the computers was hurting our ability to
have safe code.
I hear you both, I appreciate the problem and I do think we need to revise
some of the recent changes, such as the start_lifetime_as(). You can see my
post on the subject on why that one in particular should never have existed.
But arguing for no UB at all is too steep of a hill to climb. In particular,
you're arguing effectively for another language inside of C++. (Maybe you
should give Rust a try)
I'll give you two examples of where the standard explicitly leaves UB because
it allows for "don't pay for what you don't need". And neither are signed
integer overflow:
1) shift beyond the width of a type
This is left undefined because processors have different behaviours when doing
shifting, usually because shifting used to be very slow (1 cycle per shift
count).. Some processors will mask off the number of bits in the count, so
shifting a 32-bit integer by 32 is the same as shifting by 0. Other processors
may not mask at all and would then shift the input out of the register,
resulting in zero. Some others may not mask by the type's width but use the
same machinery as a bigger type, so shifting by 32 would return 0, but
shifting by 64 would be a no-op.
x86 is actually all of the above. The SHL instruction on 8- and 16-bit
registers uses 5 bits of the shift count, so shifting by 16 shifts everything
out of those. But when you use 32- and 64-bit registers, it uses the type's
width, so shifting by 64 is a no-op.
2) converting an out-of-range floating point number to integer
This is also left undefined because different processors will do different
things. Some may return the saturated maximum and minimum, some others may
return a value in range that matches the original modulo the integer's width,
some others may return a sentinel value indicating overflow (x86/SSE is the
latter), whereas some others may set a flag indicating overflow and return
garbage. And then add to this FP emulation in software.
The latter discussion happened recently in the IETF CBOR mailing list, which
made me look up what Rust does. It does define what the result shall be
(saturated values, and zero for NaN), which is safe but requires more code.
Compare: https://rust.godbolt.org/z/8h6Mhf885
If your argument is "define what is today UB", then it requires more code and a
ton of time by standard writers and compiler implementers. If your argument is
"generate assembly with no assumptions", then the behaviour isn't portable. It
might not be even inside of a single processor architecture, q.v. fused
multiply adds, the extended precision x87 stack, and the DPPS/DPPD
instructions differing in behaviour between Atom and Big Core lines.
-- Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org Software Architect - Intel DCAI Cloud Engineering
Received on 2023-08-27 04:04:05