pub(crate) mod scalar;
#[cfg(target_arch = "x86_64")]
pub(crate) mod x86;
use scalar::scalar_text_run_len;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ScanBackend {
Scalar,
Avx2,
Avx512,
}
impl ScanBackend {
pub(crate) const fn name(self) -> &'static str {
match self {
ScanBackend::Scalar => "scalar",
ScanBackend::Avx2 => "avx2",
ScanBackend::Avx512 => "avx512",
}
}
}
pub(crate) fn text_run_len(bytes: &[u8], backend: ScanBackend) -> usize {
match backend {
ScanBackend::Scalar => scalar_text_run_len(bytes),
ScanBackend::Avx2 => {
#[cfg(target_arch = "x86_64")]
{
unsafe { x86::avx2_text_run_len(bytes) }
}
#[cfg(not(target_arch = "x86_64"))]
{
let _ = bytes;
scalar_text_run_len(bytes)
}
}
ScanBackend::Avx512 => {
#[cfg(target_arch = "x86_64")]
{
unsafe { x86::avx512_text_run_len(bytes) }
}
#[cfg(not(target_arch = "x86_64"))]
{
let _ = bytes;
scalar_text_run_len(bytes)
}
}
}
}
pub(crate) fn active_backend() -> ScanBackend {
*SELECTED.get_or_init(select_backend)
}
#[inline]
pub(crate) fn text_run_len_auto(bytes: &[u8]) -> usize {
text_run_len(bytes, active_backend())
}
pub(crate) fn set_backend_for_tests(backend: ScanBackend) {
let _ = SELECTED.set(backend);
}
static SELECTED: std::sync::OnceLock<ScanBackend> = std::sync::OnceLock::new();
fn select_backend() -> ScanBackend {
if let Ok(v) = std::env::var("LIBXML_RS_SCAN_BACKEND") {
match v.to_ascii_lowercase().as_str() {
"scalar" => return ScanBackend::Scalar,
"avx2" => return ScanBackend::Avx2,
"avx512" | "avx-512" => return ScanBackend::Avx512,
_ => {}
}
}
policy()
}
fn policy() -> ScanBackend {
#[cfg(target_arch = "x86_64")]
{
if std::arch::is_x86_feature_detected!("avx2") {
return ScanBackend::Avx2;
}
}
ScanBackend::Scalar
}
pub(crate) fn backend_supported(backend: ScanBackend) -> bool {
match backend {
ScanBackend::Scalar => true,
ScanBackend::Avx2 => {
#[cfg(target_arch = "x86_64")]
{
std::arch::is_x86_feature_detected!("avx2")
}
#[cfg(not(target_arch = "x86_64"))]
{
false
}
}
ScanBackend::Avx512 => {
#[cfg(target_arch = "x86_64")]
{
std::arch::is_x86_feature_detected!("avx512f")
&& std::arch::is_x86_feature_detected!("avx512bw")
}
#[cfg(not(target_arch = "x86_64"))]
{
false
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn expected(b: &[u8]) -> usize {
scalar_text_run_len(b)
}
#[test]
fn backends_agree_on_boundaries() {
let supported: Vec<ScanBackend> =
[ScanBackend::Scalar, ScanBackend::Avx2, ScanBackend::Avx512]
.into_iter()
.filter(|b| backend_supported(*b))
.collect();
assert!(!supported.is_empty());
let mut data: Vec<u8> = Vec::with_capacity(320);
for len in 0..260usize {
data.clear();
let bytes_of_interest = [
b'<', b'&', b']', b'\n', b'\r', 0x00, 0x1F, 0x20, 0x7E, 0x7F, 0x80, 0xC3, 0xFF,
b'a', b'~', b'\t', b' ', b'5', b']', b'<',
];
for i in 0..len {
data.push(bytes_of_interest[i % bytes_of_interest.len()]);
}
for start in 0..len {
let want = expected(&data[start..]);
for be in &supported {
assert_eq!(
text_run_len(&data[start..], *be),
want,
"backend {:?} start {start} len {len}",
be
);
}
}
}
for len in [63usize, 64, 65, 127, 128, 129, 191, 192, 193, 1000] {
let data = vec![b'x'; len];
for be in &supported {
assert_eq!(text_run_len(&data, *be), len, "clean len {len} {:?}", be);
}
}
}
#[test]
fn selection_consistency() {
let be = select_backend();
assert!(backend_supported(be), "{:?} must be supported", be);
}
#[test]
fn backends_agree_under_prng_fuzz() {
let supported: Vec<ScanBackend> =
[ScanBackend::Scalar, ScanBackend::Avx2, ScanBackend::Avx512]
.into_iter()
.filter(|b| backend_supported(*b))
.collect();
assert!(supported.len() >= 2, "need a vector backend to compare");
let mut rng = XorShift64(0x16_7_7_7_7_7);
let mut data: Vec<u8> = Vec::with_capacity(1 << 20);
for iter in 0..400_000u64 {
let len = match iter % 16 {
0..=5 => (rng.next() % 130) as usize, 6..=9 => (rng.next() % 70) as usize + 30, 10..=12 => (rng.next() % 12) as usize + 60, 13 => (rng.next() % 260) as usize,
14 => (rng.next() % 4096) as usize,
_ => (rng.next() % 64) as usize, };
data.clear();
let dist = iter % 8;
for i in 0..len {
let b = match dist {
0 => (rng.next() & 0xFF) as u8,
1 => {
if (rng.next() & 0x1F) == 0 {
[b'<', b'&', b']', b'\n', b'\r'][(rng.next() % 5) as usize]
} else {
0x20 + (rng.next() % 0x5F) as u8
}
}
2 => {
if (rng.next() & 0x3F) == 0 {
[0x80, 0xC2, 0xE2, 0xF0, 0xFF][(rng.next() % 5) as usize]
} else {
b'a' + (rng.next() % 26) as u8
}
}
3 => [b' ', b'\t', b'\n', b'\r', 0x0C, 0x00, 0x1F][(rng.next() % 7) as usize],
4 => 0x20 + (rng.next() % 0x5F) as u8,
5 => [b'<', b'a', b']'][(rng.next() % 3) as usize],
6 => {
if i + 1 == len {
b']'
} else {
b'a'
}
}
_ => {
if i == len / 2 {
b'<'
} else {
0x30 + (rng.next() % 0x4F) as u8
}
}
};
data.push(b);
}
let want = expected(&data);
for be in &supported {
let got = text_run_len(&data, *be);
assert_eq!(
got, want,
"iter {iter} len {len} dist {dist} backend {:?}",
be
);
}
if len >= 8 {
let off = (rng.next() as usize) % 8;
let want = expected(&data[off..]);
for be in &supported {
assert_eq!(
text_run_len(&data[off..], *be),
want,
"iter {iter} unaligned off {off} len {} backend {:?}",
len - off,
be
);
}
}
}
}
struct XorShift64(u64);
impl XorShift64 {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
}
}