How Should You Write a Fast Integer Overflow Check?
blog.regehr.org
blog.regehr.org
int checked_add_n(int_t a, int_t b, int_t *rp) {
uint_t s = (uint_t)a + (uint_t)b;
*rp = s;
return ((a ^ s)&(b ^ s)) >> (BITS - 1);
}
This is essentially doing the same thing as checked_add_3. First, we extract the carry into the sign bit with ci = s ^ a ^ b; // upper bit of ci contains the relevant bit
Carry out can be found via the usual majority expression: co = ci&a ^ ci&b ^ a&b;
Xoring the two, and simplifying using De Morgan's laws and their relatives results in the expression above.Although the metric is far from optimal, clang gives me 6 arithmetic instructions (minus the ret): http://goo.gl/Amdo93
*rp = s <= INTN_MAX ? s : (intN_t)(s + INTN_MIN) + INTN_MIN;
"What the hell?” I hear the internet ask. if s <= INTN_MAX, then the result is representable as an intN_t, so we’re fine; otherwise s + INTN_MIN is guaranteed* to be representable as an intN_t, and the second addition of INTN_MIN “completes" the twos-complement (no-op) conversion to signed.“But you can’t assume that the signed type is twos complement!” an actually of pedants responds. Fortunately, the <stdint.h> intN_t types, if they exist, are required to be twos-complement.
“But more operations and—oh-horrors-a-branch!” say the premature optimizers. Not to fear. Compilers are smart. Both gcc and clang are happy to look through this and no-op the whole thing away.
[*] for the <stdint.h> intN_t types, which are twos-complement with no padding.
checked_add_3() has a lot of instructions that can be executed in parallel. checked_add_4() has a lot of conditional code which can be skipped. (Branching might be expensive in that case, though on the other hand overflow is rare, so these branches can probably be predicted accurately.)
I have little doubt that the assembly version is fastest in practice, but to compare the other versions, they should be benchmarked.
EDIT 1: as spotted by @pbsd this was not working good with some values...
I have added a way of checking the overflow and I have tested it:
#define BITS (8*sizeof(int))-1
int checked_add(int a, int b, int *rp)
{
*rp = a+b;
return (a^b) < 0 ? 0 : (a^(*rp)) < 0 ? 1 : 0) ;
}
It is around 30% faster than the fastest in the blog and I believe the code is way cleaner. What we are doing is checking if all of the values has the same sign (the "^" is just a "xor"), if so, no overflow in other case, overflow.This is the testing program (g++ compliant):
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#define BITS (8*sizeof(int))-1
int checked_add2(int a, int b, int *rp) {
uint ur = (uint)a + (uint)b;
uint sr = ur >> (BITS-1);
uint sa = (uint)a >> (BITS-1);
uint sb = (uint)b >> (BITS-1);
*rp = ur;
return
(sa && sb && !sr) ||
(!sa && !sb && sr);
}
int checked_add(int a, int b, int *rp)
{
*rp = a+b;
return ((a^b) | (a^(*rp)) < 0) ? 1 : 0 ;
}
int main(int argc, char* argv[])
{
int a, b, c;
long clong;
srand(time(NULL));
for(unsigned int i = 0; i < 50000000; ++i)
{
bool overflow = false;
a = rand();
b = rand();
clong = (long)a + (long)b;
overflow = checked_add(a,b,&c);
if(clong != (long)c && !overflow)
printf("Overflow not detected %i + %i = %i \n", a, b, c);
}
return 0;
}
Time for "checked_add": 2.694sTime for "checked_add2": 3.200s
This is the dump (6 instructions so far): http://goo.gl/cnBKdS
This actually easier to do in Java and many other languages because signed arithmetic is precisely defined.
if(checked_addr(a,b,rp)) {
//something 1
} else {
//something 2
}
then compiler is free to assume no overflow happens and replace code with: checked_add(a,b,rp);
//something 2edit: Ok, I do missed one spot (my mind tricked me), the answer shouldn't be 0.
return((a ^ b) | (a ^ (*rp)) < 0) ? 1 : 0;
doesn't it?
Child, this is why I always add unit tests.
I have also come to think "hey this perhaps could be a security hole" every time it comes to unchecked overflow. Does anyone know such exploits?
— Henry Petroski1. http://stackoverflow.com/questions/2913618/how-is-integer-ov...
2. http://rogunix.com/docs/Reversing&Exploiting/Detecting%20and...
3. http://www.exploit-db.com/wp-content/themes/exploit/docs/284...
Also, it is weird to write the result to memory just to avoid to repeat the addition. A memory operation is quite complex, but there a few things simpler than adding two registers. A better abstraction would be to write a function that returns the overflow bit and nothing else. The addition itself is (almost) free anyway.
[1] http://gcc.gnu.org/onlinedocs/gcc-3.4.6/gcc/Extended-Asm.htm...
Don't let the small clean assembly for this simple function fool you, writing optimized assembly on modern architectures is getting harder and harder, you have to take the cache into account for instance (both instruction and data) as well the particular implementation of the instruction on the CPU (microcode etc...). Fewer instructions does not always mean faster code, maybe TFA should have been more explicit about that instead of just saying it's "a crude measure of code quality". Look at the assembly generated by a compiler for a modern X86-64 architecture, you will see stuff that seem to make no sense such as NOPs in the middle of functions.
Also, in general, it's interesting to know what the compiler can and cannot optimize to decide when it's interesting to handcraft some assembly. It's always better to benchmark first instead of doing some premature optimization.
For instance, C does not have a bitwise rotation operator (while many architectures support a rotate instruction) but gcc easily recognizes rotation patterns and optimizes them without trouble.
>Don't let the small clean assembly for this simple function fool you, writing optimized assembly on modern architectures is getting harder and harder
Maybe in the general case, but this is literally a single instruction over unchecked arithmetic. You just add and then branch on overflow. Kinda hard to screw that up.
It's really sad that people are so afraid of assembly these days that they can't even bother to write, say, a couple lines of assembly for their language VM's overflow-checking add instruction, and rely on hacks like this instead. Who cares if pure C is more portable if the assembly is shorter and it takes two minutes at worst to look up what the opcode for "branch on overflow" is for any other platform you want to port to? At some point, you're not playing it safe or even saving much time, you're just wasting CPU for the sake of laziness.
So at that point it's probably valuable to see if you can't get the compiler to generate the correct code by itself in a portable way.
I'm not afraid of writing assembly when I have to but I always consider it a last resort scenario when I really can't get the same result with some good old C.
Better yet, imagine that the compiler has performed value range propagation [1] to determine that the range checks, after inlining, were redundant or simply elidable. This would be infeasible if it were written in assembly.
[1] http://llvm.org/devmtg/2007-05/05-Lewycky-Predsimplify.pdf
Better yet, imagine that the compiler has performed value range propagation [...] This would be infeasible if it were written in assembly.
VRP doesn't depend on the instruction set; it could be LLVM IR or it could be x86 or ARM or 6502 or anything else. It's just looking at values and operations on them.
To the compiler's own IR, whatever that is.
Maybe a better term is "premature nonportability".
public static int safeAdd(int l, int r) throws OverflowException {
if (r > 0
? l > Integer.MAX_VALUE - r
: l < Integer.MIN_VALUE - r)
throw new ArithmeticException(String.format(
"Integer overflow: %d + %d", l, r));
return l + r;
}https://www.securecoding.cert.org/confluence/display/java/NU...
In other words, the abstraction boundary introduced by the function call mechanism gets in the way of using the overflow flag directly with jo. Inlining would, presumably, re-expose the opportunity.
For most code overflows are rare, jumping to deal with the rare cases but otherwise just doing the normal is often best for whole program performance.
But to be honest I am not current in assembly as I like managed languages to much.
On a modern processor, a correctly predicted branch-not-taken will almost always be free. Not just inexpensive, actually free, since a superscalar processor can execute it on an otherwise unused port. A correctly predicted branch-taken will not take more than a single extra cycle. And branch prediction is good enough that you can presume that if a pattern exists, it will be correctly predicted.
The thing that can be expensive is a mispredicted branch in a tight loop. That will cost you about 15 cycles (about twice as much as accessing L1, and 1/10th the price of hitting RAM). In a case like this, unless you are expecting an unpredictable mix of overflow and non-overflow, branches are not a problem. If the overflow is going to happen less than 10% of the time, using "jo" (jump overflow) after a numeric operation will be the fastest option.
Anybody know the logic behind this?