Skip to content
Merged
5 changes: 5 additions & 0 deletions .vscode/cspell.dictionaries/jargon.wordlist.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
AFAICT
asimd
ASIMD
alloc
arity
autogenerate
Expand Down Expand Up @@ -71,6 +73,7 @@ hardlink
hardlinks
hasher
hashsums
hwcaps
infile
iflag
iflags
Expand Down Expand Up @@ -149,6 +152,8 @@ tokenize
toolchain
totalram
truthy
tunables
TUNABLES
ucase
unbuffered
udeps
Expand Down
13 changes: 9 additions & 4 deletions src/uu/wc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,21 @@ workspace = true
path = "src/wc.rs"

[dependencies]
clap = { workspace = true }
uucore = { workspace = true, features = ["parser", "pipes", "quoting-style"] }
bytecount = { workspace = true, features = ["runtime-dispatch-simd"] }
clap = { workspace = true }
fluent = { workspace = true }
thiserror = { workspace = true }
uucore = { workspace = true, features = [
"hardware",
"parser",
"pipes",
"quoting-style",
] }
unicode-width = { workspace = true }
fluent = { workspace = true }

[target.'cfg(unix)'.dependencies]
nix = { workspace = true }
libc = { workspace = true }
nix = { workspace = true }

[dev-dependencies]
divan = { workspace = true }
Expand Down
7 changes: 7 additions & 0 deletions src/uu/wc/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,10 @@ decoder-error-io = underlying bytestream error: { $error }
# Other messages
wc-standard-input = standard input
wc-total = total

# Debug messages
wc-debug-hw-unavailable = debug: hardware support unavailable on this CPU
wc-debug-hw-using = debug: using hardware support (features: { $features })
wc-debug-hw-disabled-env = debug: hardware support disabled by environment
wc-debug-hw-disabled-glibc = debug: hardware support disabled by GLIBC_TUNABLES ({ $features })
wc-debug-hw-limited-glibc = debug: hardware support limited by GLIBC_TUNABLES (disabled: { $disabled }; enabled: { $enabled })
7 changes: 7 additions & 0 deletions src/uu/wc/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,10 @@ decoder-error-io = erreur du flux d'octets sous-jacent : { $error }
# Autres messages
wc-standard-input = entrée standard
wc-total = total

# Messages de débogage
wc-debug-hw-unavailable = debug : prise en charge matérielle indisponible sur ce CPU
wc-debug-hw-using = debug : utilisation de l'accélération matérielle (fonctions : { $features })
wc-debug-hw-disabled-env = debug : prise en charge matérielle désactivée par l'environnement
wc-debug-hw-disabled-glibc = debug : prise en charge matérielle désactivée par GLIBC_TUNABLES ({ $features })
wc-debug-hw-limited-glibc = debug : prise en charge matérielle limitée par GLIBC_TUNABLES (désactivé : { $disabled } ; activé : { $enabled })
17 changes: 14 additions & 3 deletions src/uu/wc/src/count_fast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
// file that was distributed with this source code.

// cSpell:ignore sysconf
use crate::word_count::WordCount;
use crate::{wc_simd_allowed, word_count::WordCount};
use uucore::hardware::SimdPolicy;

use super::WordCountable;

Expand Down Expand Up @@ -232,6 +233,8 @@ pub(crate) fn count_bytes_chars_and_lines_fast<
) -> (WordCount, Option<io::Error>) {
let mut total = WordCount::default();
let buf: &mut [u8] = &mut AlignedBuffer::default().data;
let policy = SimdPolicy::detect();
let simd_allowed = wc_simd_allowed(policy);
loop {
match handle.read(buf) {
Ok(0) => return (total, None),
Expand All @@ -240,10 +243,18 @@ pub(crate) fn count_bytes_chars_and_lines_fast<
total.bytes += n;
}
if COUNT_CHARS {
total.chars += bytecount::num_chars(&buf[..n]);
total.chars += if simd_allowed {
bytecount::num_chars(&buf[..n])
} else {
bytecount::naive_num_chars(&buf[..n])
};
}
if COUNT_LINES {
total.lines += bytecount::count(&buf[..n], b'\n');
total.lines += if simd_allowed {
bytecount::count(&buf[..n], b'\n')
} else {
bytecount::naive_count(&buf[..n], b'\n')
};
}
}
Err(ref e) if e.kind() == ErrorKind::Interrupted => (),
Expand Down
127 changes: 126 additions & 1 deletion src/uu/wc/src/wc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ use uucore::translate;
use uucore::{
error::{FromIo, UError, UResult},
format_usage,
hardware::{HardwareFeature, HasHardwareFeatures as _, SimdPolicy},
parser::shortcut_value_parser::ShortcutValueParser,
quoting_style::{self, QuotingStyle},
show,
show, show_error,
};

use crate::{
Expand All @@ -49,6 +50,7 @@ struct Settings<'a> {
show_lines: bool,
show_words: bool,
show_max_line_length: bool,
debug: bool,
files0_from: Option<Input<'a>>,
total_when: TotalWhen,
}
Expand All @@ -62,6 +64,7 @@ impl Default for Settings<'_> {
show_lines: true,
show_words: true,
show_max_line_length: false,
debug: false,
files0_from: None,
total_when: TotalWhen::default(),
}
Expand All @@ -85,6 +88,7 @@ impl<'a> Settings<'a> {
show_lines: matches.get_flag(options::LINES),
show_words: matches.get_flag(options::WORDS),
show_max_line_length: matches.get_flag(options::MAX_LINE_LENGTH),
debug: matches.get_flag(options::DEBUG),
files0_from,
total_when,
};
Expand All @@ -95,6 +99,7 @@ impl<'a> Settings<'a> {
Self {
files0_from: settings.files0_from,
total_when,
debug: settings.debug,
..Default::default()
}
}
Expand Down Expand Up @@ -122,6 +127,7 @@ mod options {
pub static MAX_LINE_LENGTH: &str = "max-line-length";
pub static TOTAL: &str = "total";
pub static WORDS: &str = "words";
pub static DEBUG: &str = "debug";
}
static ARG_FILES: &str = "files";
static STDIN_REPR: &str = "-";
Expand Down Expand Up @@ -445,6 +451,12 @@ pub fn uu_app() -> Command {
.help(translate!("wc-help-words"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::DEBUG)
.long(options::DEBUG)
.action(ArgAction::SetTrue)
.hide(true),
)
.arg(
Arg::new(ARG_FILES)
.action(ArgAction::Append)
Expand Down Expand Up @@ -805,6 +817,74 @@ fn escape_name_wrapper(name: &OsStr) -> String {
.expect("All escaped names with the escaping option return valid strings.")
}

fn hardware_feature_label(feature: HardwareFeature) -> &'static str {
match feature {
HardwareFeature::Avx512 => "AVX512F",
HardwareFeature::Avx2 => "AVX2",
HardwareFeature::PclMul => "PCLMUL",
HardwareFeature::Vmull => "VMULL",
HardwareFeature::Sse2 => "SSE2",
HardwareFeature::Asimd => "ASIMD",
}
}

fn is_simd_runtime_feature(feature: &HardwareFeature) -> bool {
matches!(
feature,
HardwareFeature::Avx2 | HardwareFeature::Sse2 | HardwareFeature::Asimd
)
}

fn is_simd_debug_feature(feature: &HardwareFeature) -> bool {
matches!(
feature,
HardwareFeature::Avx512
| HardwareFeature::Avx2
| HardwareFeature::Sse2
| HardwareFeature::Asimd
)
}

struct WcSimdFeatures {
enabled: Vec<HardwareFeature>,
disabled: Vec<HardwareFeature>,
disabled_runtime: Vec<HardwareFeature>,
}

fn wc_simd_features(policy: &SimdPolicy) -> WcSimdFeatures {
let enabled = policy
.iter_features()
.filter(is_simd_runtime_feature)
.collect();

let mut disabled = Vec::new();
let mut disabled_runtime = Vec::new();
for feature in policy.disabled_features() {
if is_simd_debug_feature(&feature) {
disabled.push(feature);
}
if is_simd_runtime_feature(&feature) {
disabled_runtime.push(feature);
}
}

WcSimdFeatures {
enabled,
disabled,
disabled_runtime,
}
}

pub(crate) fn wc_simd_allowed(policy: &SimdPolicy) -> bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we cache this ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's already cached elsewhere, it's a tough call whether to cache it here.

let disabled_features = policy.disabled_features();
if disabled_features.iter().any(is_simd_runtime_feature) {
return false;
}
policy
.iter_features()
.any(|feature| is_simd_runtime_feature(&feature))
}

fn wc(inputs: &Inputs, settings: &Settings) -> UResult<()> {
let mut total_word_count = WordCount::default();
let mut num_inputs: usize = 0;
Expand All @@ -814,6 +894,51 @@ fn wc(inputs: &Inputs, settings: &Settings) -> UResult<()> {
_ => (compute_number_width(inputs, settings), true),
};

if settings.debug {
let policy = SimdPolicy::detect();
let features = wc_simd_features(policy);

let enabled: Vec<&'static str> = features
.enabled
.iter()
.copied()
.map(hardware_feature_label)
.collect();
let disabled: Vec<&'static str> = features
.disabled
.iter()
.copied()
.map(hardware_feature_label)
.collect();
Comment on lines +901 to +912
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why these two ?
if it is enabled, it can not be disabled?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enabled is derived from policy.iter_features() (hardware minus GLIBC_TUNABLES‑disabled), while disabled is the subset explicitly disabled by GLIBC_TUNABLES for debug output. They’re intentionally different sets, so you can have both when some SIMD features are available and others are disabled


let enabled_empty = enabled.is_empty();
let disabled_empty = disabled.is_empty();
Comment on lines +914 to +915
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

let runtime_disabled = !features.disabled_runtime.is_empty();

if enabled_empty && !runtime_disabled {
show_error!("{}", translate!("wc-debug-hw-unavailable"));
} else if runtime_disabled {
show_error!(
"{}",
translate!("wc-debug-hw-disabled-glibc", "features" => disabled.join(", "))
);
} else if !enabled_empty && disabled_empty {
show_error!(
"{}",
translate!("wc-debug-hw-using", "features" => enabled.join(", "))
);
} else {
show_error!(
"{}",
translate!(
"wc-debug-hw-limited-glibc",
"disabled" => disabled.join(", "),
"enabled" => enabled.join(", ")
)
);
}
}

for maybe_input in inputs.try_iter(settings)? {
num_inputs += 1;

Expand Down
3 changes: 2 additions & 1 deletion src/uucore/src/lib/features/hardware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,9 @@ impl SimdPolicy {
}
}

/// Returns true if any SIMD feature remains enabled after applying GLIBC_TUNABLES.
pub fn allows_simd(&self) -> bool {
self.disabled_by_env.is_empty()
self.iter_features().next().is_some()
}

pub fn disabled_features(&self) -> Vec<HardwareFeature> {
Expand Down
66 changes: 66 additions & 0 deletions tests/by-util/test_wc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -808,3 +808,69 @@ fn wc_w_words_with_emoji_separator() {
.succeeds()
.stdout_contains("3");
}

#[cfg(unix)]
#[test]
fn test_simd_respects_glibc_tunables() {
// Ensure debug output reflects that SIMD paths are disabled via GLIBC_TUNABLES
let debug_output = new_ucmd!()
.args(&["-l", "--debug", "/dev/null"])
.env("GLIBC_TUNABLES", "glibc.cpu.hwcaps=-AVX2,-AVX512F")
.succeeds()
.stderr_str()
.to_string();
assert!(
!debug_output.contains("using hardware support"),
"SIMD should be reported as disabled when GLIBC_TUNABLES blocks AVX features: {debug_output}"
);
assert!(
debug_output.contains("hardware support disabled"),
"Debug output should acknowledge GLIBC_TUNABLES restrictions: {debug_output}"
);

// WC results should be identical with and without GLIBC_TUNABLES overrides
let sample_sizes = [0usize, 1, 7, 128, 513, 999];
use std::fmt::Write as _;
for &lines in &sample_sizes {
let content: String = (0..lines).fold(String::new(), |mut acc, i| {
// Build the input buffer efficiently without allocating per line.
let _ = writeln!(acc, "{i}");
acc
});

let base = new_ucmd!()
.arg("-l")
.pipe_in(content.clone())
.succeeds()
.stdout_str()
.trim()
.to_string();

let no_avx512 = new_ucmd!()
.arg("-l")
.env("GLIBC_TUNABLES", "glibc.cpu.hwcaps=-AVX512F")
.pipe_in(content.clone())
.succeeds()
.stdout_str()
.trim()
.to_string();

let no_avx2_avx512 = new_ucmd!()
.arg("-l")
.env("GLIBC_TUNABLES", "glibc.cpu.hwcaps=-AVX2,-AVX512F")
.pipe_in(content)
.succeeds()
.stdout_str()
.trim()
.to_string();

assert_eq!(
base, no_avx512,
"Line counts should not change when AVX512 is disabled (lines={lines})"
);
assert_eq!(
base, no_avx2_avx512,
"Line counts should not change when AVX2/AVX512 are disabled (lines={lines})"
);
}
}
Loading