sort: pre-compute ICU collation sort keys for fast locale sorting

Add a fast_locale_collation path that pre-computes ICU sort keys during
line parsing (O(n)), then uses cheap byte-array comparison during
sorting (O(n log n)). This replaces per-comparison ICU compare_utf8
calls for the common case of plain `sort` with a UTF-8 locale.

With 1M lines and LC_ALL=en_US.UTF-8, this is ~2.6x faster than
GNU sort (314ms vs 822ms).
This commit is contained in:
Sylvestre Ledru
2026-03-20 15:35:34 +01:00
committed by Daniel Hofstetter
parent 1f9b95d68d
commit d830e84ec3
2 changed files with 27 additions and 3 deletions
+2 -2
View File
@@ -75,7 +75,7 @@ impl LineData<'_> {
impl Chunk {
/// Destroy this chunk and return its components to be reused.
pub fn recycle(mut self) -> RecycledChunk {
let recycled_contents = self.with_dependent_mut(|_, contents| {
let mut recycled_contents = self.with_dependent_mut(|_, contents| {
contents.lines.clear();
contents.line_data.selections.clear();
contents.line_data.num_infos.clear();
@@ -100,7 +100,7 @@ impl Chunk {
&mut contents.line_data.selections,
))
};
(
RecycledChunk {
lines,
selections,
num_infos: std::mem::take(&mut contents.line_data.num_infos),
+25 -1
View File
@@ -48,7 +48,7 @@ use uucore::error::{FromIo, strip_errno};
use uucore::error::{UError, UResult, USimpleError, UUsageError};
use uucore::extendedbigdecimal::ExtendedBigDecimal;
#[cfg(feature = "i18n-collator")]
use uucore::i18n::collator::locale_cmp;
use uucore::i18n::collator::{compute_sort_key_utf8, locale_cmp};
use uucore::i18n::decimal::locale_decimal_separator;
use uucore::line_ending::LineEnding;
use uucore::parser::num_parser::{ExtendedParser, ExtendedParserError};
@@ -324,6 +324,7 @@ struct Precomputed {
floats_per_line: usize,
selections_per_line: usize,
fast_lexicographic: bool,
fast_locale_collation: bool,
fast_ascii_insensitive: bool,
tokenize_blank_thousands_sep: bool,
tokenize_allow_unit_after_blank: bool,
@@ -387,6 +388,8 @@ impl GlobalSettings {
self.precomputed.fast_lexicographic =
!disable_fast_lexicographic && self.can_use_fast_lexicographic();
self.precomputed.fast_locale_collation =
disable_fast_lexicographic && self.can_use_fast_lexicographic();
self.precomputed.fast_ascii_insensitive = self.can_use_fast_ascii_insensitive();
}
@@ -632,6 +635,15 @@ impl<'a> Line<'a> {
token_buffer: &mut Vec<Field>,
settings: &GlobalSettings,
) -> Self {
#[cfg(feature = "i18n-collator")]
if settings.precomputed.fast_locale_collation {
compute_sort_key_utf8(line, &mut line_data.collation_key_buffer);
line_data
.collation_key_ends
.push(line_data.collation_key_buffer.len());
return Self { line, index };
}
let needs_line_data = settings.precomputed.needs_tokens
|| settings.precomputed.selections_per_line > 0
|| settings.precomputed.num_infos_per_line > 0
@@ -2614,6 +2626,18 @@ fn compare_by<'a>(
};
}
#[cfg(feature = "i18n-collator")]
if global_settings.precomputed.fast_locale_collation {
let a_key = a_line_data.collation_key(a.index);
let b_key = b_line_data.collation_key(b.index);
let cmp = a_key.cmp(b_key);
return if global_settings.reverse {
cmp.reverse()
} else {
cmp
};
}
if global_settings.precomputed.fast_ascii_insensitive {
let cmp = ascii_case_insensitive_cmp(a.line, b.line);
if cmp != Ordering::Equal || a.line == b.line {