为什么矩阵乘法与并发性比没有并发性慢?

时间:2016-05-21 08:36:20

标签: concurrency rust

我的程序中有一些部分导入一个包含两个矩阵的文件并将它们相乘。

但我很困惑为什么并发的持续时间比没有并发的持续时间长?

我的代码中是否有任何错误?

// without concurrency
let mut result = vec![];

let time1 = Instant::now();
for i in 0..n {
    let mut temp_vector = vec![];
    for j in 0..n {
        let mut temp_num = 0;
        for multiple_count in 0..m {
            temp_num = temp_num + arr1[i][multiple_count] * arr2[multiple_count][j];
        }
        temp_vector.push(temp_num);
    }
    result.push(temp_vector);
}
let time2 = Instant::now();
println!("normal solving result:\n");

for i in 0..n {
    for j in 0..n {
        print!("{:?} ", result[i][j]);
    }
   println!("");
}
let pass = time2.duration_since(time1);
println!("{:?}\n",pass);

println!("concurrency solving solution:\n");


// start the concurrency

let mut handles = vec![];

let arr1 = Arc::new(RwLock::new(arr1));
let arr2 = Arc::new(RwLock::new(arr2));

let count_time1 = Instant::now();
for i in 0..n {
    for j in 0..n {
        let arr1 = arr1.clone();
        let arr2 = arr2.clone();
        let handle = thread::spawn(move || {
            let mut count = 0;
            let arr1 = arr1.try_read().unwrap();
            let arr2 = arr2.try_read().unwrap();
            for k in 0..m {
                count = count + arr1[i][k] * arr2[k][j];
            }
            count
        });
        handles.push(handle);
    }
}
let count_time2 = Instant::now();
let pass_time = count_time2.duration_since(count_time1);

2 个答案:

答案 0 :(得分:4)

有几个原因,但少数原因会跳出来:

  1. 你每次都要克隆两个弧,这涉及一个原子加法,这很慢。虽然它们不在线程中,但它会延迟每个线程的START。
  2. 您每次都使用读写互斥(RWLock)。即使它只是一个读取,这几乎肯定涉及至少一个原子写入,以及一些Mutex状态的原子读取,到一些内部计数器。
  3. 产生OS线程不是免费的或即时的。它有启动成本。
  4. 确保您使用足够大小的数据!我们通常至少会说n * n = 10000来从并行性中获得任何明显的信息。通常几个数量级以上。这是3为什么不好的部分原因。
  5. Rust不使用轻量级协同程序。这些是完整的操作系统线程。您可以更好地生成尽可能多的线程,因为您拥有逻辑核心(逻辑意味着物理+超线程,您的操作系统应将它们全部报告为实际核心)并在所有核心上均匀分配成本。
  6. 你可能通过抛弃RWLock(因为你的数据是只读的,你不需要它)来获得相当大的加速,因为Arc只是延迟了启动时间和所需的时间要加入的线程(因为它需要删除Arc)。但是,到目前为止,你最大的加速只会产生4-8个线程,具体取决于你的处理器。我会告诉你如何最好地把它分成几块,但它相当简单。

    编辑:事实上,你也可以摆脱Arc,因为线程会立即加入,但是根据Rust线程的生命周期怪异,你可能需要crossbeam::scoped来自crossbeam的功能。 1}} crate实际上让它工作。

    顺便说一句,一旦你转向并行到同一个数据结构,我强烈建议你查看处理器缓存的信息,特别是虚假共享。虽然互斥量可能是Rust中较高的成本,但如果你能以某种方式避开它们(例如通过用split_mut分割切片),你可能会因为不断地使边界周围的缓存无效而导致错误。

答案 1 :(得分:3)

不考虑太多细节:你正在产生n²个线程 - 结果矩阵中每个单元格一个。产生线程很昂贵(注意Rust不使用“绿色线程”,但默认使用系统线程)。

并发并不是简单地加快一切;一个人必须对此有点聪明。通常,您只想利用所有CPU内核,因此您应该只生成与内核一样多的线程。在你的情况下,产生线程可能比线程花费更多的时间,因此减速。