Handling integer overflowsteemCreated with Sketch.

in programming •  6 years ago  (edited)

In a previous article I showed that the Swift compiler checks for overflow after every mathematical operation, like this assembly fragment:

         imul    rdi, r8
         jo      .LBB1_10

That's part of the language; in C or C++ you have to do it yourself. Can we get the compiler to help out? Let's explore some options using the Compiler Explorer.

Detecting overflow with integer comparison

The code I'll work with is the following, which attempts to add two 64-bit values and add the carry in a second 64-bit output:

typedef unsigned long long u64;

// Add x to low, and add any carry to high
void add( u64 &high, u64 &low, u64 x ) {
    u64 orig = low;
    low += x;
    if ( orig > low ) {
        high += 1;
    }
}

GCC return compiles to this verison:

It successfully avoided an extra subtraction to do the comparison, and just checked the carry flag, but it used a branch to avoid the addition.

Clang manages to do it a little more simply, but still with a branch:

What's the best we can do? Well this case, we could be using the ADC (add-with-carry) instruction, which adds 1 to the result if the carry flag is set.

 clc    
 adc    QWORD PTR [rsi],rdx
 adc    QWORD PTR [rdi],0x0
 ret  

Or maybe even

 add    QWORD PTR [rsi],rdx   # no carry in, but carry out
 adc    QWORD PTR [rdi],0x0   # carry in *and* carry out
 ret  

I don't think we can convince the compiler to emit this version. In fact, the GNU MultiPrecision library (GMP) uses assembly for exactly this purpose, when it's adding two multi-word integers the inner loop looks like this (sorry for the switch away from Intel syntax):

L(top): ADCSBB  (vp), %r8
    ADCSBB  8(vp), %r9
    ADCSBB  16(vp), %r10
    ADCSBB  24(vp), %r11
    mov %r8, (rp)
    lea 32(up), up
    mov %r9, 8(rp)
    mov %r10, 16(rp)
    dec n
    mov %r11, 24(rp)
    lea 32(vp), vp
    mov (up), %r8
    mov 8(up), %r9
    lea 32(rp), rp
L(mid): mov 16(up), %r10
    mov 24(up), %r11
    jnz L(top)

The same code is used for both addition and subtraction, so ADDSBB is a macro. Source

A side node: the DEC instruction specifically doesn't touch the carry flag, so it can be used in cases like this one where we want to preserve the flag into the next iteration of the loop.

Detecting overflow directly with a builtin function

Both GCC and Clang support a builtin function which checks for overflow. To use it, we can write:

void add_ovf( u64 &high, u64 &low, u64 x ) {
    u64 out;
    if ( __builtin_add_overflow( low, x, &out ) ) {
        high += 1;
    }
    low = out;
}

GCC produces a little bit of a tangle:

While clang only jumps forward:

This gives the compiler a big hint that we are interested in the carry flag, which it successfully uses for the branch. But neither understands that the conditional branch could be replaced by ADC. I'm not quite sure why the compiler chose to add memory-to-register, then move the result back, rather than register-to-memory, It looks like ADD supports both.

Similar builtins exist for other operations subject to integer overflow.

Enabling runtime detection

The C and C++ standards explicitly state that there's no such thing as unsigned overflow! Unsigned integers are defined to wrap around modulo 2^N. But signed overflow is undefined behavior, and some compilers offer a flag that checks it. Let's simplify our function to:

void add( long long &low, long long x ) {
    low += x;
}

And compile with the command line flags -O3 -ftrapv. Sadly, the implementation of addition is now a function call!

So, we'd have to dig into GCC's runtime to figure out how they implemented it--- but because the integers are unsigned, the check is more complicated. I was easily able to find the LLVM version, which looks like this:

di_int
__addvdi3(di_int a, di_int b)
{
    di_int s = (du_int) a + (du_int) b;
    if (b >= 0)
    {
        if (s < a)
            compilerrt_abort();
    }
    else
    {
        if (s >= a)
            compilerrt_abort();
    }
    return s;
}
Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

I did find an example online

uint64_t adc(uint64_t a, uint64_t b)
{
    a += b;
    if (a < b) /* should simplify to nothing (setting carry is implicit in the add) */
        a++; /* should simplify to adc r0, 0 */
    return a;
}

Which does compile down to an ADC:

adc(unsigned long long, unsigned long long):
        add     rdi, rsi
        mov     rax, rdi
        adc     rax, 0
        ret

Source: https://stackoverflow.com/questions/17371570/getting-a-compiler-to-generate-adc-instruction

But this seems fragile and doesn't react well to changing any of the arguments to a structure or pointer or reference. It may be that if the original code was inlined, then ADC would be used.

Chic article. I learned a lot of interesting and cognitive. I'm screwed up with you, I'll be glad to reciprocal subscription))