|
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.
|
Table of Content | Chapter Nine (Part 3) |
Chapter Nine: Arithmetic And Logical
Operations (Part 2)
27 SEP 1996