【问题标题】:More efficient way to iterate through columns of flattened 2D Vec更有效的方法来遍历展平的 2D Vec 列
【发布时间】:2020-09-20 06:34:30
【问题描述】:

所以我有一个Vec<bool> 存储在self.board.data 中,它代表一个二维网格。逐行遍历它并在其索引和 x,y 坐标之间进行转换很容易,但是要逐列索引它,我需要使用嵌套的 for 循环。有没有更有效的方法来做到这一点?我有与这个类似的函数,它们会运行一堆次,所以我想让它们尽可能高效。

    ///returns average height of all columns
    fn calc_avg_height(&self) -> f32 {
        let mut heights = 0;
        for x in 0..self.board.width {
            for y in 0..self.board.height {
                if self.board.data[(y*self.board.width)+x] {
                    heights+=self.board.height-y;
                    break
                }
            }
        }
        heights as f32/self.board.width as f32
    }

【问题讨论】:

    标签: indexing rust iterator


    【解决方案1】:

    无论你做什么,逐列读取数据都会很慢,因为你会在内存中跳跃。与此相比,计算索引是一种噪音。

    有没有更有效的方法来做到这一点?

    一个想法是将位放在一起以减少 8 个内存。另一个想法是逐列保存数据,仅在需要时更新(这取决于您的访问模式)。

    【讨论】:

    • 这是正确答案。一个流行的位向量包是“bit-vec”。除了 bit-vec 字段之外,保留行和列的两个向量来总结长度应该可以在大多数情况下提供最佳性能。
    【解决方案2】:

    您可以预加载索引,因此您只需要遍历它们。这可能在时间上更有效,但在空间上却没有:

    fn preload_indexes(row_size: usize, column_size: usize) -> Vec<usize> {
        let mut indexes = Vec::new();
        for x in 0..row_size {
            for y in 0..column_size {
                let i = y*row_size+x;
                indexes.push(i);
            }
        }
        indexes
    }
    

    作为一个完整的例子:

    use std::fmt::Display;
    
    struct Matrix<T: Display> {
        row_size: usize,
        column_size: usize,
        data: Vec<T>,
        columns_indexes: Vec<usize>,
    }
    
    
    fn preload_indexes(row_size: usize, column_size: usize) -> Vec<usize> {
        let mut indexes = Vec::new();
        for x in 0..row_size {
            for y in 0..column_size {
                let i = y*row_size+x;
                indexes.push(i);
            }
        }
        indexes
    }
    
    impl<T: Display> Matrix<T> {
        fn new(data: Vec<T>, row_size: usize, column_size: usize) -> Self {
            assert_eq!(data.len(), row_size*column_size);
            
            Self {
                row_size,
                column_size,
                data,
                columns_indexes: preload_indexes(row_size, column_size),
            }
        }
        
        fn print_rows(&self) {
            for e in self.data.iter() {
                println!("{}", e);
            }
        }
        
        fn print_columns(&self) {
            for i in self.columns_indexes.iter() {
                println!("{}", self.data[*i]);
            }
        }
    }
    
    
    fn main() {
        let matrix = Matrix::new(vec![1, 2, 3, 4], 2, 2);
        println!("Rows");
        matrix.print_rows();
        println!("Columns");
        matrix.print_columns();
    }
    

    Playground

    【讨论】:

    • 从缓存中读取普通索引可能比计算它们慢。如果它们不在缓存中,肯定会更慢。
    【解决方案3】:

    围绕着你的例子,我很惊讶地看到 索引计算(y*width+x)似乎不是 由优化器简化。 此外,我虽然内存访问更重要 比索引计算中删除的乘法。 但是当谈到时间安排时,我可以看到很大的不同 (具有各种网格大小和真/假初始化)。

    编辑

    看了下面的一些cmets,对比了两个版本, _v1_v2,在编译器资源管理器 (godbolt) 中。 它们看起来非常相似,除了 _v1 使用 lea 指令和 _v2 使用 addinc 代替。 根据vtune-amplifier,_v1的大部分时间是 花费在这个确切的lea 指令上。 所以,至少在我的电脑上,在这个具体的例子上, 与直觉相反,选择 lea 而不是 addinc 似乎比不规则的内存访问更有害 模式。

    /*
      $ rustc -C opt-level=3 prog.rs && ./prog
      avg1=50.5
        v1: 4453 ms
      avg2=50.5
        v2: 2361 ms
    */
    
    pub struct Board {
        width: usize,
        height: usize,
        data: Vec<bool>,
    }
    
    impl Board {
        pub fn new(
            width: usize,
            height: usize,
        ) -> Self {
            Self {
                width,
                height,
                data: vec![false; width * height],
            }
        }
    
        pub fn calc_avg_height_v1(&self) -> f32 {
            let mut heights = 0;
            for x in 0..self.width {
                for y in 0..self.height {
                    if self.data[(y * self.width) + x] {
                        heights += self.height - y;
                        break;
                    }
                }
            }
            heights as f32 / self.width as f32
        }
    
        pub fn calc_avg_height_v2(&self) -> f32 {
            let mut heights = 0;
            for x in 0..self.width {
                let mut idx = x;
                for y in 0..self.height {
                    if self.data[idx] {
                        heights += self.height - y;
                        break;
                    }
                    idx += self.width;
                }
            }
            heights as f32 / self.width as f32
        }
    }
    
    pub fn run<F>(
        name: &str,
        repeat: usize,
        fnct: F,
    ) -> u128
    where
        F: Fn() -> f32,
    {
        let mut prev = -1.0_f32;
        let warmup = repeat / 10;
        for _ in 0..warmup {
            let avg = fnct();
            if avg != prev {
                println!("{}={}", name, avg);
                prev = avg;
            }
        }
        let now = std::time::Instant::now();
        for _ in 0..repeat {
            let avg = fnct();
            if avg != prev {
                println!("{}={}", name, avg);
                prev = avg;
            }
        }
        now.elapsed().as_millis()
    }
    
    pub fn main() {
        let mut board = Board::new(100, 100);
        for y in 0..board.height {
            for x in 0..board.width {
                board.data[y * board.width + x] = x == y;
            }
        }
        let repeat = 1_000_000;
        println!(
            "  v1: {} ms",
            run("avg1", repeat, || board.calc_avg_height_v1())
        );
        println!(
            "  v2: {} ms",
            run("avg2", repeat, || board.calc_avg_height_v2())
        );
    }
    

    【讨论】:

    • 哦,谢谢!这真是太好了。我刚刚在我目前正在优化的模块中完成并实施了新的索引技术,哇它有什么不同。正确地说:我在一个有 10 个线程的线程池上进行了 1000 次批处理测试。使用 Vec> 需要 6m15s~ 才能完成。在我切换到 Vec 后,它会持续 15 分钟。但现在我实施了您的索引技术,它下降到 5 分 30 秒。
    • 你的基准测试是错误的,它没有考虑缓存效果,很难做到。 Rust 确实提供了用于基准测试的工具,使用它们来获得可比较的结果。此外,正如已经写过的,索引的计算几乎是一个 noop,可能会变成一个 lea 指令。内存访问是可以优化的地方。
    • '让 mut idx = x;'不应该是'让mut idx =(y * self.width)+ x'吗?显然,选择这样的 idx 会产生更好的计时结果,以及完全错误的计算结果。
    • 不,他说得对。迭代列,使其从 y0 开始,然后对于每个内部循环,它增加一个 y 级别。另外我有检查以确保预测结果和实际结果匹配,到目前为止他们有 100% 的时间使用它。我还应该提到,线程池的东西并不是要成为基准。这就是我正在运行的程序的运行方式。我知道 rusts 基准测试功能,并且我一直在使用它们。我认为 id 只是给出整体结果以添加一些透视它所产生的效果
    • @KonstantinW 正如我的答案正文中所述,我的直觉与您的直觉相同,这就是为什么我发布了与这种直觉相矛盾的答案。关于idx,对不起,我不明白你的反对意见;这个想法是用乘法换加法,我认为这个公式给出了完全相同的索引序列。
    猜你喜欢
    • 2019-09-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-02
    • 2017-02-03
    • 2018-12-12
    • 2012-05-10
    • 1970-01-01
    相关资源
    最近更新 更多