3

I noticed that std::bitset does not have a function for returning or popping the lowest set bit of a bitset (nor the highest set bit for that matter). What is the fastest way to accomplish this task, specifically for std::bitset objects not guaranteed to be a certain number of bits long? I looked in the g++ compiler extensions and C++20 numerics library and didn't find anything related to my problem.

Two obvious methods to consider would be looping over the length of the bitset and using the operator[] or gradually shifting the bitset using operator>> until the first set bit is found.

qq4
  • 282
  • 4
  • 14
  • 8
    `bitset` has an `operator[]`. `[0]` is LSB and and `[size() - 1]` is MSB. https://en.cppreference.com/w/cpp/utility/bitset/operator_at – NathanOliver May 06 '22 at 01:47
  • By "popping", you do *not* mean "removing" (like, e.g. [`std::vector::pop_back`](https://en.cppreference.com/w/cpp/container/vector/pop_back)), do you? – Bob__ May 06 '22 at 09:48
  • you can't *remove* a bit from a bitset, it is a fixed-length data structure – Caleth May 06 '22 at 10:12
  • 3
    Do you perhaps mean the index of the lowest `true` bit? Because NathanOliver's answer (just use `[0]`) seems so trivial. – MSalters May 06 '22 at 11:59
  • I apologize for my confusion when first posting this question. For some reason I had it in my mind that the LSB was equivalent with the "lowest set bit" even though I know this to be false. (Perhaps too much coffee and too little sleep, I digress). – qq4 May 06 '22 at 18:21
  • Convert the bitset to an unsigned long long and use std::countl_zero and std::countr_zero – Sebastian May 06 '22 at 21:52
  • @Sebastian a bitset can be much larger than an unsigned long long – qq4 May 06 '22 at 21:55
  • If this is the case for your use case, one other idea (in addition to looping like in the existing answers) would be a binary search by using AND: E.g. for the lowest set bit for 1024 bit: First AND with a number with the 512 lowest bits set and test with `none()` function for 0. Depending on result, test by ANDing with 256 or 768 lowest bits set, and so on ... One would need 10 steps for 1024 bits width to find the position of the lowest bit compared to 1024 for a loop. It depends on the implementation of bitset, if this way is faster or slower. The 1024 masks can be precomputed. – Sebastian May 06 '22 at 22:05

4 Answers4

1

I'm going to use trailing zero like most modern implementations to make it easy to deal with the case where there's no set bit. To utilize the hardware count trailing zero instruction we can use to_ullong(), but to make it work we'll need to mask the value to make it fit in an unsigned long long

#include <bitset>
#include <bit>
#include <climits>

template<size_t N>
size_t count_trailingzero(std::bitset<N> b)
{
    if (b.none())
        return N;                   // The whole bitset was zero

    const decltype(b) mask(-1ULL);  // Mask to get the lowest unsigned long long
    size_t tz = 0;                  // The number of trailing zero bits
    const int width = sizeof(unsigned long long)*CHAR_BIT;
    do {
        auto lsw = (b & mask).to_ullong();  // The least significant word
        auto lsb = std::countr_zero(lsw);   // Position of the least significant bit

        if (lsb < width)                    // Found the first set bit from right
            return tz + lsb;
        
        // A set bit was not found because the lsw is all zero
        // so we'll increase the number of trailing zero bits
        tz += width;

        // Shift the bitset to get the next higher significant word
        b >>= width;
    } while (b.any());

    return tz;
}

Demo on Godbolt

This way no looping over individual bits is required and hardware acceleration can be used. But it's still not the most efficient method because each iteration the whole bitset still needs to be masked off. That's why std::bitset isn't a good tool for operating on bits in general and I always avoid them in practice. A class wrapping an array for bit operations will be much better in performance and flexibility

phuclv
  • 37,963
  • 15
  • 156
  • 475
  • I'm wondering what's the expected time complexity of `std::bitset::any` (and, well, `none`). If it's linear, it may be better to just iterate over the whole set. – Bob__ May 07 '22 at 15:58
  • 1
    I think the standard does not prescribe any complexity for `bitset`. (https://eel.is/c++draft/bitset) – Sebastian May 07 '22 at 15:59
0

There is no .lsb() or .msb() member functions, but std::bitset does provide .size() and .test() (and .any(), credit to @phuctv for use of .any() over .count()) with which you can construct the lsb and msb routines.

Presuming a valid std::bitset you can verify that at least one bit is set true using .any() (or just check the unsigned value). After verifying at least one bit is true, simply loop from bit-0 to bit-(bitset.size() - 1) checking for a set bit with .test() to obtain the LSB. Then just loop in reverse with the same test to find the MSB.

A short implementation would be:

#include <iostream>
#include <bitset>

int main () {
  
  size_t i = 0;                 /* bit indiex */
  std::bitset<8> bits (236);    /* bitset '11101100' */
  
  if (!bits.any()) {  /* validate at least 1 bit set */
    std::cerr << "pop count is zero.\n";
    return 1;
  }
  
  /* loop bit 0 to bits.size() - 1 for LSB */
  do {
    if (bits.test(i))             /* test if bit set */
      break;
  } while (++i < bits.size());
  
  std::cout << "lsb in '" << bits << "' is: " << i << '\n';
  
  /* loop bit bits.size() - 1 to 0 for MSB */
  i = bits.size();
  while (i--) {
    if (bits.test(i))             /* test if bit set */
      break;
  }
  
  std::cout << "msb in '" << bits << "' is: " << i << '\n';
}

Example Use/Output

$ ./bin//bitset_test
lsb in '11101100' is: 2
msb in '11101100' is: 7

Extend std::bitset and Add .lsb() and .msb() Member Functions

In addition to simply writing a couple of functions, you can just derive from std::bitset and add .lsb() and .msb() member functions to the derived class.

A short class declaration using the same implementations above could be:

template<size_t Nb>
class mybitset : public std::bitset<Nb> {
  
  std::bitset<Nb> bits;
  
 public:
  mybitset (const std::bitset<Nb>&b) : std::bitset<Nb>{b} { bits = b; }
  
  size_t lsb();     /* extend std::bitset with .lsb() and .msb() members */
  size_t msb();
  
  template<size_t NB>
  friend std::ostream& operator << (std::ostream& os, const mybitset<NB>& b);
};

Then you can simply use the .lsb() and .msb() members directly, e.g.

int main () {
  
  mybitset<8> bits (236);       /* derived class */
  
  if (!bits.any()) {  /* validate at least one bit set */
    std::cerr << "bitset value is zero -- zero pop count.\n";
    return 1;
  }
  
  /* output LSB and MSB */
  std::cout << "lsb in '" << bits << "' is: " << bits.lsb() <<
             "\nmsb in '" << bits << "' is: " << bits.msb() << '\n';
}

(same output)

David C. Rankin
  • 81,885
  • 6
  • 58
  • 85
  • but `count()` will iterate the whole bitset so it's not quite efficient. `std::bitset::any()` would be much better. Looping and testing each bit is also inefficient. Unfortunately we can't access the low-level array to utilize the count leading/trailing zero to get the fastest solution – phuclv May 07 '22 at 01:43
  • @phuclv - good eye and good thinking. That would save iterations (as long as the only 1-bit isn't the last) – David C. Rankin May 07 '22 at 02:04
  • Depending on implementation, the `count` could be bookkept (?), when modifying the bits, so returning it could be O(1). – Sebastian May 07 '22 at 05:10
  • @Sebastian not having looked at the source for either, the comment by phuctv made sense that `count()` would need to traverse the entire bitset, while `any()` would just need to traverse as far as the first bit set to `1` (granted, in normal bitsets of 64-bits or less, the difference will be negligible -- but the question did contain "fastest" -- so I'm happy to defer to that logic) – David C. Rankin May 07 '22 at 05:13
  • @DavidC.Rankin probably phuctv is right for typical implementations (e.g. https://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-html-USERS-3.4/bitset-source.html line 193), but one could implement bitset in a way that count() would not need to traverse even for huge bitsets. – Sebastian May 07 '22 at 05:55
  • Agreed, the "Counting Bits Set" routines in [Bit Twiddling Hacks](https://graphics.stanford.edu/~seander/bithacks.html) provide much more efficient solutions for pop count than a full iteration over the bits. Thanks for the source link, saved me the trouble of searching (and provided a good push to browse through it `:)` – David C. Rankin May 07 '22 at 06:02
  • I meant you can do the counting when modifying the bitset instead of when reading. – Sebastian May 07 '22 at 06:08
  • Yes, you are thinking one step ahead and filling `count` when the bitset is constructed or modified. That would work for no more than the cost of an additional private var, and an instruction or two during construction or modification. – David C. Rankin May 07 '22 at 06:11
  • Line 280 in the source has a LSB `_M_do_find_first` but for internal use. Who would have thought.... – David C. Rankin May 07 '22 at 06:18
  • They seem to be available as _Find_first and there is also _Find_next, but no _Find_last. – Sebastian May 07 '22 at 06:34
  • 1
    Yikes, only had to make it to line 1111 to learn it's part of the `SGIextensions` group. It is fully available, even with strict `-std=c++17`. Just added `std::cout << bits._Find_first() << '\n';` works just fine. I was at least expected a feature test macro like `_GNU_SOURCE` or the like. Double thank you for the link to the source. – David C. Rankin May 07 '22 at 06:58
  • I found this documentation http://web.mit.edu/ghudson/dev/nokrb/third/gcc/libstdc++-v3/docs/html/ext/sgiexts.html#ch23 (chapters were C++ standard chapter numbers) – Sebastian May 07 '22 at 11:09
0

A bit of a late answer, C++20 includes <bit> which has the functions you want.

In particular std::countr_zero which returns the number of consecutive 0 bits starting from the least significant bit.

#include <bit>
#include <iostream>

// This requires C++20 (or later)

int main() {
  std::cout << "lsb of 0b0100u is " << std::countr_zero(0b0100u) << "\n";
}

// std::countr_zero(0b0100u) is 2.
daveb
  • 74,111
  • 6
  • 45
  • 51
-1

You can do something like this:

#include <bitset>
#include <iostream>
int main(int argc, char **argv) {
  std::bitset<32> your_bit{0B1010000};
  int lsb = your_bit[0];
  std::cout << lsb << '\n';
}
ramsay
  • 3,197
  • 2
  • 14
  • 13