Languages which attempt to detect overflows have historically defined the associated semantics in ways that severely restricted what would otherwise have been useful optimizations. Among other things, while it will often be useful to perform computations in a different sequence from what is specified in code, most languages that trap overflows guarantee that given code like:
for (int i=0; i<100; i++)
{
Operation1();
x+=i;
Operation2();
}
if the starting value of x would cause an overflow to occur on the 47th
pass through the loop, Operation1 will execute 47 times and Operation2
will execute 46. In the absence of such a guarantee, if nothing else
within the loop uses x, and nothing will use the value of x following
a thrown exception by Operation1 or Operation2, code could be replaced
with:
x+=4950;
for (int i=0; i<100; i++)
{
Operation1();
Operation2();
}
Unfortunately, performing such optimizations while guaranteeing correct semantics in cases where an overflow would have occurred within the loop is
difficult--essentially requiring something like:
if (x < INT_MAX-4950)
{
x+=4950;
for (int i=0; i<100; i++)
{
Operation1();
Operation2();
}
}
else
{
for (int i=0; i<100; i++)
{
Operation1();
x+=i;
Operation2();
}
}
If one considers that a lot of real-world code uses loops that are more
involved, it will be obvious that optimizing code while preserving
overflow semantics is difficult. Further, because of caching issues, it's entirely possible that the increase in code size would make the overall program run more slowly even though there are fewer operations on the commonly-executed path.
What would be needed to make overflow detection inexpensive would be a
defined set of looser overflow-detection semantics which would make it easy for code to report whether a computation was performed without any overflows that might have affected the results(*), but without burdening the compiler with details beyond that. If a language spec were focused on reducing the cost of overflow detection to the bare minimum necessary to achieve the above, it could be made much less costly than it is in existing languages. I'm unaware of any efforts to facilitate efficient overflow detection, however.
(*) If a language promises that all overflows will be reported, then an expression like x*y/y
cannot be simplified to x
unless x*y
can be guaranteed not to overflow. Likewise, even if the result of a computation would be ignored, a language that promises to report all overflows will need to perform it anyway so it can perform the overflow check. Since overflow in such cases cannot result in arithmetically-incorrect behavior, a program would not need to perform such checks to guarantee that no overflows have caused potentially-inaccurate results.
Incidentally, overflows in C are especially bad. Although almost every hardware platform that supports C99 uses two's-complement silent-wraparound semantics, it is fashionable for modern compilers to generate code which may cause arbitrary side-effects in case of overflow. For example, given something like:
#include <stdint.h>
uint32_t test(uint16_t x, uint16_t y) { return x*y & 65535u; }
uint32_t test2(uint16_t q, int *p)
{
uint32_t total=0;
q|=32768;
for (int i = 32768; i<=q; i++)
{
total+=test(i,65535);
*p+=1;
}
return total;
}
GCC will generate code for test2 which unconditionally increments (*p) once and returns 32768 regardless of the value passed into q. By its reasoning, the computation of (32769*65535) & 65535u would cause an overflow and there is thus no need for the compiler to consider any cases where (q | 32768) would yield a value larger than 32768. Even though there is no reason that the computation of (32769*65535) & 65535u should care about the upper bits of the result, gcc will use signed overflow as justification for ignoring the loop.
checked { }
section to mark the parts of the code that should perform Arithmetic Overflow checks. This is due to performance(1..2_000_000).sum #=> 2000001000000
. Another one of my favorite languages:sum [1 .. 2000000] --=> 2000001000000
. Not my favorite:Array.from({length: 2000001}, (v, k) => k).reduce((acc, el) => acc + el) //=> 2000001000000
. (To be fair, the last one is cheating.)Integer
in Haskell is arbitrary-precision, it will hold any number as long as you don't run out of allocatable RAM.But with the 10,000,000,000 repetitions, the time taken by a check is still less than 1 nanosecond.
that's an indication of the loop being optimized out. Also that sentence contradicts previous numbers which appear very valid to me.