On 7/22/2026 3:41 AM, David Brown wrote:
On 22/07/2026 07:33, Janis Papanagnou wrote:
On 2026-07-22 02:26, Keith Thompson wrote:
Janis Papanagnou <janis_papanagnou+ng@hotmail.com> writes:
There was some recent longish thread with discussions addressing
Undefined Behavior. I just stumbled across a paper from Russ Cox
on that topic with a provoking title. [...]
For those who don't follow the link, the full title of the paper is
"C and C++ Prioritize Performance over Correctness".ÿ (Your
paraphrase could be interpreted as advice, which I'm sure is not
how you intended it.)
It was actually a (deliberate) "marketing trick" to provoke interest
by reducing the title to an (IMO) yet more aggravating formulation.
Yes, it was not my intention to suggest performance over correctness.
(Would anyone?)
Unfortunately, the answer to your final question is yes - and that is
one of the reasons UB can be a problem.ÿ If by "correctness" you mean
"code does what it should", people do sometimes release code that they
know has bugs, but they know that fixing them would slow down the code - perhaps it is still good enough for their purposes at the time.
Or, the cliche: "It's not a bug, it's a feature..."
More relevant to this discussion, if by "correct" code you mean code
that has fully defined or appropriate implementation-defined behaviour
(most code does not need to be fully portable) according to the C
standards and/or platform or compiler extended semantics, then it is absolutely the case that programmers regularly prioritise performance
over correctness by relying on UB.ÿ The result is code that works efficiently and as intended at the time, using tools tested by the developer.ÿ Then the UB bites the next person down the road that uses
the same code in good faith, but with a different compiler or different options.
Well, or for example, this pile of wonk:
#define gfxedit_getu16(ptr) (*(vol_u16p)(ptr))
#define gfxedit_getu32(ptr) (*(vol_u32p)(ptr))
#define gfxedit_getu64(ptr) (*(vol_u64p)(ptr))
#define gfxedit_setu16(ptr,val) (*(vol_u16p)(ptr)=(val))
#define gfxedit_setu32(ptr,val) (*(vol_u32p)(ptr)=(val))
#define gfxedit_setu64(ptr,val) (*(vol_u64p)(ptr)=(val))
Insert other versions for different platform constraints.
But, say:
u64 gfxedit_getu64(void *ptr)
{ u64 v; memcpy(&v, ptr, 8); return(v); }
Being also possible, but a significant performance penalty on some targets.
The situation is potentially much worse for big endian machines, but
these are rare at this point.
Say:
u16 gfxedit_getu16(void *ptr)
{ u16 v; v=((byte *)ptr)[0]|
(((u16)((byte *)ptr)[1]))<<8); return(v); }
u32 gfxedit_getu32(void *ptr)
{ u32 v; v=gfxedit_getu16(ptr)|
(((u32)gfxedit_getu16(((byte *)ptr)+2))<<16); return(v); }
u64 gfxedit_getu64(void *ptr)
{ u64 v; v=gfxedit_getu32(ptr)|
(((u64)gfxedit_getu32(((byte *)ptr)+4))<<32); return(v); }
Though, for BE machines, one wouldn't want to overuse these.
...
And, the function:
byte *gfxedit_memlzcpyf(byte *dst, byte *src, int len)
{
byte *cs, *ct, *cte;
u64 v0, v1;
int d;
int i;
d=dst-src;
if(d>len)
{
cs=src; ct=dst; cte=dst+len;
v0=gfxedit_getu64(cs); v1=gfxedit_getu64(cs+8);
gfxedit_setu64(ct, v0); gfxedit_setu64(ct+8, v1);
cs+=16; ct+=16;
if(ct<cte)
{
v0=gfxedit_getu64(cs); v1=gfxedit_getu64(cs+8);
gfxedit_setu64(ct, v0); gfxedit_setu64(ct+8, v1);
cs+=16; ct+=16;
if(ct<cte)
{
v0=gfxedit_getu64(cs); v1=gfxedit_getu64(cs+8);
gfxedit_setu64(ct, v0); gfxedit_setu64(ct+8, v1);
cs+=16; ct+=16;
while(ct<cte)
{
v0=gfxedit_getu64(cs); v1=gfxedit_getu64(cs+8);
gfxedit_setu64(ct, v0); gfxedit_setu64(ct+8, v1);
cs+=16; ct+=16;
}
}
}
return(cte);
}
if(d<=8)
{
if(d==1)
{
v0=*(byte *)src; v0|=(v0<<8); v0|=(v0<<16); v0|=(v0<<32);
ct=dst; cte=dst+len;
while(ct<cte)
{ gfxedit_setu64(ct, v0); gfxedit_setu64(ct+8, v0); ct+=16; }
return(cte);
}
if(d==2)
{
v0=gfxedit_getu16(src); v0|=(v0<<16); v0|=(v0<<32);
ct=dst; cte=dst+len;
while(ct<cte)
{ gfxedit_setu64(ct, v0); gfxedit_setu64(ct+8, v0); ct+=16; }
return(cte);
}
if(d==4)
{
v0=gfxedit_getu32(src); v0|=(v0<<32);
ct=dst; cte=dst+len;
while(ct<cte)
{ gfxedit_setu64(ct, v0); gfxedit_setu64(ct+8, v0); ct+=16; }
return(cte);
}
if(d==8)
{
v0=gfxedit_getu64(src);
ct=dst; cte=dst+len;
while(ct<cte)
{ gfxedit_setu64(ct, v0); gfxedit_setu64(ct+8, v0); ct+=16; }
return(cte);
}
if(d<0)
{
cs=src; ct=dst; cte=dst+len;
while(ct<cte)
{
v0=gfxedit_getu64(cs); v1=gfxedit_getu64(cs+8);
gfxedit_setu64(ct, v0); gfxedit_setu64(ct+8, v1);
cs+=16; ct+=16;
}
return(cte);
}
if(d==3)
{
v0=gfxedit_getu32(src); v0=v0<<40; v0=(v0>>16)|(v0>>40);
ct=dst; cte=dst+len;
while(ct<cte)
{
gfxedit_setu64(ct+ 0, v0);
gfxedit_setu64(ct+ 6, v0);
gfxedit_setu64(ct+12, v0);
ct+=18;
}
return(cte);
}
if(d==5)
{
v0=gfxedit_getu64(src);
ct=dst; cte=dst+len;
while(ct<cte)
{ gfxedit_setu64(ct, v0); ct+=5; }
return(cte);
}
if(d==7)
{
v0=gfxedit_getu64(src);
ct=dst; cte=dst+len;
while(ct<cte)
{ gfxedit_setu64(ct, v0); ct+=7; }
return(cte);
}
cs=src; ct=dst; cte=dst+len;
while(ct<cte)
{ *ct++=*cs++; }
return(cte);
}
if(d>=16)
{
cs=src; ct=dst; cte=dst+len;
while(ct<cte)
{
v0=gfxedit_getu64(cs); v1=gfxedit_getu64(cs+8);
gfxedit_setu64(ct, v0); gfxedit_setu64(ct+8, v1);
cs+=16; ct+=16;
}
return(cte);
}
else
{
cs=src; ct=dst; cte=dst+len;
while(ct<cte)
{ v0=gfxedit_getu64(cs); gfxedit_setu64(ct, v0); cs+=8; ct+=8; }
return(cte);
}
}
Where one might ask, why not just:
byte *gfxedit_memlzcpyf(byte *dst, byte *src, int len)
{
int i, d;
d=dst-src;
if(d>len)
{ memcpy(dst, src, len); return(dst+len); }
if(d<0)
{ memmove(dst, src, len); return(dst+len); }
if(d==1)
{ memset(dst, *src, len); return(dst+len); }
for(i=0; i<len; i++)
dst[i]=src[i];
return(dst+len);
}
But, on the relevant implementation, trying to do the latter being
around an order of magnitude slower...
Note: Not on my target... Where I had added _memlzcpyf() as an extension
to the C library to avoid having to endlessly re-implement this stuff.
But, for other targets, it is still necessary.
Where, the 'f' variant is basically the variant that prioritizes speed
on the allowance that it may copy a little extra.
Well, except on SiFive chips where this in not the fastest option.
The fastest case for a SiFive style CPU being or similar, say:
byte *gfxedit_memlzcpyf(byte *dst, byte *src, int len)
{
byte *cs, *ct, *cte;
int i, d;
cs=src; ct=dst; cte=dst+len;
while(ct<cte)
{ *ct++=*cs++; }
return(cte);
}
But, for the original example, this works better on a CPU with higher instruction latency, but the ability to use misaligned loads and stores
with zero added cost (whereas, the SiFive chips have lower
per-instruction latency but a very steep cost for misaligned memory
access; basically in the form of hidden trap-and-emulate).
Where, say, both CPU's can run RISc-V code, but have significant
differences in performance characteristics...
One might also want to use the latter naive byte copy for big endian
RISC machines (like on PowerPC or similar), ...
So, it can all turn into a big mess of cruft and ifdef's.
And, No:
You can't just use memmove() here...
In this case the typical repeating bytes on self-overlap behavior being required for correct operation.
But, while small and elegant and portable, the above version would be undesirable due to being around 10x slower.
One could try to detect alignment and then have separate aligned vs
unaligned loops, but in the use-case it gains very little and may end up slower than always assuming misaligned (where it is a sort of chaos
where most of the copies are both misaligned and fairly short).
If the reliance on the UB was unintentional, it's a bug - programmers
are humans, they don't always know everything, and people make mistakes.
ÿBut some programmers do so /knowing/ that the code is UB.ÿ Sometimes
it is even stated in comments.ÿ The most common cases, IME, are reliance
on two's complement wrapping, and messing with pointer casts to access
data as different types, possibly with incorrect alignment.
As for the linked paper, I am not particularly impressed by its arguments.
I think it is fair enough to have different opinions about what should
or should not be UB in language standards - and it is inarguably the
case that some things that are UB could have defined behaviour at quite minor performance costs.ÿ (Note, however, that studies looking at the performance cost of disabling certain optimisations - such as "-fno- strict-aliasing" or "-fwrapv" - almost invariably use only x86 targets.
x86 processors are designed to get the best from poorly optimised code, because that's what is often run on them, while most other processors
except compilers to do more of the work.ÿ Compiler optimisations often
have a much bigger impact on non-x86 targets and on processors with
smaller relative memory latencies.)
The idea is that compilers should still try to produce efficient code
even with these semantic limitations.
Fundamentally, the idea of "UB" is that it describes code that does not
make sense, where there is no correct answer or meaningful specification
of behaviour.ÿ What should it mean to dereference a pointer that does
not point to a valid object of the type you are using?ÿ What should it
mean to store a large result in a target type that is too small?ÿ Code
that does this kind of thing is buggy - it cannot give correct answers. Nothing the compiler can do could make the code correct.
The compiler's logic then goes like this:
1. UB is incorrect code.
2. C is a "trust the programmer" language.
3. The compiler assumes the programmer has written correct code.
4. Therefore, the programmer has not written code with UB.
5. Therefore, UB cannot happen.
6. Therefore the compiler can optimise on that assumption, such as by skipping some code generation.
The only question is, is it a good idea for the compiler to be allowed
to make the program "more wrong" ?ÿ I don't think it matters as much as people often think.ÿ It's easy to find or create examples where compiler optimisations on UB can amplify errors.ÿ Equally, however, you can find
or create examples where assuming some kind of na‹ve "obvious" implementation of the UB code will also lead to bad results.ÿ Programs adding two positive integers are going to expect a positive result - a wrapped negative result is going to lead to tears.ÿ You need the same
kind of care in development and testing no matter how the compiler
treats its UB - and if you have done that job, these kinds of compiler optimisations do not significantly (IMHO) affect the risk of later
problems.
The killer point, however, is when you have received some code that you
can justifiably assume is correct and well tested, but contains reliance
on how certain types of UB happen to be implemented.ÿ It is not really
any different than code that makes other undocumented assumptions, such
as implementation-dependent details, reliance on certain files or OS features, timing requirements, etc.ÿ And compiling with "-fwrapv -fno- strict-aliasing" (if you are using a compiler that supports such
features) will mitigate the most common cases of reliance on UB.
In some cases, it makes sense to assume that some things formally
considered UB would be better considered as Implementation Defined.
In practice, it likely wouldn't make that big of a difference; since
many cases the compiler still treats these cases is implementation
defined rather than true UB.
In this case, this leaves "true UB" as mostly cases where no sensible or consistent behavior can be expected even within a given machine, say:
reliance on the values of uninitialized local variables;
going out of bounds on a conventional array (*1).
*1: I consider out-of-bounds within an array inside a struct to be a
partial exception, as one can reasonably assume that going OOB would
access other members within the same struct.
But, the relative ordering and layout of stack and global variables I considered to be outside the scope of where valid assumptions can be
made in C.
However, such assumptions could be made for assembler...
Granted, in premise one could do, say:
extern u64 arr1[4];
extern u64 arr2[4];
__asm {
.section .data
.global arr1
.global arr2
arr1:
.quad 0
.quad 0
.quad 0
.quad 0
arr2:
.quad 0
.quad 0
.quad 0
.quad 0
};
And, arguably in this case one can argue that it may be valid to assume
that arr1 overflows into arr2.
But, then again, one also can't rely on the portability of __asm or
__asm__ blocks between targets. So, like with the structs, this would
exist more as a partial exception.
This example being partly N/A for GCC or similar, which uses a different syntax for ASM blocks (well, and some compilers, like 64-bit MSVC,
having dropped support for inline ASM).
But, I had seen non-zero code which had relied on the assumption of
overflows carrying over consistently between global arrays.
In my compiler, such assumptions are not safe to be made, as the
compiler likes to shuffle the declarations around. Partly balancing randomization and efficiency; as it can be more efficient to arrange
variables by usage frequency, and functions by a combination of
frequency and proximity. Say, ideally keeping commonly called functions
closer together, and if possible also clustering callers and callees
closer together. Even in the absence of profiler feedback, it tends to
make it such that the "cold path" is less likely to be part of the core working set (and reducing the TLB miss frequency).
Did run into a little bit of a hassle recently that my compilers' output
tends to have a section alignment smaller than the page alignment, which turned into a bit of pain trying to set up page access permissions for
the kernel binary (moving away from just setting the whole thing as RWX).
Ended up adding a few special case flags to the implementation of the "mprotect" functionality:
INNER: Only applies to completely covered pages,
ignores partial pages.
Default: covers all pages in range.
ALLOW: Only allows new access, does not remove access.
DENY: Only removes access.
Default: New permissions fully replace old permissions.
These allowed dealing better with the partial page scenarios.
Can initially set the whole image READ|NOUSER;
Then use ALLOW to add WRITE/EXEC/USER to the relevant sections.
Maybe questionable, but there does exist a R|X|USER section.
Mostly has stuff for Syscalls and COM.
It is likely this part could be migrated into Userland.
Mostly it is a vestige of an earlier developmental stage.
As for the paper itself, I have a few points.
It follows the time-worn path of complaining that "modern" compilers
have distorted the "original" idea of UB and abuse it for optimisation
in ways that are dangerous to the uninitiated.ÿ First, optimisation
using UB is not new - I first used a compiler that assumed integer
overflow does not wrap over thirty years ago.ÿ Second, compilers already default to "-O0" - if you know so little about your tools that you can't enable warnings, you won't have optimisation either.ÿ Third,
improvements in compiler optimisation have gone hand-in-hand with improvements in compiler static error checking.ÿ Using "-Wall" along
with optimisation will eliminate the solid majority of the risks and
worries of the author.ÿ Fourth, the development of "sanitizers" not only allows far more UB to be found during testing, but it allows far more
bugs to be found than would be possible if the mistakes were /not/ UB - "-fsanitize=signed-integer-overflow" catches overflow bugs precisely
because it is UB in C, whereas if it were defines as wrapping then the mistake in the programmer's code would be correct as far as the language
is concerned.
For keeping old code working, it makes sense to assume wrapping.
But, maybe it could make sense to have a semantic distinction for cases
where wrapping is assumed vs where it is likely unintentional.
My proposal, if any, would be, say:
If "signed" is used explicitly, it means wrap-on-overflow is expected.
So, say:
int i;
signed int v;
If 'i' overflows, one can question whether or not it was intended; but
if 'v' overflows, assume it is intentional (as with 'unsigned').
This being in part because an explicit "signed" keyword is uncommon to
be used with normal counting/index type variables.
Compilers are, of course, not perfect.ÿ But they are usually pretty
good.ÿ Most of the paper could be scraped and replaced with the advice
of always using "-Wall", possibly with "-Wextra" and/or fine-tuning of
other warning flags, regardless of optimisation.ÿ And make use of
sanitizers while testing.ÿ And always test with solid optimisations
enabled (like "-O2"), though lower optimisations can be handy for fault- finding and debugging some aspects of code.
It repeats the myths about signed integer overflow being UB because some computers have different representations of signed integers - C uses implementation-defined behaviour to handle implementation differences,
and only resorts to UB when there is no sensible definition of "correct" behaviour, or where implementation-defined behaviour could have
significant overhead costs.
Both C++ (from C++20, IIRC) and C (from C23) allow only two's complement signed integer representations.ÿ Both committees made it clear that
signed arithmetic overflow is still UB, because it does not lead to a sensible answer and allows optimisations and additional error checking.
It is not hard to get wrapping behaviour if you want it (using
conversions to and from unsigned types), and C23 introduced standardised checked arithmetic functions.
As for infinite loops, loops with a constant controlling expression
(like "for (;;)" ) are exempt from the "assumed to terminate" clause in
C.ÿ I don't know if the example given was a bug in Clang or if it used
C++ and C++ has different rules here.
And in the next example, merging two loops into one, the merge won't introduce new bugs or race conditions - if another thread is using
"count2", then there needs to be some kind of coordination (like
atomics), and then the loops could not be merged anyway.ÿ I am not
convinced that allowing the compiler to assume termination of certain
types of loop is a particularly useful feature - it seems to me to be an added complication with very few potential benefits.ÿ But I can't see it being a problem for sensible working code either.
Don't dereference null pointers.ÿ It's a bad idea.
Dereferencing NULL is one of those cases where the most sensible option
is to treat it as a fault...
Most sensible option is to assume that trying to dereference NULL is unintentional (given the main purpose of NULL being as a "this object
doesn't exist" pointer).
But:
Assuming it doesn't happen and then removing the offending code path
entirely is also bad IMO. However, turning the code-path into a break instruction or similar (following any other externally visible side
effects) would still likely be acceptable IMO (or, at least, less bad in
this case than just continuing down the other path as if nothing had
ever happened).
Read the specifications of the functions you want to use.ÿ std::sort requires a comparison function with a strict weak ordering - give it something else, and it's a user bug.ÿ Perhaps std::sort could have been specified in a nicer way, but that is hardly a compiler problem.
--- PyGate Linux v1.5.18
* Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)