Skip to content

Commit

Permalink
Rollup merge of rust-lang#70835 - yoshuawuyts:int-log2, r=dtolnay
Browse files Browse the repository at this point in the history
Add Integer::checked_{log,log2,log10}

This implements `{log,log2,log10}` methods for all integer types. The implementation was provided by @substack for use in the stdlib.

_Note: I'm not big on math, so this PR is a best effort written with limited knowledge. It's likely I'll be getting things wrong, but happy to learn and correct. Please bare with me._

## Motivation
Calculating the logarithm of a number is a generally useful operation. Currently the stdlib only provides implementations for floats, which means that if we want to calculate the logarithm for an integer we have to cast it to a float and then back to an int.

> would be nice if there was an integer log2 instead of having to either use the f32 version or leading_zeros() which i have to verify the results of every time to be sure

_— [@substack, 2020-03-08](https://twitter.com/substack/status/1236445105197727744)_

At higher numbers converting from an integer to a float we also risk overflows. This means that Rust currently only provides log operations for a limited set of integers.

The process of doing log operations by converting between floats and integers is also prone to rounding errors. In the following example we're trying to calculate `base10` for an integer. We might try and calculate the `base2` for the values, and attempt [a base swap](https://www.rapidtables.com/math/algebra/Logarithm.html#log-rules) to arrive at `base10`. However because we're performing intermediate rounding we arrive at the wrong result:

```rust
// log10(900) = ~2.95 = 2
dbg!(900f32.log10() as u64);

// log base change rule: logb(x) = logc(x) / logc(b)
// log2(900) / log2(10) = 9/3 = 3
dbg!((900f32.log2() as u64) / (10f32.log2() as u64));
```
_[playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=6bd6c68b3539e400f9ca4fdc6fc2eed0)_

This is somewhat nuanced as a lot of the time it'll work well, but in real world code this could lead to some hard to track bugs. By providing correct log implementations directly on integers we can help prevent errors around this.

## Implementation notes

I checked whether LLVM intrinsics existed before implementing this, and none exist yet. ~~Also I couldn't really find a better way to write the `ilog` function. One option would be to make it a private method on the number, but I didn't see any precedent for that. I also didn't know where to best place the tests, so I added them to the bottom of the file. Even though they might seem like quite a lot they take no time to execute.~~

## References

- [Log rules](https://www.rapidtables.com/math/algebra/Logarithm.html#log-rules)
- [Rounding error playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=6bd6c68b3539e400f9ca4fdc6fc2eed0)
- [substack's tweet asking about integer log2 in the stdlib](https://twitter.com/substack/status/1236445105197727744)
- [Integer Logarithm, A. Jaffer 2008](https://people.csail.mit.edu/jaffer/III/ilog.pdf)
  • Loading branch information
Manishearth authored Jul 9, 2020
2 parents 5db778a + 6cef103 commit b5c1cb3
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 0 deletions.
218 changes: 218 additions & 0 deletions src/libcore/num/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2160,6 +2160,115 @@ assert_eq!((-a).rem_euclid(-b), 1);
}
}

doc_comment! {
concat!("Returns the logarithm of the number with respect to an arbitrary base.
Returns `None` if the number is negative or zero, or if the base is not at least 2.
This method may not be optimized owing to implementation details;
`self.checked_log2()` can produce results more efficiently for base 2, and
`self.checked_log10()` can produce results more efficiently for base 10.
# Examples
```
#![feature(int_log)]
let five = 5", stringify!($SelfT), ";
// log5(5) == 1
let result = five.checked_log(5);
assert_eq!(result, Some(1));
```"),
#[unstable(feature = "int_log", issue = "70887")]
#[must_use = "this returns the result of the operation, \
without modifying the original"]
#[inline]
pub fn checked_log(self, base: Self) -> Option<Self> {
// SAFETY: We check the input to this is always positive
let logb2 = |x: Self| unsafe { intrinsics::ctlz_nonzero(1 as Self) - intrinsics::ctlz_nonzero(x) };

if self <= 0 || base <= 1 {
None
} else {
let mut n = 0;
let mut r = self;

// Optimization for 128 bit wide integers.
if mem::size_of::<Self>() * 8 == 128 {
let b = logb2(self) / (logb2(base) + 1);
n += b;
r /= base.pow(b as u32);
}

while r >= base {
r /= base;
n += 1;
}
Some(n)
}
}
}

doc_comment! {
concat!("Returns the base 2 logarithm of the number.
Returns `None` if the number is negative or zero.
# Examples
```
#![feature(int_log)]
let two = 2", stringify!($SelfT), ";
// checked_log2(2) == 1
let result = two.checked_log2();
assert_eq!(result, Some(1));
```"),
#[unstable(feature = "int_log", issue = "70887")]
#[must_use = "this returns the result of the operation, \
without modifying the original"]
#[inline]
pub fn checked_log2(self) -> Option<Self> {
if self <= 0 {
None
} else {
// SAFETY: We just checked that this number is positive
let log = unsafe { intrinsics::ctlz_nonzero(1 as Self) - intrinsics::ctlz_nonzero(self) };
Some(log)
}
}
}

doc_comment! {
concat!("Returns the base 10 logarithm of the number.
Returns `None` if the number is negative or zero.
# Examples
```
#![feature(int_log)]
let ten = 10", stringify!($SelfT), ";
// checked_log10(10) == 1
let result = ten.checked_log10();
assert_eq!(result, Some(1));
```"),
#[unstable(feature = "int_log", issue = "70887")]
#[must_use = "this returns the result of the operation, \
without modifying the original"]
#[inline]
pub fn checked_log10(self) -> Option<Self> {
self.checked_log(10)
}
}

doc_comment! {
concat!("Computes the absolute value of `self`.
Expand Down Expand Up @@ -4169,6 +4278,115 @@ assert_eq!(7", stringify!($SelfT), ".rem_euclid(4), 3); // or any other integer
}
}

doc_comment! {
concat!("Returns the logarithm of the number with respect to an arbitrary base.
Returns `None` if the number is zero, or if the base is not at least 2.
This method may not be optimized owing to implementation details;
`self.checked_log2()` can produce results more efficiently for base 2, and
`self.checked_log10()` can produce results more efficiently for base 10.
# Examples
```
#![feature(int_log)]
let five = 5", stringify!($SelfT), ";
// log5(5) == 1
let result = five.checked_log(5);
assert_eq!(result, Some(1));
```"),
#[unstable(feature = "int_log", issue = "70887")]
#[must_use = "this returns the result of the operation, \
without modifying the original"]
#[inline]
pub fn checked_log(self, base: Self) -> Option<Self> {
// SAFETY: We check the input to this is always positive.
let logb2 = |x: Self| unsafe { intrinsics::ctlz_nonzero(1 as Self) - intrinsics::ctlz_nonzero(x) };

if self <= 0 || base <= 1 {
None
} else {
let mut n = 0;
let mut r = self;

// Optimization for 128 bit wide integers.
if mem::size_of::<Self>() * 8 == 128 {
let b = logb2(self) / (logb2(base) + 1);
n += b;
r /= base.pow(b as u32);
}

while r >= base {
r /= base;
n += 1;
}
Some(n)
}
}
}

doc_comment! {
concat!("Returns the base 2 logarithm of the number.
Returns `None` if the number is zero.
# Examples
```
#![feature(int_log)]
let two = 2", stringify!($SelfT), ";
// checked_log2(2) == 1
let result = two.checked_log2();
assert_eq!(result, Some(1));
```"),
#[unstable(feature = "int_log", issue = "70887")]
#[must_use = "this returns the result of the operation, \
without modifying the original"]
#[inline]
pub fn checked_log2(self) -> Option<Self> {
if self <= 0 {
None
} else {
// SAFETY: We just checked that this number is positive
let log = unsafe { intrinsics::ctlz_nonzero(1 as Self) - intrinsics::ctlz_nonzero(self) };
Some(log)
}
}
}

doc_comment! {
concat!("Returns the base 10 logarithm of the number.
Returns `None` if the number is zero.
# Examples
```
#![feature(int_log)]
let ten = 10", stringify!($SelfT), ";
// checked_log10(10) == 1
let result = ten.checked_log10();
assert_eq!(result, Some(1));
```"),
#[unstable(feature = "int_log", issue = "70887")]
#[must_use = "this returns the result of the operation, \
without modifying the original"]
#[inline]
pub fn checked_log10(self) -> Option<Self> {
self.checked_log(10)
}
}

doc_comment! {
concat!("Returns `true` if and only if `self == 2^k` for some `k`.
Expand Down
1 change: 1 addition & 0 deletions src/libcore/tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#![feature(fmt_internals)]
#![feature(hashmap_internals)]
#![feature(try_find)]
#![feature(int_log)]
#![feature(is_sorted)]
#![feature(pattern)]
#![feature(range_is_empty)]
Expand Down
64 changes: 64 additions & 0 deletions src/libcore/tests/num/int_log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#[test]
fn checked_log() {
assert_eq!(999u32.checked_log(10), Some(2));
assert_eq!(1000u32.checked_log(10), Some(3));
assert_eq!(555u32.checked_log(13), Some(2));
assert_eq!(63u32.checked_log(4), Some(2));
assert_eq!(64u32.checked_log(4), Some(3));
assert_eq!(10460353203u64.checked_log(3), Some(21));
assert_eq!(10460353202u64.checked_log(3), Some(20));
assert_eq!(147808829414345923316083210206383297601u128.checked_log(3), Some(80));
assert_eq!(147808829414345923316083210206383297600u128.checked_log(3), Some(79));
assert_eq!(22528399544939174411840147874772641u128.checked_log(19683), Some(8));
assert_eq!(22528399544939174411840147874772631i128.checked_log(19683), Some(7));

for i in i16::MIN..=0 {
assert_eq!(i.checked_log(4), None);
}
for i in 1..=i16::MAX {
assert_eq!(i.checked_log(13), Some((i as f32).log(13.0) as i16));
}
for i in 1..=u16::MAX {
assert_eq!(i.checked_log(13), Some((i as f32).log(13.0) as u16));
}
}

#[test]
fn checked_log2() {
assert_eq!(5u32.checked_log2(), Some(2));
assert_eq!(0u64.checked_log2(), None);
assert_eq!(128i32.checked_log2(), Some(7));
assert_eq!((-55i16).checked_log2(), None);

for i in 1..=u8::MAX {
assert_eq!(i.checked_log2(), Some((i as f32).log2() as u8));
}
for i in 1..=u16::MAX {
assert_eq!(i.checked_log2(), Some((i as f32).log2() as u16));
}
for i in i8::MIN..=0 {
assert_eq!(i.checked_log2(), None);
}
for i in 1..=i8::MAX {
assert_eq!(i.checked_log2(), Some((i as f32).log2() as i8));
}
for i in i16::MIN..=0 {
assert_eq!(i.checked_log2(), None);
}
for i in 1..=i16::MAX {
assert_eq!(i.checked_log2(), Some((i as f32).log2() as i16));
}
}

#[test]
fn checked_log10() {
for i in i16::MIN..=0 {
assert_eq!(i.checked_log10(), None);
}
for i in 1..=i16::MAX {
assert_eq!(i.checked_log10(), Some((i as f32).log10() as i16));
}
for i in 1..=u16::MAX {
assert_eq!(i.checked_log10(), Some((i as f32).log10() as u16));
}
}
1 change: 1 addition & 0 deletions src/libcore/tests/num/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ mod u8;
mod bignum;
mod dec2flt;
mod flt2dec;
mod int_log;

/// Adds the attribute to all items in the block.
macro_rules! cfg_block {
Expand Down

0 comments on commit b5c1cb3

Please sign in to comment.