Reputation: 1289
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
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
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