Creative Misuse of Branches

CS 301 Lecture, Dr. Lawlor

Overflow Checking in Assembly

C++ just blindly ignores overflow, so it's possible for a program to fail silently and give you the wrong answer.  In assembly, you can check the overflow flag with "jo" (jump-if-overflow), like this:
mov edi,1000000000

add edi,edi
jo uhoh ; error-check the add

mov eax,edi
ret ; ordinary return

uhoh:
mov eax,-999 ; Special value to indicate we had an overflow
ret ; error return

(Try this in NetRun now!)

This program will hence return the right answer, or admit something failed.  You do have to be careful to do a "jo uhoh" after every possible arithmetic instruction, since the overflow flag gets re-set after each "add".

warning: comparison between signed and unsigned integer expressions

Why do you care about all those silly jump instructions we talked about last class? 

Well, for one thing, it affects how you can write C++ code.  For example, this code produces a warning, then gives the wrong answer "999":
unsigned int i=3000000000;
if (i<-3) return 999; /* what?! */
else return 0;

(Try this in NetRun now!)

The problem is, in assembly language:
When comparing unsigned i to signed -3, which jump do you use?  Well, sadly, they both give the wrong answer!  This is a hardware limitation (no mixed-sign comparisons) that ends up as a language limitation (warning and wrong answer for mixed-sign comparisons). 

In the above code, if the compiler decides to perform the signed comparison:
   unsigned 3 billion < signed -3
(convert to signed)
   signed -1 billion < signed -3
 So this will return true: 3 billion is less than -3 if they're both converted to signed 32-bit values.

It doesn't actually help if we perform an unsigned comparison:
  unsigned 3 billion < signed -3
(convert to unsigned)
  unsigned 3 billion < signed 4 billion
This will also return true: 3 billion is less than -3 if they're both converted to unsigned 32-bit values.

On a 64-bit machine, you can fix this problem by typecasting both sides to a "long" before doing the compare.  This works because a signed long has enough bits to represent +3 billion without wraparound:
unsigned int i=3000000000;
if ((long)i<(long)-3) return 999;
else return 0;

(Try this in NetRun now!)

This fixes the problem, but "unsigned long" suffers from the same exact malfunction at 15 quadrillion:
unsigned long i=15000000000000000000ul;
if ((long)i<(long)-3) return 999;
else return 0;

(Try this in NetRun now!)

Bottom line: you can't reliably compare signed and unsigned numbers.  Pick one or the other, and stick with it.

Reminder: negative signed numbers are represented by setting the high bit, which corresponds to a really huge unsigned number; similarly, huge unsigned numbers like above correspond to negative signed numbers.

Low-Branch Bounds Checking

You can actually exploit the signed/unsigned comparison differences to speed up this common bounds-checking code:
int i=3;
const int n=10;
int arr[n];
int foo(void) {
if (i>=0 && i<n) return arr[i];
else return 0;
}

(Try this in NetRun now!)

Note that we have to do two separate tests on "i": it could be negative, so we check against zero; or it could be n or bigger, so we compare against n.  But if you look at the code the compiler generates, there's only one test on "i":
	mov    edx, <<i>>
mov eax,0x0
cmp edx,0x9
ja bad
... load up arr[i] ...
bad: ret
Wait, only one comparison, and it's an *unsigned* compare?  But "i" is signed!

This is intentional: if "i" is negative (signed), then treated as an unsigned number, it's huge.  This lets us replace a two-test signed "negative or too big" with a single unsigned "too big" test!

Conditionals via the Multiply Instruction

The simplest example of this sort of non-branch switching is just a multiply instruction.  For example, here "doit" determines whether the output will be 4, or 6:
for (int doit=0;doit<=1;doit++) {
int blended=4+doit*2;
std::cout<<"doit=="<<doit<<" -> "<<blended<<"\n";
}

(Try this in NetRun now!)

This outputs:
doit==0  -> 4
doit==1 -> 6
Program complete. Return 0 (0x0)
More generally, if you want to return A if doit is false (0), and B if doit is true (1), then you can do:
for (int doit=0;doit<=1;doit++) {
int A=4;
int B=6;
int blended=A+doit*(B-A);
std::cout<<"doit=="<<doit<<" -> "<<blended<<"\n";
}

(Try this in NetRun now!)

You also see this written as the algebraically equivalent "blended = (1-doit)*A+doit*B;".  This has four operations instead of the three above, so you don't see it as much anymore.

There are some interesting advantages of the multiply approach to conditionals.  One is you can do "fuzzy conditionals", where your code blends between A and B if doit is somewhere between 0 and 1:
for (float doit=0.0;doit<=1.0;doit+=0.25) {
int A=4;
int B=6;
float blended=A+doit*(B-A);
std::cout<<"doit == "<<doit<<" -> "<<blended<<"\n";
}

(Try this in NetRun now!)

This outputs:
doit == 0	->	4
doit == 0.25 -> 4.5
doit == 0.5 -> 5
doit == 0.75 -> 5.5
doit == 1 -> 6
Program complete. Return 0 (0x0)
Multiply-based conditionals work fine for a single digit, but they don't scale well to multiple digits.  For example, "0x4444 + 0x0100*(0x6666-0x4444)" gives you "0x226644", not "0x4644".