Feature #22483
openRegion forms of String#bit_get
Description
Impl: (WIP)
This ticket completes the region arguments of #22279 for bit_get. #22279 added the (offset, length) and range forms to bit_set, bit_clear, bit_flip and bit_count, and the commit message of #22118 announced the same extension for bit_get. It is also the second half of my reply to the comment of Matz in #22399#note-3: where the examples of #22399 folded the Array of bits into an Integer, the region form of bit_get reads the Integer directly. The search method is proposed separately in #22482.
What this adds¶
String#bit_get(offset, length, lsb_first: true) -> Integer | nil
String#bit_get(range, lsb_first: true) -> Integer | nil
bit_get with a length or a range reads a bit region as a non-negative Integer, as Integer#[](i, len) and Integer#[](range) do for the bits of an Integer. The existing one-bit form is the case length = 1.
data = "\xF0\xAA".b
data.bit_get(4) # => 1
data.bit_get(0, 8) # => 0xF0
data.bit_get(4, 8) # => 0xAF
data.bit_get(8..) # => 0xAA
data.bit_get(0, 16) # => 0xAAF0
hdr = "\x12\x34\x81\x80".b # a DNS header
hdr.bit_get(16, 16, lsb_first: false) # => 0x8180
hdr.bit_get(4, 12, lsb_first: false) # => 0x234
Use cases¶
-
Protocol headers (MSB-first). RFC diagrams number bits MSB-first and give each field a width.
bit_get(offset, width, lsb_first: false)reads a field by the coordinates of the diagram, without a bit reversal and without an Array. -
Non-aligned sensor frames on a microcontroller. A telemetry format packs 12 + 10 + 14 bits into a 36-bit frame, so every second frame starts in the middle of a byte. With
getbytethe decoder needs two code paths, one per alignment. Withbit_getit needs one. -
Hardware registers. A device register packs several fields into one or two bytes.
bit_get(5, 3)reads a 3-bit mode field as the datasheet writes it. -
Variable-length codes. A Rice or Golomb decoder reads the
kremainder bits after a unary run withbit_get(pos, k, lsb_first: false). With the search method of #22482, the decoder is two calls per value.
API Contracts¶
- Arguments follow #22118 and #22279. A negative
offsetraisesIndexError. A negativelengthraisesArgumentError. A position or length beyond2**64 - 1raisesArgumentError. Thelsb_firstvalue must betrueorfalse. A Range with alengthraisesArgumentError, as inbit_count. - The region form does not clamp.
bit_countclamps a region that extends past the end, because a smaller count is still correct. A truncated value would be wrong, sobit_getreturnsnilinstead, as the one-bit form does beyond the end and asunpack1does on short data. Thenilhas one meaning, so a parser of a truncated packet can test it without arescue. An empty region returns0, unless it begins beyond the end:bit_get(16, 0)on a 2-byte string returns0, andbit_get(17, 0)returnsnil, asbit_get(17)does. - The result has no width limit. A region wider than 64 bits returns a Bignum. In mruby, a build without
mruby-bigintraisesRangeErrorfor a region wider than its Integer. lsb_firstchanges the bit numbering and the significance. The byte order never changes. Withlsb_first: true, the first bit of the region is the least significant bit of the result, as inInteger#[](i, len). Withlsb_first: false, the first bit of the region is the most significant bit of the result, which is the value that an RFC diagram shows for the field.- Frozen receivers work. Encoding has no effect. The method treats the receiver as a byte sequence, as in #22118.
Why a region form of bit_get, not a new method¶
Integer#[]sets the precedent:Integer#[](i)returns one bit, andInteger#[](i, len)andInteger#[](range)return a bit region as an Integer. The region form ofbit_getis the same extension for the bits of a String, with the same significance of the first bit underlsb_first: true.- #22279 added the same forms to the other bit methods, and the commit message of #22118 announced it for
bit_get. This ticket completes it, andbit_getstays in line with the other bit methods. - The
nilrule carries over without a new rule: a read past the end returnsnil, whether it is one bit or a region. - No new name is needed.
A write side of the region form is not part of this ticket, because the use cases above only read. Note that bit_set(offset, length) already means "set all bits of the region", so a write of an Integer value into a region would need a name of its own, such as bit_put(offset, length, value).
Performance on CRuby¶
Each time is the median of 5 calls. The machine is an AMD Ryzen 5 5600X with Ruby 4.1.0dev (master at 2db3072cfb) and this patch, gcc 13.3.0 -O3, YJIT off.
bit_get fold |
unpack1 + to_i(2) |
getbyte + masks |
bit_get(offset, length) |
|
|---|---|---|---|---|
| 16-bit field at offset 4, x100,000 | 234 ms | 35 ms | - | 16 ms |
same, lsb_first: false |
297 ms | 28 ms | - | 13 ms |
| 233,016 frames of 12 + 10 + 14 bits | 1,219 ms | - | 54 ms | 30 ms |
The region form of bit_get is faster than hand-written getbyte masks and needs no alignment branch.
Performance on PicoRuby¶
I ported the region form to the mruby VM of PicoRuby (mruby-string-bitops, which already has the methods of #22118 and #22279) and ran the same comparisons on a board.
- Board: Raspberry Pi Pico 2 W (RP2350, Cortex-M33 at 150 MHz)
- Firmware: R2P2 4.0.6, PicoRuby master at a90afd12 plus this patch, mruby VM,
MRB_INT64, release build - Buffer: 8 KiB (65,536 bits), so that every variant fits in RAM. Each time is the median of 5 runs, measured with
Machine.uptime_us.
bit_get fold |
getbyte + masks |
bit_get(offset, length) |
|
|---|---|---|---|
| 16-bit field at offset 4, x1,000 | 2,445 ms | - | 166 ms |
same, lsb_first: false |
2,682 ms | - | 160 ms |
| 1,820 frames of 12 + 10 + 14 bits | 10,130 ms | 555 ms | 213 ms |
The ratios are the same as on CRuby, but the absolute times matter more here. The region form of bit_get is 2.6 times faster than hand-written getbyte masks and 48 times faster than a one-bit bit_get fold. None of the variants allocates per element, so the heap in use did not change beyond GC noise (within 8 KiB) during any row.
Updated by hasumikin (hitoshi hasumi) about 4 hours ago
- Backport deleted (
3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN) - Tracker changed from Bug to Feature