This is probably the simplest useful assembler routine you will ever come across:
* Integer Log base 2 *)
function IntLog2 Value: integer ): integer;
asm
BSR EAX, EAX
end;
Of course, you might want to do some error checking. This version throws EInvalidOp when zero is passed as a parameter:
(* Integer Log base 2 with error checking *)
function IntLog2( Value: integer ): integer;
const TExceptClass: TClass = EInvalidOp;
Msg: string = 'Invalid input to logarithm function.';
asm
CMP EAX, 0
JG @@Ok
MOV DL,1
MOV EAX, TExceptClass
MOV ECX, Msg
CALL Exception.Create
CALL System.@RaiseExcept
RET
@@Ok:
BSR EAX, EAX
end;
Delphi does have a Log2() function, which
uses x86 floating-point instructions. Our function provides the equivalent of:
Trunc(Log2(SomeInteger)).
But this involves a parameter conversion, since Log2() takes extended
as a parameter, and an extra call to Trunc().
IntLog2 is about eight times faster.
How does the function work? It relies on the fact that a number written in any base is simply a sum of consecutive powers. Consider that for any number written in base B with n digits dn..d0, then its value is the sum of dnBn..d0B0. For example, in base 10, a number such as 12,345 is:
Value = 1·104 + 2·103 + 3·102 + 4·101 + 5·100 = 12,34510
And in binary:
Value = 1·213+ 1·212+ 0·211+ 0·210+ 0·29+ 0·28+ 0·27+ 0·26+ 1·25+ 1·24+ 1·23+ 0·22+ 0·21+ 1·20 = 12,34510
Take a look at the leftmost factor, which is the product of the digit at that position with the base, raised to its corresponding power. The power at this position is equal to Trunc(LogBase(Value)), which is also the digit's (0-numbered) position. So to compute Trunc(LogBase(Value)), we somehow need to find this factor. In our case, we need to find the number of digits when an integer is written in base 2. But since the leftmost digit in binary can only be 1, we only need to find the number of digits the number would have when written in base 2. Luckily, our hardware already represents integers in binary, so we simply need to count the number of binary digits. However, since our hardware uses fix-length memory locations and registers (eg, 16, 32, 64 bits), we actually need to find the position of the highest set bit within a register or memory location.
You might scan for the first set bit in EAX using:
function IntLog2( Value: integer ): integer;
asm
MOV ECX, 32
@@loop:
SHL EAX, 1
JC @@getout
LOOP @@loop
@@getout:
MOV EAX, ECX
DEC EAX
end;
But as we saw earlier, the Intel processor family provides a shortcut, in the form of the BSR instruction. Its format is: [BSR src, dest]. It scans src for the position of the highest set bit, counted from the rightmost (lowest) bit and stores the result in dest. Why EAX? With Delphi's register calling convention (the default), a 32-bit integer parameter and integer function result are both stored in EAX. This feature, which is not present in other RAD tools (and not guaranteed on C++ compilers) allows the function to be just one assembly instruction long.
Download
A unit containing the 2 versions can be downloaded as a zip file here.
--John Moore
(c) 2002 emil santos