rusttype-conversionsuperscript

rust: improve number iterations and conversions into superscript


Currently, that's the way I'm doing it.

fn main() {
    let superscript_digits = ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"];
    let sample_num = 1234;

    for i in sample_num.to_string().chars() {
        print!("{}", superscript_digits[i.to_digit(10).unwrap() as usize])
    }
    println!();
}

playground

This gets it done with a fairly minimal amount of code. But I worry that it's not an optimal solution and the conversions are a performance impairment.

In the final implementation the incoming numbers are floats, so even more conversion is happening. I would just need the rounded numbers and do something like:

fn main() {
    let superscript_digits = ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"];
    let sample_nums: Vec<f64> = [12.7, 27.4, 32.8, 41.2].to_vec();

    for num in sample_nums {
        for c in num.round().to_string().chars() {
            print!("{}", superscript_digits[c.to_digit(10).unwrap() as usize])
        }
        println!();
    }
}

I hoped that someone with more experience than myself could share a better way of doing it and why it is so.


Solution

  • Since benchmarking println!() was not very relevant in my opinion, I decided to use criterion on two variants returning a String (see superscript_v1_collect() and superscript_v2_collect() below). The v1 version is near to the original code; the v2 version tries to get rid of intermediate storage and conversions. The result was disappointing (see my_benchmark.rs below) because the performances were not so different and were varying a lot.

    I guess this is due to the memory allocation in the returned String, so I decided to produce two other variants, much less convenient but avoiding many dynamic allocations (see superscript_v1_ref_mut() and superscript_v2_ref_mut() below). This time, the results are much more stable (especially for v2) and show a substantial improvement in v2 over v1.

    Note that in v2 I chose a kind of hardcoded solution in which I know the maximal number of digits (10 for u32).

    src/lib.rs

    pub fn superscript_v1_collect(value: u32) -> String {
        const SUPERSCRIPT_DIGITS: [&str; 10] =
            ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"];
        value
            .to_string()
            .chars()
            .map(|c| SUPERSCRIPT_DIGITS[c.to_digit(10).unwrap() as usize])
            .collect()
    }
    
    pub fn superscript_v2_collect(mut value: u32) -> String {
        const SUPERSCRIPT_DIGITS: [char; 10] =
            ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'];
        let mut started = false;
        let mut power_of_ten = 1_000_000_000;
        if value == 0 {
            '⁰'.to_string()
        } else {
            (0..10)
                .filter_map(|_| {
                    let digit = value / power_of_ten;
                    value -= digit * power_of_ten;
                    power_of_ten /= 10;
                    if digit != 0 || started {
                        started = true;
                        Some(SUPERSCRIPT_DIGITS[digit as usize])
                    } else {
                        None
                    }
                })
                .collect()
        }
    }
    
    pub fn superscript_v1_ref_mut(
        value: u32,
        result: &mut String,
    ) {
        const SUPERSCRIPT_DIGITS: [&str; 10] =
            ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"];
        result.clear();
        for c in value.to_string().chars() {
            result.push_str(SUPERSCRIPT_DIGITS[c.to_digit(10).unwrap() as usize]);
        }
    }
    
    pub fn superscript_v2_ref_mut(
        mut value: u32,
        result: &mut String,
    ) {
        const SUPERSCRIPT_DIGITS: [char; 10] =
            ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'];
        result.clear();
        let mut started = false;
        let mut power_of_ten = 1_000_000_000;
        if value == 0 {
            result.push('⁰')
        } else {
            for _ in 0..10 {
                let digit = value / power_of_ten;
                value -= digit * power_of_ten;
                power_of_ten /= 10;
                if digit != 0 || started {
                    started = true;
                    result.push(SUPERSCRIPT_DIGITS[digit as usize]);
                }
            }
        }
    }
    

    src/main.rs

    use prog::{
        superscript_v1_collect, superscript_v1_ref_mut, superscript_v2_collect,
        superscript_v2_ref_mut,
    };
    
    fn main() {
        let sample_nums = [
            0,
            12,
            345,
            6_789,
            12_345,
            678_912,
            3_456_789,
            12_345_678,
            912_345_678,
        ];
        let mut s1 = String::new();
        let mut s2 = String::new();
        for num in sample_nums.iter() {
            superscript_v1_ref_mut(*num, &mut s1);
            superscript_v2_ref_mut(*num, &mut s2);
            println!(
                "{}  {}  {}  {}  {}",
                *num,
                superscript_v1_collect(*num),
                superscript_v2_collect(*num),
                s1,
                s2
            );
        }
    }
    /*
    0  ⁰  ⁰  ⁰  ⁰
    12  ¹²  ¹²  ¹²  ¹²
    345  ³⁴⁵  ³⁴⁵  ³⁴⁵  ³⁴⁵
    6789  ⁶⁷⁸⁹  ⁶⁷⁸⁹  ⁶⁷⁸⁹  ⁶⁷⁸⁹
    12345  ¹²³⁴⁵  ¹²³⁴⁵  ¹²³⁴⁵  ¹²³⁴⁵
    678912  ⁶⁷⁸⁹¹²  ⁶⁷⁸⁹¹²  ⁶⁷⁸⁹¹²  ⁶⁷⁸⁹¹²
    3456789  ³⁴⁵⁶⁷⁸⁹  ³⁴⁵⁶⁷⁸⁹  ³⁴⁵⁶⁷⁸⁹  ³⁴⁵⁶⁷⁸⁹
    12345678  ¹²³⁴⁵⁶⁷⁸  ¹²³⁴⁵⁶⁷⁸  ¹²³⁴⁵⁶⁷⁸  ¹²³⁴⁵⁶⁷⁸
    912345678  ⁹¹²³⁴⁵⁶⁷⁸  ⁹¹²³⁴⁵⁶⁷⁸  ⁹¹²³⁴⁵⁶⁷⁸  ⁹¹²³⁴⁵⁶⁷⁸
    */
    

    benches/my_benchmark.rs

    use criterion::{black_box, criterion_group, criterion_main, Criterion};
    
    use prog::{
        superscript_v1_collect, superscript_v1_ref_mut, superscript_v2_collect,
        superscript_v2_ref_mut,
    };
    
    fn benchmark(c: &mut Criterion) {
        let mut c = c.benchmark_group("benches");
        let sample_nums = [
            0,
            12,
            345,
            6_789,
            12_345,
            678_912,
            3_456_789,
            12_345_678,
            912_345_678,
        ];
        let mut s1 = String::new();
        let mut s2 = String::new();
        c.bench_function("superscript_v1_collect", |b| {
            b.iter(|| {
                for num in sample_nums.iter() {
                    black_box(superscript_v1_collect(black_box(*num)));
                }
            })
        });
        c.bench_function("superscript_v2_collect", |b| {
            b.iter(|| {
                for num in sample_nums.iter() {
                    black_box(superscript_v2_collect(black_box(*num)));
                }
            })
        });
        c.bench_function("superscript_v1_ref_mut", |b| {
            b.iter(|| {
                for num in sample_nums.iter() {
                    superscript_v1_ref_mut(black_box(*num), black_box(&mut s1));
                }
            })
        });
        c.bench_function("superscript_v2_ref_mut", |b| {
            b.iter(|| {
                for num in sample_nums.iter() {
                    superscript_v2_ref_mut(black_box(*num), black_box(&mut s2));
                }
            })
        });
    }
    
    criterion_group!(benches, benchmark);
    criterion_main!(benches);
    /*
    superscript_v1_collect ... [578.36 ns 580.18 ns 583.00 ns]
    superscript_v2_collect ... [571.34 ns 572.70 ns 574.11 ns]
    superscript_v1_ref_mut ... [321.37 ns 321.79 ns 322.27 ns]
    superscript_v2_ref_mut ... [271.51 ns 272.00 ns 272.59 ns]
    */