Last active
November 24, 2023 04:20
-
-
Save ClementTsang/e242f12f7e1d1902ed414dcc18c3b321 to your computer and use it in GitHub Desktop.
A simple fast path for truncate_str code when the entire string is ASCII. Related PR: https://github.com/ClementTsang/bottom/pull/1330
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//! ```cargo | |
//! [dependencies] | |
//! unicode-segmentation = "1.10.1" | |
//! unicode-width = "0.1.11" | |
//! | |
//! [dev-dependencies] | |
//! criterion = "0.5" | |
//! | |
//! [[bench]] | |
//! name = "truncate_str_bench" | |
//! harness = false | |
//! ``` | |
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; | |
use unicode_segmentation::UnicodeSegmentation; | |
use unicode_width::UnicodeWidthStr; | |
#[inline] | |
fn grapheme_width(g: &str) -> usize { | |
if g.contains('\u{200d}') { | |
2 | |
} else { | |
UnicodeWidthStr::width(g) | |
} | |
} | |
#[inline] | |
fn fast_ascii<U: Into<usize>>(content: &str, width: U) -> String { | |
let width = width.into(); | |
if width > 0 { | |
if content.is_ascii() { | |
// If the entire string is ASCII, we can use a much simpler approach. | |
if content.len() <= width { | |
content.to_owned() | |
} else { | |
let mut text = String::with_capacity(width); | |
let (keep, _throw) = content.split_at(width - 1); | |
text.push_str(keep); | |
text.push('…'); | |
text | |
} | |
} else { | |
let mut text = String::with_capacity(width); | |
let mut curr_width = 0; | |
let mut early_break = false; | |
// This tracks the length of the last added string - note this does NOT match the grapheme *width*. | |
let mut last_added_str_len = 0; | |
// Cases to handle: | |
// - Completes adding the entire string. | |
// - Adds a character up to the boundary, then fails. | |
// - Adds a character not up to the boundary, then fails. | |
// Inspired by https://tomdebruijn.com/posts/rust-string-length-width-calculations/ | |
for g in UnicodeSegmentation::graphemes(content, true) { | |
let g_width = grapheme_width(g); | |
if curr_width + g_width <= width { | |
curr_width += g_width; | |
last_added_str_len = g.len(); | |
text.push_str(g); | |
} else { | |
early_break = true; | |
break; | |
} | |
} | |
if early_break { | |
if curr_width == width { | |
// Remove the last grapheme cluster added. | |
text.truncate(text.len() - last_added_str_len); | |
} | |
text.push('…'); | |
} | |
text | |
} | |
} else { | |
String::default() | |
} | |
} | |
#[inline] | |
fn original<U: Into<usize>>(content: &str, width: U) -> String { | |
let width = width.into(); | |
let mut text = String::with_capacity(width); | |
if width > 0 { | |
let mut curr_width = 0; | |
let mut early_break = false; | |
// This tracks the length of the last added string - note this does NOT match the grapheme *width*. | |
let mut last_added_str_len = 0; | |
// Cases to handle: | |
// - Completes adding the entire string. | |
// - Adds a character up to the boundary, then fails. | |
// - Adds a character not up to the boundary, then fails. | |
// Inspired by https://tomdebruijn.com/posts/rust-string-length-width-calculations/ | |
for g in UnicodeSegmentation::graphemes(content, true) { | |
let g_width = grapheme_width(g); | |
if curr_width + g_width <= width { | |
curr_width += g_width; | |
last_added_str_len = g.len(); | |
text.push_str(g); | |
} else { | |
early_break = true; | |
break; | |
} | |
} | |
if early_break { | |
if curr_width == width { | |
// Remove the last grapheme cluster added. | |
text.truncate(text.len() - last_added_str_len); | |
} | |
text.push('…'); | |
} | |
} | |
text | |
} | |
fn ascii_only(c: &mut Criterion) { | |
let mut group = c.benchmark_group("ascii only"); | |
let content = "012345678910"; | |
for i in [1_usize, 5, 10, 15].iter() { | |
group.bench_with_input(BenchmarkId::new("original", i), i, |b, i| { | |
b.iter(|| original(black_box(content), *i)) | |
}); | |
group.bench_with_input(BenchmarkId::new("fast_ascii", i), i, |b, i| { | |
b.iter(|| fast_ascii(black_box(content), *i)) | |
}); | |
} | |
group.finish(); | |
} | |
fn header(c: &mut Criterion) { | |
let mut group = c.benchmark_group("bottom header example"); | |
let content = "CPU(c)▲"; | |
for i in [1_usize, 2, 3, 4, 5, 6, 7, 8, 9, 10].iter() { | |
group.bench_with_input(BenchmarkId::new("original", i), i, |b, i| { | |
b.iter(|| original(black_box(content), *i)) | |
}); | |
group.bench_with_input(BenchmarkId::new("fast_ascii", i), i, |b, i| { | |
b.iter(|| fast_ascii(black_box(content), *i)) | |
}); | |
} | |
group.finish(); | |
} | |
fn unicode_only(c: &mut Criterion) { | |
let mut group = c.benchmark_group("unicode only"); | |
let content = "施氏食獅施氏食獅"; | |
for i in [1_usize, 5, 10, 20].iter() { | |
group.bench_with_input(BenchmarkId::new("original", i), i, |b, i| { | |
b.iter(|| original(black_box(content), *i)) | |
}); | |
group.bench_with_input(BenchmarkId::new("fast_ascii", i), i, |b, i| { | |
b.iter(|| fast_ascii(black_box(content), *i)) | |
}); | |
} | |
group.finish(); | |
} | |
fn ascii_unicode_mix(c: &mut Criterion) { | |
let mut group = c.benchmark_group("ascii unicode mix"); | |
let content = "abc施氏食獅abc"; | |
for i in [1_usize, 5, 10, 20].iter() { | |
group.bench_with_input(BenchmarkId::new("original", i), i, |b, i| { | |
b.iter(|| original(black_box(content), *i)) | |
}); | |
group.bench_with_input(BenchmarkId::new("fast_ascii", i), i, |b, i| { | |
b.iter(|| fast_ascii(black_box(content), *i)) | |
}); | |
} | |
group.finish(); | |
} | |
criterion_group!(benches, ascii_only, header, unicode_only, ascii_unicode_mix); | |
criterion_main!(benches); | |
/* | |
Results running `cargo criterion` (Ryzen 7 5700X, 32GiB RAM, Arch Linux 6.6.1): | |
ascii only/original/1 time: [56.157 ns 56.539 ns 57.029 ns] | |
ascii only/fast_ascii/1 time: [23.273 ns 23.364 ns 23.487 ns] | |
ascii only/original/5 time: [113.56 ns 113.90 ns 114.27 ns] | |
ascii only/fast_ascii/5 time: [20.109 ns 20.175 ns 20.228 ns] | |
ascii only/original/10 time: [179.32 ns 179.66 ns 180.21 ns] | |
ascii only/fast_ascii/10 | |
time: [19.607 ns 19.620 ns 19.633 ns] | |
ascii only/original/15 time: [188.93 ns 189.24 ns 189.50 ns] | |
ascii only/fast_ascii/15 | |
time: [13.407 ns 13.422 ns 13.450 ns] | |
bottom header example/original/1 | |
time: [55.596 ns 55.666 ns 55.748 ns] | |
bottom header example/fast_ascii/1 | |
time: [56.849 ns 56.890 ns 56.934 ns] | |
bottom header example/original/2 | |
time: [69.841 ns 70.205 ns 70.862 ns] | |
bottom header example/fast_ascii/2 | |
time: [70.885 ns 71.126 ns 71.559 ns] | |
bottom header example/original/3 | |
time: [82.544 ns 82.652 ns 82.751 ns] | |
bottom header example/fast_ascii/3 | |
time: [83.702 ns 83.815 ns 83.930 ns] | |
bottom header example/original/4 | |
time: [97.009 ns 97.120 ns 97.237 ns] | |
bottom header example/fast_ascii/4 | |
time: [96.921 ns 97.015 ns 97.114 ns] | |
bottom header example/original/5 | |
time: [111.65 ns 111.91 ns 112.28 ns] | |
bottom header example/fast_ascii/5 | |
time: [113.44 ns 114.05 ns 115.17 ns] | |
bottom header example/original/6 | |
time: [129.86 ns 130.11 ns 130.47 ns] | |
bottom header example/fast_ascii/6 | |
time: [130.50 ns 131.22 ns 131.79 ns] | |
bottom header example/original/7 | |
time: [131.41 ns 131.84 ns 132.20 ns] | |
bottom header example/fast_ascii/7 | |
time: [135.24 ns 135.35 ns 135.47 ns] | |
bottom header example/original/8 | |
time: [128.13 ns 128.43 ns 128.83 ns] | |
bottom header example/fast_ascii/8 | |
time: [134.53 ns 134.68 ns 134.85 ns] | |
bottom header example/original/9 | |
time: [126.76 ns 126.94 ns 127.15 ns] | |
bottom header example/fast_ascii/9 | |
time: [126.28 ns 126.57 ns 127.00 ns] | |
bottom header example/original/10 | |
time: [127.37 ns 127.91 ns 128.37 ns] | |
bottom header example/fast_ascii/10 | |
time: [122.80 ns 124.35 ns 126.61 ns] | |
unicode only/original/1 time: [56.105 ns 56.200 ns 56.302 ns] | |
unicode only/fast_ascii/1 | |
time: [56.408 ns 56.463 ns 56.519 ns] | |
unicode only/original/5 time: [84.313 ns 84.413 ns 84.520 ns] | |
unicode only/fast_ascii/5 | |
time: [83.555 ns 83.731 ns 83.957 ns] | |
unicode only/original/10 | |
time: [131.49 ns 131.59 ns 131.69 ns] | |
unicode only/fast_ascii/10 | |
time: [130.64 ns 131.28 ns 132.12 ns] | |
unicode only/original/20 | |
time: [184.25 ns 184.59 ns 185.07 ns] | |
unicode only/fast_ascii/20 | |
time: [182.64 ns 182.73 ns 182.83 ns] | |
ascii unicode mix/original/1 | |
time: [55.785 ns 55.889 ns 56.001 ns] | |
ascii unicode mix/fast_ascii/1 | |
time: [56.824 ns 56.908 ns 56.992 ns] | |
ascii unicode mix/original/5 | |
time: [104.29 ns 104.41 ns 104.55 ns] | |
ascii unicode mix/fast_ascii/5 | |
time: [101.65 ns 101.73 ns 101.82 ns] | |
ascii unicode mix/original/10 | |
time: [135.70 ns 135.79 ns 135.91 ns] | |
ascii unicode mix/fast_ascii/10 | |
time: [133.93 ns 135.05 ns 137.08 ns] | |
ascii unicode mix/original/20 | |
time: [173.62 ns 174.01 ns 174.48 ns] | |
ascii unicode mix/fast_ascii/20 | |
time: [171.50 ns 172.04 ns 172.81 ns] | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment