A Very Fast Integer Log2 Function

 

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.

 

 

 

He who hasn't hacked assembly language as a youth has no heart. He who does as an adult has no brain.

--John Moore


(c) 2002 emil santos

codexterity
ems ATSIGN codexterity PERIOD com