简体   繁体   中英

UNDERSTANDING how to count trailing zeros for a number using bitwise operators in C

Note - This is NOT a duplicate of this question - Count the consecutive zero bits (trailing) on the right in parallel: an explanation? . The linked question has a different context, it only asks the purpose of signed() being use. DO NOT mark this question as duplicate.

I've been finding a way to acquire the number of trailing zeros in a number. I found a bit twiddling Stanford University Write up HERE here that gives the following explanation.

unsigned int v;      // 32-bit word input to count zero bits on right
unsigned int c = 32; // c will be the number of zero bits on the right
v &= -signed(v);
if (v) c--;
if (v & 0x0000FFFF) c -= 16;
if (v & 0x00FF00FF) c -= 8;
if (v & 0x0F0F0F0F) c -= 4;
if (v & 0x33333333) c -= 2;
if (v & 0x55555555) c -= 1;

Why does this end up working ? I have an understanding of how Hex numbers are represented as binary and bitwise operators, but I am unable to figure out the intuition behind this working ? What is the working mechanism ?

The code is broken (undefined behavior is present). Here is a fixed version which is also slightly easier to understand (and probably faster):

uint32_t v;  // 32-bit word input to count zero bits on right
unsigned c;  // c will be the number of zero bits on the right
if (v) {
    v &= -v; // keep rightmost set bit (the one that determines the answer) clear all others
    c = 0;
    if (v & 0xAAAAAAAAu) c |= 1; // binary 10..1010
    if (v & 0xCCCCCCCCu) c |= 2; // binary 1100..11001100
    if (v & 0xF0F0F0F0u) c |= 4;
    if (v & 0xFF00FF00u) c |= 8;
    if (v & 0xFFFF0000u) c |= 16;
}
else c = 32;

Once we know only one bit is set, we determine one bit of the result at a time, by simultaneously testing all bits where the result is odd, then all bits where the result has the 2's-place set, etc.

The original code worked in reverse, starting with all bits of the result set (after the if (c) c--; ) and then determining which needed to be zero and clearing them.

Since we are learning one bit of the output at a time, I think it's more clear to build the output using bit operations not arithmetic.

This code (from the net) is mostly C, although v &= -signed(v); isn't correct C. The intent is for it to behave as v &= ~v + 1;

First, if v is zero, then it remains zero after the & operation, and all of the if statements are skipped, so you get 32.

Otherwise, the & operation (when corrected) clears all bits to the left of the rightmost 1, so at that point v contains a single 1 bit. Then c is decremented to 31, ie all 1 bits within the possible result range.

The if statements then determine its numeric position one bit at a time (one bit of the position number, not of v ), clearing the bits that should be 0.

The code first transforms v is such a way that is is entirely null, except the left most one that remains. Then, it determines the position of this first one.

First let's see how we suppress all ones but the left most one.

Assume that k is the position of the left most one in v. v=(vn-1,vn-2,..vk+1,1,0,..0).

-v is the number that added to v will give 0 (actually it gives 2^n, but bit 2^n is ignored if we only keep the n less significant bits).

What must the value of bits in -v so that v+-v=0?

  • obviously bits k-1..0 of -k must be at 0 so that added to the trailing zeros in v they give a zero.

  • bit k must be at 1. Added to the one in vk, it will give a zero and a carry at one at order k+1

  • bit k+1 of -v will be added to vk+1 and to the carry generated at step k. It must be the logical complement of vk+1. So whatever the value of vk+1, we will have 1+0+1 if vk+1=0 (or 1+1+0 if vk+1=1) and result will be 0 at order k+1 with a carry generated at order k+2.

  • This is similar for bits n-1..k+2 and they must all be the logical complement of the corresponding bit in v.

Hence, we get the well-known result that to get -v, one must

  • leave unchanged all trailing zeros of v

  • leave unchanged the left most one of v

  • complement all the other bits.

If we compute v&-v, we have

 v      vn-1  vn-2    ...  vk+1  1   0   0  ... 0
-v   & ~vn-1 ~vn-2    ... ~vk+1  1   0   0  ... 0
v&-v      0     0     ...    0   1   0   0  ... 0

So v&-v only keeps the left most one in v.

To find the location of first one, look at the code:

if (v) c--;  // no 1 in result? -> 32 trailing zeros. 
             // Otherwise it will be in range c..0=31..0
if (v & 0x0000FFFF) c -= 16; // If there is a one in left most part of v  the range
                             //   of possible values for the location of this one
                             //   will be 15..0.
                             // Otherwise, range must 31..16
                             // remaining range is c..c-15 
if (v & 0x00FF00FF) c -= 8;  // if there is one in either byte 0 (c=15) or byte 2 (c=31), 
                             // the one is in the lower part of range.
                             // So we must substract 8 to boundaries of range.
                             // Other wise, the one is in the upper part.
                             // Possible range of positions of v is now c..c-7
if (v & 0x0F0F0F0F) c -= 4;  // do the same for the other bits.
if (v & 0x33333333) c -= 2;
if (v & 0x55555555) c -= 1;

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM