The Art of
ASSEMBLY LANGUAGE PROGRAMMING

Chapter Nine (Part 1)

Table of Content

Chapter Nine (Part 3)

CHAPTER NINE:
ARITHMETIC AND LOGICAL OPERATIONS (Part 2)
9.2 - Logical (Boolean) Expressions

9.2 Logical (Boolean) Expressions

Consider the following expression from a Pascal program:

	B := ((X=Y) and (A <= C)) or ((Z-A) <> 5);

B is a boolean variable and the remaining variables are all integers.

How do we represent boolean variables in assembly language? Although it takes only a single bit to represent a boolean value most assembly language programmers allocate a whole byte or word for this purpose. With a byte there are 256 possible values we can use to represent the two values true and false. So which two values (or which two sets of values) do we use to represent these boolean values? Because of the machine's architecture it's much easier to test for conditions like zero or not zero and positive or negative rather than to test for one of two particular boolean values. Most programmers (and indeed some programming languages like "C") choose zero to represent false and anything else to represent true. Some people prefer to represent true and false with one and zero (respectively) and not allow any other values. Others select 0FFFFh for true and 0 for false. You could also use a positive value for true and a negative value for false. All these mechanisms have their own advantages and drawbacks.

Using only zero and one to represent false and true offers one very big advantage: the 80x86 logical instructions (and or xor and to a lesser extent not) operate on these values exactly as you would expect. That is if you have two boolean variables A and B then the following instructions perform the basic logical operations on these two variables:

                mov     ax
A
and     ax
B
mov     C
ax           ;C := A and B;

mov     ax
A
or      ax
B
mov     C
ax           ;C := A or B;

mov     ax
A
xor     ax
B
mov     C
ax           ;C := A xor B;

mov     ax
A           ;Note that the NOT instruction does not
not     ax              ; properly compute B := not A by itself.
and     ax
1           ; I.e.
(NOT 0)does not equal one.
mov     B
ax           ;B := not A;

mov     ax
A           ;Another way to do B := NOT A;
xor     ax
1
mov     B
ax           ;B := not A;

Note as pointed out above that the not instruction will not properly compute logical negation. The bitwise not of zero is 0FFh and the bitwise not of one is 0FEh. Neither result is zero or one. However by anding the result with one you get the proper result. Note that you can implement the not operation more efficiently using the xor ax 1 instruction since it only affects the L.O. bit.

As it turns out using zero for false and anything else for true has a lot of subtle advantages. Specifically the test for true or false is often implicit in the execution of any logical instruction. However this mechanism suffers from a very big disadvantage: you cannot use the 80x86 and or xor and not instructions to implement the boolean operations of the same name. Consider the two values 55h and 0AAh. They're both non-zero so they both represent the value true. However if you logically and 55h and 0AAh together using the 80x86 and instruction the result is zero. (True and true) should produce true not false. A system that uses non-zero values to represent true and zero to represent false is an arithmetic logical system. A system that uses the two distinct values like zero and one to represent false and true is called a boolean logical system or simply a boolean system. You can use either system as convenient. Consider again the boolean expression:

	B := ((X=Y) and (A <= D)) or ((Z-A) <> 5);

The simple expressions resulting from this expression might be:

        Temp2 := X = Y
Temp := A <= D
Temp := Temp and Temp2
Temp2 := Z-A
Temp2 := Temp2 <> 5
B := Temp or Temp2

The assembly language code for these expressions could be:

                mov     ax
x           ;See if X = Y and load zero or
cmp     ax
y           ; one into AX to denote the result
jnz     L1              ; of this comparison.
mov     al
1           ;X = Y
jmp     L2
L1:             mov     al
0           ;X <> Y
L2:
mov     bx
A           ;See if A <= D and load zero or one
cmp     bx
D           ; into BX to denote the result of
jle     ST1             ; this comparison.
mov     bl
0
jmp     L3
ST1:            mov     bl
1
L3:
and     bl
al          ;Temp := Temp and Temp2

mov     ax
Z           ;See if (Z-A) <> 5.
sub     ax
A           ;Temp2 := Z-A;
cmp     ax
5           ;Temp2 := Temp2 <> 5;
jnz     ST2
mov     al
0
jmp     short L4
ST2:            mov     al
1
L4:
or      al
bl          ;Temp := Temp or Temp2;
mov     B
al           ;B := Temp;

As you can see this is a rather unwieldy sequence of statements. One slight optimization you can use is to assume a result is going to be true or false and initialize the corresponding boolean result ahead of time:

                mov     bl
0           ;Assume X <> Y
mov     ax
x
cmp     ax
Y
jne     L1
mov     bl
1           ;X is equal to Y
so make this true.
L1:
mov     bh
0           ;Assume not (A <= D)
mov     ax
A
cmp     ax
D
jnle    L2
mov     bh
1           ;A <= D so make this true
L2:
and     bl
bh          ;Compute logical AND of results.

mov     bh
0           ;Assume (Z-A) = 5
mov     ax
Z
sub     ax
A
cmp     ax
5
je      L3:
mov     bh
1           ;(Z-A) <> 5
L3:
or      bl
bh          ;Logical OR of results.
mov     B
bl           ;Save boolean result.

Of course if you have an 80386 or later processor you can use the setcc instructions to simplify this a bit:

                mov     ax
x
cmp     ax
y
sete    al              ;TEMP2 := X = Y

mov     bx
A
cmp     bx
D
setle   bl              ;TEMP := A <= D
and     bl
al          ;Temp := Temp and Temp2
mov     ax
Z
sub     ax
A           ;Temp2 := Z-A;
cmp     ax
5           ;Temp2 := Temp2 <> 5;
setne   al
or      bl
al          ;Temp := Temp or Temp2;
mov     B
bl           ;B := Temp;

This code sequence is obviously much better than the previous one but it will only execute on 80386 and later processors.

Another way to handle boolean expressions is to represent boolean values by states within your code. The basic idea is to forget maintaining a boolean variable throughout the execution of a code sequence and use the position within the code to determine the boolean result. Consider the following implementation of the above expression. First let's rearrange the expression to be

        B := ((Z-A) <> 5) or ((X=Y) and (A <= D));

This is perfectly legal since the or operation is commutative. Now consider the following implementation:

                mov     B
1            ;Assume the result is true.
mov     ax
Z           ;See if (Z-A) <> 5
sub     ax
A           ;If this condition is true
the
cmp     ax
5           ; result is always true so there
jne     Done            ; is no need to check the rest.

mov     ax
X           ;If X <> Y
the result is false

cmp     ax
Y           ; no matter what A and D contain
jne     SetBtoFalse

mov     ax
A           ;Now see if A <= D.
cmp     ax
D
jle     Done            ;If so
quit.
SetBtoFalse:            mov     B
0            ;If B is false
handle that here.
Done:

Notice that this section of code is a lot shorter than the first version above (and it runs on all processors). The previous translations did everything computationally. This version uses program flow logic to improve the code. It begins by assuming a true result and sets the B variable to true. It then checks to see if (Z-A) <> 5. If this is true the code branches to the done table because B is true no matter what else happens. If the program falls through to the mov ax X instruction we know that the result of the previous comparison is false. There is no need to save this result in a temporary since we implicitly know its value by the fact that we're executing the mov ax X instruction. Likewise the second group of statements above checks to see if X is equal to Y. If it is not we already know the result is false so this code jumps to the SetBtoFalse label. If the program begins executing the third set of statements above we know that the first result was false and the second result was true; the position of the code guarantees this. Therefore there is no need to maintain temporary boolean variables that keep track of the state of this computation.

Consider another example:

	B := ((A = E) or (F <> D)) and ((A<>B) or (F = D));

Computationally this expression would yield a considerable amount of code. However by using flow control you can reduce it to the following:

                mov     b
0            ;Assume result is false.
mov     ax
a           ;See if A = E.
cmp     ax
e
je      test2           ;If so
1st subexpression is true.

mov     ax
f           ;If not
check 2nd subexpression
cmp     ax
d           ; to see if F <> D.
je      Done            ;If so
we're done
else fall
; through to next tests.
Test2:          mov     ax
a           ;Does A <> B?
cmp     ax
b
jne     SetBto1         ;If so
we're done.

mov     ax
f           ;If not
see if F = D.
cmp     ax
d
jne     Done

SetBto1:        mov     b
1
Done:

There is one other difference between using control flow vs. computation logic: when using control flow methods you may skip the majority of the instructions that implement the boolean formula. This is known as short-circuit evaluation. When using the computation model even with the setcc instruction you wind up executing most of the statements. Keep in mind that this is not necessarily a disadvantage. On pipelined processors it may be much faster to execute several additional instructions rather than flush the pipeline and prefetch queue. You may need to experiment with your code to determine the best solution.

When working with boolean expressions don't forget the that you might be able to optimize your code by simplifying those boolean expressions (See Chapter Two). You can use algebraic transformations (especially DeMorgan's theorems) and the mapping method to help reduce the complexity of an expression.

Chapter Nine (Part 1)

Table of Content

Chapter Nine (Part 3)

Chapter Nine: Arithmetic And Logical Operations (Part 2)
27 SEP 1996