Skip to content

Commit d535824

Browse files
authored
Merge pull request uutils#8442 from sylvestre/tr-perf
Improve tr performances (Closes: uutils#8439)
2 parents 620360a + e7fdeb3 commit d535824

2 files changed

Lines changed: 117 additions & 37 deletions

File tree

src/uu/tr/BENCHMARKING.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Benchmarking `tr`
2+
3+
<!-- spell-checker:ignore aeiou -->
4+
5+
`tr` performance is critical for large data processing pipelines. The implementation uses lookup tables for O(1) character operations.
6+
7+
## Building
8+
9+
```shell
10+
cargo build --release --package uu_tr
11+
```
12+
13+
## Basic Benchmarks
14+
15+
### Character Translation
16+
17+
```shell
18+
# Create test data
19+
dd if=/dev/zero bs=1M count=10 | tr '\0' 'x' > 10mb_input
20+
21+
# Benchmark translation
22+
hyperfine --warmup 3 \
23+
'tr a b < 10mb_input > /dev/null' \
24+
'./target/release/tr a b < 10mb_input > /dev/null'
25+
```
26+
27+
### Character Deletion
28+
29+
```shell
30+
# Create mixed input
31+
yes "abcdefghijklmnopqrstuvwxyz" | head -n 1000000 > mixed_input
32+
33+
# Benchmark deletion
34+
hyperfine --warmup 3 \
35+
'tr -d aeiou < mixed_input > /dev/null' \
36+
'./target/release/tr -d aeiou < mixed_input > /dev/null'
37+
```
38+
39+
### Character Classes
40+
41+
```shell
42+
# Benchmark character classes
43+
hyperfine --warmup 3 \
44+
'tr "[:lower:]" "[:upper:]" < mixed_input > /dev/null' \
45+
'./target/release/tr "[:lower:]" "[:upper:]" < mixed_input > /dev/null'
46+
```
47+
48+
## Quick Regression Test
49+
50+
```shell
51+
#!/bin/bash
52+
cargo build --release --package uu_tr
53+
54+
echo "=== Translation ==="
55+
hyperfine 'tr a b < 10mb_input > /dev/null' './target/release/tr a b < 10mb_input > /dev/null'
56+
57+
echo "=== Deletion ==="
58+
hyperfine 'tr -d aeiou < mixed_input > /dev/null' './target/release/tr -d aeiou < mixed_input > /dev/null'
59+
```
60+
61+
## Performance Notes
62+
63+
- Uses lookup tables instead of hash maps for O(1) operations
64+
- 32KB I/O buffers for improved throughput
65+
- Should be competitive with GNU `tr` for most operations
66+

src/uu/tr/src/operation.rs

Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,9 @@ use nom::{
1717
};
1818
use std::{
1919
char,
20-
collections::{HashMap, HashSet},
2120
error::Error,
2221
fmt::{Debug, Display},
2322
io::{BufRead, Write},
24-
ops::Not,
2523
};
2624
use uucore::error::{FromIo, UError, UResult};
2725
use uucore::translate;
@@ -583,42 +581,58 @@ impl<A: SymbolTranslator, B: SymbolTranslator> SymbolTranslator for ChainedSymbo
583581
}
584582
}
585583

584+
/// Convert a set of bytes to a 256-element bitmap for O(1) lookup
585+
fn set_to_bitmap(set: &[u8]) -> [bool; 256] {
586+
let mut bitmap = [false; 256];
587+
for &byte in set {
588+
bitmap[byte as usize] = true;
589+
}
590+
bitmap
591+
}
592+
586593
#[derive(Debug)]
587594
pub struct DeleteOperation {
588-
set: Vec<u8>,
595+
delete_table: [bool; 256],
589596
}
590597

591598
impl DeleteOperation {
592599
pub fn new(set: Vec<u8>) -> Self {
593-
Self { set }
600+
Self {
601+
delete_table: set_to_bitmap(&set),
602+
}
594603
}
595604
}
596605

597606
impl SymbolTranslator for DeleteOperation {
598607
fn translate(&mut self, current: u8) -> Option<u8> {
599-
// keep if not present in the set
600-
self.set.contains(&current).not().then_some(current)
608+
// keep if not present in the delete set
609+
(!self.delete_table[current as usize]).then_some(current)
601610
}
602611
}
603612

604613
#[derive(Debug)]
605614
pub struct TranslateOperation {
606-
translation_map: HashMap<u8, u8>,
615+
translation_table: [u8; 256],
607616
}
608617

609618
impl TranslateOperation {
610619
pub fn new(set1: Vec<u8>, set2: Vec<u8>) -> Result<Self, BadSequence> {
620+
// Initialize translation table with identity mapping
621+
let mut translation_table = std::array::from_fn(|i| i as u8);
622+
611623
if let Some(fallback) = set2.last().copied() {
612-
Ok(Self {
613-
translation_map: set1
614-
.into_iter()
615-
.zip(set2.into_iter().chain(std::iter::repeat(fallback)))
616-
.collect::<HashMap<_, _>>(),
617-
})
624+
// Apply translations from set1 to set2
625+
for (from, to) in set1
626+
.into_iter()
627+
.zip(set2.into_iter().chain(std::iter::repeat(fallback)))
628+
{
629+
translation_table[from as usize] = to;
630+
}
631+
632+
Ok(Self { translation_table })
618633
} else if set1.is_empty() && set2.is_empty() {
619-
Ok(Self {
620-
translation_map: HashMap::new(),
621-
})
634+
// Identity mapping for empty sets
635+
Ok(Self { translation_table })
622636
} else {
623637
Err(BadSequence::EmptySet2WhenNotTruncatingSet1)
624638
}
@@ -627,33 +641,28 @@ impl TranslateOperation {
627641

628642
impl SymbolTranslator for TranslateOperation {
629643
fn translate(&mut self, current: u8) -> Option<u8> {
630-
Some(
631-
self.translation_map
632-
.get(&current)
633-
.copied()
634-
.unwrap_or(current),
635-
)
644+
Some(self.translation_table[current as usize])
636645
}
637646
}
638647

639648
#[derive(Debug, Clone)]
640649
pub struct SqueezeOperation {
641-
set1: HashSet<u8>,
650+
squeeze_table: [bool; 256],
642651
previous: Option<u8>,
643652
}
644653

645654
impl SqueezeOperation {
646655
pub fn new(set1: Vec<u8>) -> Self {
647656
Self {
648-
set1: set1.into_iter().collect(),
657+
squeeze_table: set_to_bitmap(&set1),
649658
previous: None,
650659
}
651660
}
652661
}
653662

654663
impl SymbolTranslator for SqueezeOperation {
655664
fn translate(&mut self, current: u8) -> Option<u8> {
656-
let next = if self.set1.contains(&current) {
665+
let next = if self.squeeze_table[current as usize] {
657666
match self.previous {
658667
Some(v) if v == current => None,
659668
_ => Some(current),
@@ -672,19 +681,26 @@ where
672681
R: BufRead,
673682
W: Write,
674683
{
675-
let mut buf = [0; 8192];
676-
let mut output_buf = Vec::new();
684+
const BUFFER_SIZE: usize = 32768; // Large buffer for better throughput
685+
let mut buf = [0; BUFFER_SIZE];
686+
let mut output_buf = Vec::with_capacity(buf.len());
687+
688+
loop {
689+
let length = match input.read(&mut buf[..]) {
690+
Ok(0) => break, // EOF reached
691+
Ok(len) => len,
692+
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
693+
Err(e) => return Err(e.map_err_context(|| translate!("tr-error-read-error"))),
694+
};
677695

678-
while let Ok(length) = input.read(&mut buf[..]) {
679-
if length == 0 {
680-
break; // EOF reached
696+
// Process the buffer and collect translated chars to output
697+
output_buf.clear();
698+
for &byte in &buf[..length] {
699+
if let Some(translated) = translator.translate(byte) {
700+
output_buf.push(translated);
701+
}
681702
}
682703

683-
let filtered = buf[..length]
684-
.iter()
685-
.filter_map(|&c| translator.translate(c));
686-
output_buf.extend(filtered);
687-
688704
#[cfg(not(target_os = "windows"))]
689705
output
690706
.write_all(&output_buf)
@@ -699,8 +715,6 @@ where
699715
return Err(err.map_err_context(|| translate!("tr-error-write-error")));
700716
}
701717
}
702-
703-
output_buf.clear();
704718
}
705719

706720
Ok(())

0 commit comments

Comments
 (0)