【问题标题】:Checking a Vec<u8> to see if it's all zero?检查 Vec<u8> 看它是否全为零?
【发布时间】:2021-03-29 17:28:43
【问题描述】:

我有许多 4KiB 缓冲区,它们有 50% 的机会只包含零值。非零缓冲区通常在缓冲区的早期有一个非零字节。

fn is_zero(buf: &Vec<u8>) -> bool {
    for byte in buf.into_iter() {
        if *byte != 0 {
            return false;
        }
    }
    return true;
}

这是使用--release 签入 Rust 的一种高效方式吗? (我正在处理许多 GB 的数据。)

(在 C 版本中,我在检查之前将缓冲区转换为 unsigned long long。考虑到 SSE 等,这可能不是我能做的最好的)

【问题讨论】:

标签: vector rust vec


【解决方案1】:

您可以使用rayon,这是一个似乎非常适合您的用例的数据并行库。使用起来非常简单:只需将buf.iter() 更改为buf.par_iter(),其余的交给Rayon:

use rayon::prelude::*;

fn is_zero_par(buf: &[u8]) -> bool {
    buf.par_iter().all(|&b| b == 0)
}

对于 2000 万个元素的向量,人造丝的性能提高了 7 倍:

#![feature(test)]
use rayon::prelude::*;
extern crate test;

fn v() -> Vec<u8> {
    std::iter::repeat(0).take(20000000).collect()
}

fn is_zero(buf: &[u8]) -> bool {
    buf.into_iter().all(|&b| b == 0)
}

fn is_zero_par(buf: &[u8]) -> bool {
    buf.par_iter().all(|&b| b == 0)
}

#[bench]
fn bench_is_zero(b: &mut test::Bencher) {
    let v = test::black_box(v());
    b.iter(|| is_zero(&v[..]))
}

#[bench]
fn bench_is_zero_par(b: &mut test::Bencher) {
    let v = test::black_box(v());
    b.iter(|| is_zero_par(&v[..]))
}
running 2 tests
test tests::bench_is_zero     ... bench:   7,217,686 ns/iter (+/- 478,845)
test tests::bench_is_zero_par ... bench:   1,080,959 ns/iter (+/- 111,692)

请注意,多线程对性能的影响取决于工作负载(元素数量),较小的工作负载可能会受到负面影响。

【讨论】:

  • 虽然 OP 的缓冲区更小,但可能不会获得那么多好处。
  • @PeterHall 并行迭代缓冲区同样容易,并且对于每个缓冲区只需按顺序迭代字节。但是,OP 没有提供任何代码来阐明他的缓冲区集合。
【解决方案2】:

您可以使用align_tou8 的切片转换为u128 的切片,使比较效率更高:

fn is_zero(buf: &[u8]) -> bool {
    let (prefix, aligned, suffix) = unsafe { buf.align_to::<u128>() };

    prefix.iter().all(|&x| x == 0)
        && suffix.iter().all(|&x| x == 0)
        && aligned.iter().all(|&x| x == 0)
}

在我的机器上运行一个简单的基准测试显示 16 倍的性能提升!

#![feature(test)]
extern crate test;

fn v() -> Vec<u8> {
    std::iter::repeat(0).take(1000000).collect()
}

fn is_zero(buf: &[u8]) -> bool {
    buf.into_iter().all(|&b| b == 0)
}

fn is_zero_aligned(buf: &[u8]) -> bool {
    let (prefix, aligned, suffix) = unsafe { buf.align_to::<u128>() };

    prefix.iter().all(|&x| x == 0)
        && suffix.iter().all(|&x| x == 0)
        && aligned.iter().all(|&x| x == 0)
}

#[bench]
fn bench_is_zero(b: &mut test::Bencher) {
    let v = test::black_box(v());
    b.iter(|| is_zero(&v[..]))
}

#[bench]
fn bench_is_zero_aligned(b: &mut test::Bencher) {
    let v = test::black_box(v());
    b.iter(|| is_zero_aligned(&v[..]))
}
running 2 tests
test tests::bench_is_zero         ... bench:     455,975 ns/iter (+/- 414)
test tests::bench_is_zero_aligned ... bench:      28,615 ns/iter (+/- 116)

根据您的机器,不同的整数类型 (u64) 可能会产生更好的性能。

感谢 Rust discord 服务器上的@Globi 提出这个想法

【讨论】:

  • 不错。我不知道align_to
【解决方案3】:

下面的函数是纯保存Rust

fn is_zero ( slice : &[u8] ) -> bool {
    for i in (0..slice.len()).step_by(16) {
        if slice.len() - i >= 16 {
            let arr : [u8; 16] = slice[i..i+16].try_into().expect("this should always succeed");
            if u128::from_be_bytes(arr) != 0 {
                return false;
            }
        } else {
            for i in i..slice.len() {
                if slice[i] != 0 {
                    return false;
                }
            }
        }
    }
    return true;
}

具体来说,它使用u128::from_be_bytes 函数将[u8; 16] 数组转换为u128 作为非操作,并使用TryInto trait 将适当长度的[u8] 转换为@987654327 @ — 其余的相当琐碎。可以手动展开内部循环以对其进行转换,但我怀疑这将是一个重要的性能瓶颈,构成列表尾部的u8s 不是干净的 16 字节将起作用。

根据处理器的不同,使用 u64 甚至 u32 可能会更快,您必须自己分析。

【讨论】:

  • 如果切片不是 16 字节对齐的,try_into 会失败吗?
  • @PeterHall 不,它不会因为对齐而失败;它只能因为长度而失败。我检查了实施。您可能会想到 SSE,其中一些指令在给定未对齐的地址时可能会失败。
  • 嗯,那么在无法处理未对齐整数的架构上会发生什么?
  • @PeterHall 小数组在变为整数之前存储在堆栈中。因此,在禁止未对齐整数加载的架构上,数组加载可能会变成一系列字节加载。然而,流行的架构允许不对齐的字加载,即使这种缺乏对齐会带来额外的性能损失,通常只有当加载跨越缓存行甚至页面边界时。所以,很好,您在响应中发现了一个(小)缺陷。
  • @Peter_Hall,它不会失败,但这是一个非常好的优化点,因为未对齐的转换显然会更慢。一个正确的实现也会单独做列表的头部,直到到达一个对齐的指针并从那里开始。
【解决方案4】:

在我的笔记本电脑上使用byteorder,通过一次读取u64,以本机字节序找到4倍的加速。

lib.rs

extern crate byteorder;

use byteorder::{NativeEndian, ReadBytesExt};
use std::io::Cursor;

pub fn one(buf: &[u8]) -> bool {
    buf.into_iter().all(|&byte| byte == 0)
}

pub fn two(buf: &[u8]) -> bool {
    let mut cur = Cursor::new(buf);
    while let Ok(val) = cur.read_u64::<NativeEndian>() {
        if val != 0 {
            return false;
        }
    }
    while let Ok(val) = cur.read_u8() {
        if val != 0 {
            return false;
        }
    }
    true
}

长凳/benches.rs

#![feature(test)]

extern crate test;
extern crate zero_slice_8;

use zero_slice_8::{one, two};

fn v() -> Vec<u8> {
    let mut result = vec![];
    for _ in 0..100000 {
        result.push(0);
    }
    result
}

#[bench]
fn bench_one(b: &mut test::Bencher) {
    let v = v();
    b.iter(|| one(&v[..]))
}

#[bench]
fn bench_two(b: &mut test::Bencher) {
    let v = v();
    b.iter(|| two(&v[..]))
}

【讨论】:

    猜你喜欢
    • 2020-10-28
    • 1970-01-01
    • 1970-01-01
    • 2011-04-20
    • 2012-04-27
    • 2021-07-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多