Feature #20163
closedIntroduce #bit_count method on Integer
Added by garrison (Garrison Jensen) over 2 years ago. Updated 16 days ago.
Description
This feature request is to implement a method called #bit_count on Integer that returns the number of ones in the binary representation of the absolute value of the integer.
This is often useful when you use an integer as a bitmask and want to count how many bits are set.
This would be equivalent to
However, this can be outperformed by
def bit_count(n)
count = 0
while n > 0
n &= n - 1 # Flip the least significant 1 bit to 0
count += 1
end
count
end
I think this would be a useful addition because it would fit alongside the other bit-related methods defined on integer: #bit_length, #allbits?, #anybits?, #nobits?. Also, when working with bitmasks, a minor upgrade to performance often results in a significant improvement.
Similar methods from other languages:
https://docs.python.org/3/library/stdtypes.html#int.bit_count
https://doc.rust-lang.org/std/primitive.i32.html#method.count_ones
Updated by Eregon (Benoit Daloze) over 2 years ago
1Actions
#1
[ruby-core:116088]
The name sounds too close to #bit_length, and length and count are often quite close in Ruby
(e.g. Enumerable#count without arguments is the same as #size/#length after Enumerable#to_a or on an Array, Hash, etc).
I think count_ones is a better name because there is no ambiguity.
popcount might be another common name for it, but that seems less clear unless the term or the assembly instruction is already known.
Also I think abbreviations are in general suboptimal in method names.
Updated by shyouhei (Shyouhei Urabe) over 2 years ago
Actions
#2
[ruby-core:116102]
count_ones could be something different from what the OP wants, because (-19).bit_count is expected to be 3.
PS. i64::count_ones(-19) is 62 for Rust.
PPS. There is no such thing like a 64bit signed integer in ruby.
Updated by nobu (Nobuyoshi Nakada) over 2 years ago
Actions
#3
[ruby-core:116109]
GCC has __builtin_popcount and Ruby defines rb_popcount function family internally.
Updated by byroot (Jean Boussier) over 2 years ago
1Actions
#4
[ruby-core:116112]
I also think popcount makes sense. Yes it's a bit of a cryptic name, but if you are dealing with bits, you are likely to be familiar with that name.
Updated by nobu (Nobuyoshi Nakada) over 2 years ago
Actions
#5
- Is duplicate of Feature #8748: Integer#popcount (Fixnum#popcount and Bignum#popcount) added
Updated by garrison (Garrison Jensen) over 2 years ago
Actions
#6
[ruby-core:116130]
I like popcount but I also like count_ones because it sounds friendlier, however I have no strong preferences for the chosen name.
Also, I want to share my performance tests in case they are helpful for the discussion. As you can see, the built-in method is significantly faster.
(0..10_000_000).each { |x| x.to_s(2).count('1') }
processing time: 3.714706s
(0..10_000_000).each { |x| ruby_pop_count(x) }
processing time: 3.367775s
(0..10_000_000).each { |x| x.pop_count }
processing time: 0.515767s
Here are my implementations:
unsigned int pop_count(long value) {
#ifdef __GNUC__
// Use GCC built-in
return __builtin_popcountl(value);
#else
// Fallback method for compilers without the built-in
unsigned int count = 0;
while (value) {
count += value & 1;
value >>= 1;
}
return count;
#endif
}
// Wrapper function for Ruby
VALUE rb_pop_count(VALUE self) {
long value = NUM2LONG(self);
unsigned int count = pop_count(labs(value));
return UINT2NUM(count);
}
Updated by tenderlovemaking (Aaron Patterson) 11 months ago
Actions
#7
[ruby-core:123112]
I would also like a popcount method. I prefer popcount over bit_count. I wrote my own here (but it only deals with single bytes).
Updated by akr (Akira Tanaka) 11 months ago
Actions
#8
[ruby-core:123114]
Updated by byroot (Jean Boussier) 11 months ago
Actions
#9
[ruby-core:123116]
What is the behavior for negative values?
IMO, the only behavior that makes sense in the context of a language with arbitrary size integers is to ignore the sign bit.
Updated by mame (Yusuke Endoh) 11 months ago
Actions
#10
[ruby-core:123118]
What are the intended use cases for this proposal?
My experience (in other languages) involves two use cases of popcount:
- Bitboards for game AI (like Reversi) to count pieces.
- Succinct data structures (like LOUDS Tries) for rank operations.
In both scenarios, integers are treated as unsigned bitsets.
Does anyone have a use case where popcount on a negative number is necessary?
If not, I guess raising an exception would be the best behavior.
Updated by tompng (tomoya ishida) 11 months ago
Actions
#11
[ruby-core:123119]
I also think popcount of a negative number should raise error because of the ambiguity.
One way to extend popcount to negative number is using a relationship below, derived from the fact that -5 == 0b111...11111011 has 1 fewer bits compared to -1 == 0b111...11111111.
x.popcount == popcount_of_minus_one - (~x).popcount
# popcount_of_minus_one: 0 or -1 or something else representing infinity bits
I'm not sure if this definition is useful, but anyway, extending to negative number has ambiguity.
If someone want a popcount of abs, n.abs.popcount is clearer.
Updated by ahorek (Pavel Rosický) 11 months ago
Actions
#12
[ruby-core:123120]
x64 and ARM have specialized CPU instructions
https://godbolt.org/z/xvGvzsvd9
and Ruby already uses it internally, for instance
https://github.com/ruby/ruby/blob/dc555a48e750b4d50eb7a7000ca1bfb927fa9459/string.c#L2209
That said, Ruby isn’t the ideal choice for implementing memory allocators, SIMD masks, parity checks, GCD calculations, UTF parsers, or prime sieving… but since many other languages, even Python, provide support for popcount, why not?
Updated by byroot (Jean Boussier) 11 months ago
Actions
#13
[ruby-core:123121]
but since many other languages, even Python, provide support for popcount, why not?
Usually a higher bar than that is required for a new method to be added to Ruby.
I personally don't have an immediate use case to point to (except for Aaron's gem of course). But more generally, in recent years we've tried to eliminate or at least reduce the need for C extensions, and bit operation are often useful in these cases, so I'm sympathetic to more bit oriented capacities.
Updated by tenderlovemaking (Aaron Patterson) 11 months ago
1Actions
#14
[ruby-core:123123]
mame (Yusuke Endoh) wrote in #note-10:
What are the intended use cases for this proposal?
My experience (in other languages) involves two use cases of popcount:
- Bitboards for game AI (like Reversi) to count pieces.
- Succinct data structures (like LOUDS Tries) for rank operations.
In both scenarios, integers are treated as unsigned bitsets.
My experience is similar. I've used it for sets (like I linked above) as well as modeling undirected graphs (a bit matrix, but I omitted popcount from the blog post). I've only used unsigned integers, and I think it would be a bug in my code if the integers were signed.
Does anyone have a use case where popcount on a negative number is necessary?
If not, I guess raising an exception would be the best behavior.
I agree, and I think it should raise an exception.
ahorek (Pavel Rosický) wrote in #note-12:
That said, Ruby isn’t the ideal choice for implementing memory allocators, SIMD masks, parity checks, GCD calculations, UTF parsers, or prime sieving…
Not yet! But hopefully someday! 😂
Updated by garrison (Garrison Jensen) 11 months ago
Actions
#15
[ruby-core:123124]
Python ignores the sign. It seems friendlier to match that behavior than throw an exception.
Updated by tenderlovemaking (Aaron Patterson) 11 months ago
Actions
#16
[ruby-core:123126]
garrison (Garrison Jensen) wrote in #note-15:
Python ignores the sign. It seems friendlier to match that behavior than throw an exception.
When would you use a negative number unless it's a mistake in your code?
Updated by tenderlovemaking (Aaron Patterson) 11 months ago
1Actions
#17
[ruby-core:123127]
It seems like the Python folks didn't have too serious a discussion about handling negative numbers.
https://github.com/python/cpython/issues/74068#issuecomment-1093743975
https://github.com/python/cpython/issues/74068#issuecomment-1093743978
I prefer it raises an exception as it's probably a mistake in the code. 🤷🏻♀️
Updated by garrison (Garrison Jensen) 11 months ago
Actions
#18
[ruby-core:123128]
tenderlovemaking (Aaron Patterson) wrote in #note-16:
When would you use a negative number unless it's a mistake in your code?
I don't have a strong argument. Raising an exception sounds good to me.
Updated by akr (Akira Tanaka) 11 months ago
Actions
#19
[ruby-core:123155]
I prefer an exception for popcount to negative values.
I think an array of Fixnums (63 bit signed integers) can be used for mutable bit array.
(Ruby's Integer is immutable. So mutable bit array needs a mutable data structure.)
In this scenario, we would like to count bits in a negative fixnum: (n & 0x7ffffffff).popcount.
This is not n.abs.popcount.
Also, this behavior is not suitable for Integer#popcount because it ignores bits higher than 63th bit.
So, I prefer an exception for negative values.
Updated by slewsys (Andrew Moore) 10 months ago
Actions
#20
[ruby-core:123292]
garrison (Garrison Jensen) wrote in #note-6:
unsigned int pop_count(long value) { #ifdef __GNUC__ // Use GCC built-in return __builtin_popcountl(value); #else // Fallback method for compilers without the built-in unsigned int count = 0; while (value) { count += value & 1; value >>= 1; } return count; #endif } // Wrapper function for Ruby VALUE rb_pop_count(VALUE self) { long value = NUM2LONG(self); unsigned int count = pop_count(labs(value)); return UINT2NUM(count); }
Section 1.8 of Matters Computational by Jörg Arndt offers efficient algorithms for popcount, including for arrays. There is also an x86 instruction POPCNT.
Matters Computational: https://www.jjj.de/fxt/fxtbook.pdf
E.g.,
unsigned long popcount (unsigned long value) {
value -= (value >> 1) & 0x5555555555555555UL;
value = ((value >> 2) & 0x3333333333333333UL) + (value & 0x3333333333333333UL);
value = ((value >> 4) + value) & 0x0f0f0f0f0f0f0f0fUL;
value *= 0x0101010101010101UL;
return value >> 56;
}
By the way, "popcount" is short for "population count", whereas "pop_count" suggests (to me) a stack operation - a bit (hm) confusing.
Updated by YO4 (Yoshinao Muramatsu) 10 months ago
Actions
#21
[ruby-core:123329]
Updated by mame (Yusuke Endoh) 10 months ago
Actions
#22
[ruby-core:123331]
In computers that use two's complement for negative numbers, I think this is expected behavior.
Yes, but do you want -1.popcount to return Float::INFINITY? That might be theoretically consistent, but useless and even problematic, I think.
Updated by YO4 (Yoshinao Muramatsu) 10 months ago
Actions
#23
[ruby-core:123337]
Popcount for negative numbers is useful when equivalent functionality to machine register operations is desired, but I do not consider operations on infinite bit widths to be useful.
When popcount is called on a negative number (without specifying a bit width if popcount has arguments), it may indicate abnormal input or a bug in user code.
I am +1 for raising exception on that case.
Updated by matz (Yukihiro Matsumoto) 9 months ago
Actions
#24
[ruby-core:123531]
Is there any real-world use-case for Integer#popcount method?
Bit-array?
Matz.
Updated by tenderlovemaking (Aaron Patterson) 9 months ago
Actions
#25
[ruby-core:123539]
matz (Yukihiro Matsumoto) wrote in #note-24:
Is there any real-world use-case for Integer#popcount method?
Bit-array?Matz.
Yes, I am using it for a bit set gem (I had to implement my own popcount here). I want to use a bit array for representing an undirected graph.
Updated by Eregon (Benoit Daloze) about 2 months ago
Actions
#26
- Related to Feature #22082: Introduce Bit Operations into String added
Updated by jhawthorn (John Hawthorn) 19 days ago
· Edited
Actions
#27
[ruby-core:125940]
I have a real-world use case: I've been working on a pure-Ruby Hash Array Mapped Trie (HAMT) https://github.com/jhawthorn/hamt. I want this because having an immutable hash-table-like map which can be cheaply copied/modified would be helpful for sharing data between Ractors.
The implementation of HAMT requires popcount: for each node in the tree we have a 32-bit bitmap representing which keys are present, and an array containing the keys/values. At each level we test for a key's presence using 5 bits of it's hash value bit = 1 << ((hash >> level) & 31). If that bit is set, we find where it exists in the array using popcount(bitmap & (bit - 1)) (feels similar to succinct bit vector).
The String#bit_count proposed in another issues (#22082/#22118) would not be a good fit here. I don't want an additional String object per-node. I'm always representing at most 32 bits (this is the typical choice for HAMT) in a fixnum. I'd also need either a way to mask only the relevant bits from the string, requiring an extra allocation (I actually don't even see a way to do this with what is in #22118). We also have the endianness issue. This is all avoided and natural with Integer (I have no objection to String#bit_count also being added, but I think Integer#bit_count is more essential).
There are other textbook uses for popcount out there: hamming distance, parity, etc.
I think Integer#bit_count makes sense and is aligned with existing use of Integer. We have bit_length which is a very similar concept and name and have many other functions for dealing with bits on integer (Integer#[], #allbits?, #anybits?, bitwise operations, etc)
Updated by jhawthorn (John Hawthorn) 19 days ago
· Edited
Actions
#28
[ruby-core:125943]
I tried finding uses in the wild via gem-codesearch. I only looked for to_s(2).count("1") which is a pretty inefficient way to bit_count, so this would miss any gems which used another technique (a loop or some bit twiddling) to count bits. Still it found a few use cases:
CIDR/netmask
Very common to do some variation of IPAddr.new(mask).to_i.to_s(2).count("1") to get the prefix length of the netmask.
Done by: sequel, facter, inspec-core, vagrant, and many others
Another case (in stdlib!) is IPAddr#prefix which uses a shift loop. If bit_count is accepted we should change the loop in IPAddr#prefix to use it.
Hamming distance
For example dhash-vips.rb doing (a ^ b).to_s(2).count("1") to get hamming distance for perceptual similarity
Also: simhash2, chromaprint, edits, dhash, dhasher, and others
Also found in unicode_plot using a different pattern: (i ^ k).digits(2).sum
Parity bit
For example in https://github.com/larskanis/pkcs11
n.to_s(2).count("1").odd?
sigstore-ruby
I only found sigstore Ruby doing this, something involving proof verification in a Merkle tree (?). It requires both bit_length (which already exists) and bit_count (being proposed here). (log_index >> inner).to_s(2).count("1")
Note: I believe this was ported from go which has a popcount (bits.OnesCount64) method.
Updated by jhawthorn (John Hawthorn) 19 days ago
Actions
#29
[ruby-core:125955]
I've opened a PR implementing Integer#bit_count https://github.com/ruby/ruby/pull/17696
For negative numbers this raises Math::DomainError, same as Integer#digits when called with a negative number
Updated by tenderlovemaking (Aaron Patterson) 19 days ago
Actions
#30
[ruby-core:125956]
I found a few more examples "in the wild":
Updated by mame (Yusuke Endoh) 18 days ago
Actions
#31
[ruby-core:125970]
Thanks for the examples. Unless I'm misreading, the popcount usage in Hash Array Mapped Trie is the rank operation from succinct data structures that I mentioned (the operation that finds which bit is the nth set bit).
I took a quick look through the other examples you listed.
The good news is that (unless I'm misreading) none of the examples appear to deal with negative numbers. Treating negatives as raising an exception is probably fine.
On the other hand, whether popcount is really what's needed seems questionable in a few cases:
- CIDR/netmask: IMO, using popcount here feels a bit tricky. Wouldn't
leading_ones/trailing_zerosbe more appropriate? - Hamming distance: This one is genuinely popcount.
- Parity bit: I couldn't find the relevant code in larskanis/pkcs11.
- sigstore-ruby: This one is genuinely popcount.
- tenderlove/aarch64: This uses popcount to build an exception message,
raise "Expected a #{popcount(mask)} bit number...", sobit_lengthlooks like the better choice here. - izawa/cidr: This is a netmask case, so
leading_ones/trailing_zerosfeels more appropriate. - slonopotamus/dyck: This uses it in the form
Dyck.popcount(tagx.bitmask) > 1, but unless I'm misreading,tagx.bitmaskseems to be a value like0b1,0b11,0b111, ..., sobit_length > 1seems more natural thanpopcount > 1. - nerzh/ton-sdk-ruby: This one is genuinely popcount. Worth noting that this library also defines
clz.
Another case (in stdlib!) is
IPAddr#prefixwhich uses a shift loop. Ifbit_countis accepted we should change the loop inIPAddr#prefixto use it.
This code is:
but this clearly seems like a case where n.bit_length, not popcount, is the correct choice.
To summarize, the cases where I felt popcount is genuinely needed are these four:
- rank operation (Succinct data structures / Hash Array Mapped Trie)
- Hamming distance
- sigstore-ruby
- TON SDK
By the way, looking at these use cases made me think leading_ones and trailing_zeros might be desirable too. And the name bit_count doesn't extend naturally to those.
Also, is bit_count a natural name for counting the set bits? TON SDK calls it count_set_bits, which felt clearer to me.
Updated by jhawthorn (John Hawthorn) 18 days ago
· Edited
Actions
#32
[ruby-core:125975]
mame (Yusuke Endoh) wrote in #note-31:
but [CIDR] clearly seems like a case where
n.bit_length, not popcount, is the correct choice.
I do think popcount is the right choice for CIDR. That's what everything other than IPAddr is using and it directly returns the correct result (all implementations we've looked at assume the netmask has a valid CIDR notation). There are other ways to do it of course, but popcount/bit_count is the most convenient.
The code in IPAddr is doing bit_length in a loop currently, but if you look at the full code. It actually needs to xor with IN4MASK/IN6MASK and then subtract the bit_length from 32 or 128.
def prefix
case @family
when Socket::AF_INET
32 - (IN4MASK ^ @mask_addr).bit_length
when Socket::AF_INET6
128 - (IN6MASK ^ @mask_addr).bit_length # may allocate in xor?
else
raise AddressFamilyError, "unsupported address family"
end
end
bit_count is a lot more natural (the only complexity is in validating the address family)
case @family
when Socket::AF_INET, Socket::AF_INET6
@mask_addr.bit_count
else
raise AddressFamilyError, "unsupported address family"
end
leading_ones
This method seems very confusing to me. It's common to have a "count leading zeros" (clz), but that only makes sense with fixed precision (in cases in Ruby we'd want "count leading zeros" we'd likely use bit_length). "leading ones" sounds like it would be the same thing. Does this have any outside out of CIDR (where it is always equal to popcount)?
I think we can perform this using bit_length (it is a bit awkward and would allocate when n << 1 is a bignum):
trailing_zeros
I think this makes a lot more sense and is a common operation in other languages. I'd support it's inclusion, but I think it's less important than popcount.
I think we can perform this using bit_length (though it allocates when n is a bignum)
Updated by jhawthorn (John Hawthorn) 18 days ago
Actions
#33
[ruby-core:125976]
Couple notes on the various gems
mame (Yusuke Endoh) wrote in #note-31:
- Parity bit: I couldn't find the relevant code in larskanis/pkcs11.
Sorry, the quoted code was from another library. Here it is: https://github.com/larskanis/pkcs11/blob/12d868912e0d17aa41e497f9bb99f66a397a9365/pkcs11_protect_server/test/helper.rb#L10
Another example of parity: https://gitlab.com/pkerling/subconv/-/blob/e560905808b2db0918b0e350f39b136906417335/lib/subconv/scc/reader.rb#L594
- slonopotamus/dyck: This uses it in the form
Dyck.popcount(tagx.bitmask) > 1, but unless I'm misreading,tagx.bitmaskseems to be a value like0b1,0b11,0b111, ..., sobit_length > 1seems more natural thanpopcount > 1.
These aren't equivalent. This is checking that MORE than one bit is set, I don't know why but other mobi libraries in other languages seem to as well. But I agree there's another way to test for this: x & (x - 1) != 0
- nerzh/ton-sdk-ruby: This one is genuinely popcount. Worth noting that this library also defines
clz.
It defines clz (for 32 bits by default), but only seems to use it in level as 32 - clz(value), which is actually bit_length. So this is another case bit_count is complementing bit_length.
Updated by matz (Yukihiro Matsumoto) 16 days ago
Actions
#34
[ruby-core:126036]
Accepted, with one change from the proposal: bit_count on a negative integer shall raise ArgumentError instead of using the absolute value. A negative integer in Ruby is conceptually an infinite two's complement bit string, so its population count is not well defined, and silently using the absolute value could mask bugs.
The name bit_count is good. In Ruby, count means counting matching or present things (Array#count, String#count), as opposed to length, which is the trivial total extent. Viewing an integer as a bit set, the set bits are exactly the elements present, so bit_count naturally reads as the number of set bits, and it is clearly distinguished from bit_length. It also fits the bit_at / bitwise_* vocabulary being discussed for String in #22082/#22118; if we later add the same operation to String, it shall use the same name.
Matz.
Updated by jhawthorn (John Hawthorn) 16 days ago
Actions
#35
- Status changed from Open to Closed
Applied in changeset git|23c056c83cebad03f716c7af490c8bec9753ec90.
[Feature #20163] Add Integer#bit_count