puritii
puritii

Reputation: 1289

How to efficiently convert a number n to an integer with that many bits set? eg 8 -> 0b11111111?

How do you efficiently convert a number n to a number with that many low significant bits set? That is, for an 8-bit integer:

0 -> 00000000
1 -> 00000001
2 -> 00000011
3 -> 00000111
4 -> 00001111
5 -> 00011111
6 -> 00111111
7 -> 01111111
8 -> 11111111

Seems trivial:

fn conv(n: u8) -> u8 { (1 << n) - 1 }

But it's not:

thread 'main' panicked at 'attempt to shift left with overflow', src/main.rs:2:5

playground

This is because, in Rust, a number with N bits cannot be shifted left N places. It is undefined behaviour on many architectures.

u8::MAX >> (8 - n) doesn't work either as shift right has the same overflow restriction.

So, what is an efficient way to achieve this without a lookup table or a conditional? Or is this not possible and one must resort to an implementation like:

fn conv2(n: u8) -> u8 {
    match 1u8.checked_shl(n.into()) {
        Some(n) => n - 1,
        None => u8::MAX,
    }
}

Upvotes: 1

Views: 447

Answers (1)

trent
trent

Reputation: 27925

The most efficient way, for any bit width smaller than the largest native integer size, will almost certainly be to use a wider type for calculations and then narrow it to the output type:

pub fn conv(n: u8) -> u8 {
    ((1u32 << n) - 1) as u8
}

rustc 1.50 at -C opt-level=1 or higher renders this as four instructions with no branches or indirection, which is probably optimal for x86-64.

example::conv:
        mov     ecx, edi
        mov     eax, 1
        shl     eax, cl
        add     al, -1
        ret

On most (all?) 32-bit or larger platforms, there's no advantage to be gained from doing math with u8 or u16 instead of u32; if you have to use a 32-bit register you may as well use the whole thing (which is also why certain integer methods only accept u32).

Even if you write a match, it may not be compiled to a branch. In my tests, the following function

pub fn conv(n: u32) -> u64 {
    match 1u64.checked_shl(n) {
        Some(n) => n - 1,
        _ => !0,
    }
}

compiles with no branches, just a cmov instruction, and looks pretty similar to the equivalent using (software-emulated) u128. Be sure to profile before assuming that the match is a problem.

Upvotes: 4

Related Questions