From 0c26a3bc0ce507cbc2bee0b3368664760bedcf25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Falk=20H=C3=BCffner?= Date: Sun, 5 Sep 2021 20:54:29 +0200 Subject: [PATCH 1/3] Add benchmark for integer log10. --- library/core/benches/lib.rs | 1 + library/core/benches/num/int_log/mod.rs | 60 +++++++++++++++++++++++++ library/core/benches/num/mod.rs | 1 + 3 files changed, 62 insertions(+) create mode 100644 library/core/benches/num/int_log/mod.rs diff --git a/library/core/benches/lib.rs b/library/core/benches/lib.rs index de4ef7949f344..0f55d6837ebd4 100644 --- a/library/core/benches/lib.rs +++ b/library/core/benches/lib.rs @@ -1,6 +1,7 @@ // wasm32 does not support benches (no time). #![cfg(not(target_arch = "wasm32"))] #![feature(flt2dec)] +#![feature(int_log)] #![feature(test)] extern crate test; diff --git a/library/core/benches/num/int_log/mod.rs b/library/core/benches/num/int_log/mod.rs new file mode 100644 index 0000000000000..542a0a0ca08ac --- /dev/null +++ b/library/core/benches/num/int_log/mod.rs @@ -0,0 +1,60 @@ +#![feature(int_log)] + +use rand::Rng; +use test::{black_box, Bencher}; + +macro_rules! int_log_bench { + ($t:ty, $predictable:ident, $random:ident, $random_small:ident) => { + #[bench] + fn $predictable(bench: &mut Bencher) { + bench.iter(|| { + for n in 0..(<$t>::BITS / 8) { + for i in 1..=(100 as $t) { + let x = black_box(i << (n * 8)); + black_box(x.log10()); + } + } + }); + } + + #[bench] + fn $random(bench: &mut Bencher) { + let mut rng = rand::thread_rng(); + /* Exponentially distributed random numbers from the whole range of the type. */ + let numbers: Vec<$t> = (0..256) + .map(|_| { + let x = rng.gen::<$t>() >> rng.gen_range(0, <$t>::BITS); + if x != 0 { x } else { 1 } + }) + .collect(); + bench.iter(|| { + for x in &numbers { + black_box(black_box(x).log10()); + } + }); + } + + #[bench] + fn $random_small(bench: &mut Bencher) { + let mut rng = rand::thread_rng(); + /* Exponentially distributed random numbers from the range 0..256. */ + let numbers: Vec<$t> = (0..256) + .map(|_| { + let x = (rng.gen::() >> rng.gen_range(0, u8::BITS)) as $t; + if x != 0 { x } else { 1 } + }) + .collect(); + bench.iter(|| { + for x in &numbers { + black_box(black_box(x).log10()); + } + }); + } + }; +} + +int_log_bench! {u8, u8_log10_predictable, u8_log10_random, u8_log10_random_small} +int_log_bench! {u16, u16_log10_predictable, u16_log10_random, u16_log10_random_small} +int_log_bench! {u32, u32_log10_predictable, u32_log10_random, u32_log10_random_small} +int_log_bench! {u64, u64_log10_predictable, u64_log10_random, u64_log10_random_small} +int_log_bench! {u128, u128_log10_predictable, u128_log10_random, u128_log10_random_small} diff --git a/library/core/benches/num/mod.rs b/library/core/benches/num/mod.rs index 852d4e481e20d..2f9cad2725d7f 100644 --- a/library/core/benches/num/mod.rs +++ b/library/core/benches/num/mod.rs @@ -1,5 +1,6 @@ mod dec2flt; mod flt2dec; +mod int_log; use std::str::FromStr; use test::Bencher; From d53c483502216bd9b3b2ff0fa1ce463690492d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Falk=20H=C3=BCffner?= Date: Sun, 5 Sep 2021 22:55:29 +0200 Subject: [PATCH 2/3] Speed up integer log10. This is achieved with a branchless bit-twiddling implementation of the case x < 100_000, and using this as building block. Benchmark on an Intel i7-8700K (Coffee Lake): name old ns/iter new ns/iter diff ns/iter diff % speedup num::int_log::u8_log10_predictable 165 169 4 2.42% x 0.98 num::int_log::u8_log10_random 438 423 -15 -3.42% x 1.04 num::int_log::u8_log10_random_small 438 423 -15 -3.42% x 1.04 num::int_log::u16_log10_predictable 633 417 -216 -34.12% x 1.52 num::int_log::u16_log10_random 908 471 -437 -48.13% x 1.93 num::int_log::u16_log10_random_small 945 471 -474 -50.16% x 2.01 num::int_log::u32_log10_predictable 1,496 1,340 -156 -10.43% x 1.12 num::int_log::u32_log10_random 1,076 873 -203 -18.87% x 1.23 num::int_log::u32_log10_random_small 1,145 874 -271 -23.67% x 1.31 num::int_log::u64_log10_predictable 4,005 3,171 -834 -20.82% x 1.26 num::int_log::u64_log10_random 1,247 1,021 -226 -18.12% x 1.22 num::int_log::u64_log10_random_small 1,265 921 -344 -27.19% x 1.37 num::int_log::u128_log10_predictable 39,667 39,579 -88 -0.22% x 1.00 num::int_log::u128_log10_random 6,456 6,696 240 3.72% x 0.96 num::int_log::u128_log10_random_small 4,108 3,903 -205 -4.99% x 1.05 Benchmark on an M1 Mac Mini: name old ns/iter new ns/iter diff ns/iter diff % speedup num::int_log::u8_log10_predictable 143 130 -13 -9.09% x 1.10 num::int_log::u8_log10_random 375 325 -50 -13.33% x 1.15 num::int_log::u8_log10_random_small 376 325 -51 -13.56% x 1.16 num::int_log::u16_log10_predictable 500 322 -178 -35.60% x 1.55 num::int_log::u16_log10_random 794 405 -389 -48.99% x 1.96 num::int_log::u16_log10_random_small 1,035 405 -630 -60.87% x 2.56 num::int_log::u32_log10_predictable 1,144 894 -250 -21.85% x 1.28 num::int_log::u32_log10_random 832 786 -46 -5.53% x 1.06 num::int_log::u32_log10_random_small 832 787 -45 -5.41% x 1.06 num::int_log::u64_log10_predictable 2,681 2,057 -624 -23.27% x 1.30 num::int_log::u64_log10_random 1,015 806 -209 -20.59% x 1.26 num::int_log::u64_log10_random_small 1,004 795 -209 -20.82% x 1.26 num::int_log::u128_log10_predictable 56,825 56,526 -299 -0.53% x 1.01 num::int_log::u128_log10_random 9,056 8,861 -195 -2.15% x 1.02 num::int_log::u128_log10_random_small 1,528 1,527 -1 -0.07% x 1.00 The 128 bit case remains ridiculously slow because llvm fails to optimize division by a constant 128-bit value to multiplications. This could be worked around but it seems preferable to fix this in llvm. From u32 up, table lookup (like suggested here https://github.com/rust-lang/rust/issues/70887#issuecomment-881099813) is still faster, but requires a hardware leading_zero to be viable, and might clog up the cache. --- library/core/src/num/int_log10.rs | 107 ++++++++++++++---------------- library/core/tests/num/int_log.rs | 3 + 2 files changed, 54 insertions(+), 56 deletions(-) diff --git a/library/core/src/num/int_log10.rs b/library/core/src/num/int_log10.rs index e4599067f85f7..d28552cf4aa4a 100644 --- a/library/core/src/num/int_log10.rs +++ b/library/core/src/num/int_log10.rs @@ -1,76 +1,71 @@ mod unchecked { // 0 < val <= u8::MAX pub const fn u8(val: u8) -> u32 { - if val >= 100 { - 2 - } else if val >= 10 { - 1 - } else { - 0 - } + let val = val as u32; + + // For better performance, avoid branches by assembling the solution + // in the bits above the low 8 bits. + + // Adding c1 to val gives 10 in the top bits for val < 10, 11 for val >= 10 + let c1 = 0b11_00000000 - 10; // 758 + // Adding c2 to val gives 01 in the top bits for val < 100, 10 for val >= 100 + let c2 = 0b10_00000000 - 100; // 412 + + // Value of top bits: + // +c1 +c2 1&2 + // 0..=9 10 01 00 = 0 + // 10..=99 11 01 01 = 1 + // 100..=255 11 10 10 = 2 + ((val + c1) & (val + c2)) >> 8 } - // 0 < val <= u16::MAX - pub const fn u16(val: u16) -> u32 { - if val >= 10_000 { - 4 - } else if val >= 1000 { - 3 - } else if val >= 100 { - 2 - } else if val >= 10 { - 1 - } else { - 0 - } + // 0 < val < 100_000 + const fn less_than_5(val: u32) -> u32 { + // Similar to u8, when adding one of these constants to val, + // we get two possible bit patterns above the low 17 bits, + // depending on whether val is below or above the threshold. + let c1 = 0b011_00000000000000000 - 10; // 393206 + let c2 = 0b100_00000000000000000 - 100; // 524188 + let c3 = 0b111_00000000000000000 - 1000; // 916504 + let c4 = 0b100_00000000000000000 - 10000; // 514288 + + // Value of top bits: + // +c1 +c2 1&2 +c3 +c4 3&4 ^ + // 0..=9 010 011 010 110 011 010 000 = 0 + // 10..=99 011 011 011 110 011 010 001 = 1 + // 100..=999 011 100 000 110 011 010 010 = 2 + // 1000..=9999 011 100 000 111 011 011 011 = 3 + // 10000..=99999 011 100 000 111 100 100 100 = 4 + (((val + c1) & (val + c2)) ^ ((val + c3) & (val + c4))) >> 17 } - // 0 < val < 100_000_000 - const fn less_than_8(mut val: u32) -> u32 { - let mut log = 0; - if val >= 10_000 { - val /= 10_000; - log += 4; - } - log + if val >= 1000 { - 3 - } else if val >= 100 { - 2 - } else if val >= 10 { - 1 - } else { - 0 - } + // 0 < val <= u16::MAX + pub const fn u16(val: u16) -> u32 { + less_than_5(val as u32) } // 0 < val <= u32::MAX pub const fn u32(mut val: u32) -> u32 { let mut log = 0; - if val >= 100_000_000 { - val /= 100_000_000; - log += 8; - } - log + less_than_8(val) - } - - // 0 < val < 10_000_000_000_000_000 - const fn less_than_16(mut val: u64) -> u32 { - let mut log = 0; - if val >= 100_000_000 { - val /= 100_000_000; - log += 8; + if val >= 100_000 { + val /= 100_000; + log += 5; } - log + less_than_8(val as u32) + log + less_than_5(val) } // 0 < val <= u64::MAX pub const fn u64(mut val: u64) -> u32 { let mut log = 0; - if val >= 10_000_000_000_000_000 { - val /= 10_000_000_000_000_000; - log += 16; + if val >= 10_000_000_000 { + val /= 10_000_000_000; + log += 10; + } + if val >= 100_000 { + val /= 100_000; + log += 5; } - log + less_than_16(val) + log + less_than_5(val as u32) } // 0 < val <= u128::MAX @@ -79,13 +74,13 @@ mod unchecked { if val >= 100_000_000_000_000_000_000_000_000_000_000 { val /= 100_000_000_000_000_000_000_000_000_000_000; log += 32; - return log + less_than_8(val as u32); + return log + u32(val as u32); } if val >= 10_000_000_000_000_000 { val /= 10_000_000_000_000_000; log += 16; } - log + less_than_16(val as u64) + log + u64(val as u64) } // 0 < val <= i8::MAX diff --git a/library/core/tests/num/int_log.rs b/library/core/tests/num/int_log.rs index 1517e8a952f93..3cd0073ddd8c2 100644 --- a/library/core/tests/num/int_log.rs +++ b/library/core/tests/num/int_log.rs @@ -96,6 +96,9 @@ fn checked_log10() { for i in 1..=u16::MAX { assert_eq!(i.checked_log10(), Some((i as f32).log10() as u32)); } + for i in 1..=100_000u32 { + assert_eq!(i.checked_log10(), Some((i as f32).log10() as u32)); + } } macro_rules! log10_loop { From 57c623570a16fd6d550627ee8fed1eb7882521f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Falk=20H=C3=BCffner?= Date: Thu, 9 Sep 2021 20:06:46 +0200 Subject: [PATCH 3/3] Cosmetic fixes. --- library/core/benches/num/int_log/mod.rs | 2 -- library/core/src/num/int_log10.rs | 16 ++++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/library/core/benches/num/int_log/mod.rs b/library/core/benches/num/int_log/mod.rs index 542a0a0ca08ac..6a219bcdd55aa 100644 --- a/library/core/benches/num/int_log/mod.rs +++ b/library/core/benches/num/int_log/mod.rs @@ -1,5 +1,3 @@ -#![feature(int_log)] - use rand::Rng; use test::{black_box, Bencher}; diff --git a/library/core/src/num/int_log10.rs b/library/core/src/num/int_log10.rs index d28552cf4aa4a..398bb07a07e4c 100644 --- a/library/core/src/num/int_log10.rs +++ b/library/core/src/num/int_log10.rs @@ -7,16 +7,16 @@ mod unchecked { // in the bits above the low 8 bits. // Adding c1 to val gives 10 in the top bits for val < 10, 11 for val >= 10 - let c1 = 0b11_00000000 - 10; // 758 + const C1: u32 = 0b11_00000000 - 10; // 758 // Adding c2 to val gives 01 in the top bits for val < 100, 10 for val >= 100 - let c2 = 0b10_00000000 - 100; // 412 + const C2: u32 = 0b10_00000000 - 100; // 412 // Value of top bits: // +c1 +c2 1&2 // 0..=9 10 01 00 = 0 // 10..=99 11 01 01 = 1 // 100..=255 11 10 10 = 2 - ((val + c1) & (val + c2)) >> 8 + ((val + C1) & (val + C2)) >> 8 } // 0 < val < 100_000 @@ -24,10 +24,10 @@ mod unchecked { // Similar to u8, when adding one of these constants to val, // we get two possible bit patterns above the low 17 bits, // depending on whether val is below or above the threshold. - let c1 = 0b011_00000000000000000 - 10; // 393206 - let c2 = 0b100_00000000000000000 - 100; // 524188 - let c3 = 0b111_00000000000000000 - 1000; // 916504 - let c4 = 0b100_00000000000000000 - 10000; // 514288 + const C1: u32 = 0b011_00000000000000000 - 10; // 393206 + const C2: u32 = 0b100_00000000000000000 - 100; // 524188 + const C3: u32 = 0b111_00000000000000000 - 1000; // 916504 + const C4: u32 = 0b100_00000000000000000 - 10000; // 514288 // Value of top bits: // +c1 +c2 1&2 +c3 +c4 3&4 ^ @@ -36,7 +36,7 @@ mod unchecked { // 100..=999 011 100 000 110 011 010 010 = 2 // 1000..=9999 011 100 000 111 011 011 011 = 3 // 10000..=99999 011 100 000 111 100 100 100 = 4 - (((val + c1) & (val + c2)) ^ ((val + c3) & (val + c4))) >> 17 + (((val + C1) & (val + C2)) ^ ((val + C3) & (val + C4))) >> 17 } // 0 < val <= u16::MAX