无法在Rust中重现错误的缓存行共享问题

时间:2019-01-10 16:33:19

标签: rust benchmarking cpu-cache false-sharing

我正在尝试复制Gallery of Processor Cache Effects的示例6。

本文以C#中的此功能为例,介绍了如何测试虚假共享:

private static int[] s_counter = new int[1024];
private void UpdateCounter(int position)
{
    for (int j = 0; j < 100000000; j++)
    {
        s_counter[position] = s_counter[position] + 3;
    }
}

如果我们创建传递给该函数0、1、2、3自变量的线程,则将需要很长时间才能完成计算(作者花了4.3秒)。例如,如果通过16、32、48、64,我们将获得更好的结果(0.28秒)。

我在Rust中提出了以下功能:

pub fn cache_line_sharing(arr: [i32; 128], pos: usize) -> (i32, i32) {
    let arr = Arc::new(arr);
    let handles: Vec<_> = (0..4).map(|thread_number| {
        let arr = arr.clone();
        let pos = thread_number * pos;
        thread::spawn(move || unsafe {
            let p = (arr.as_ptr() as *mut i32).offset(pos as isize);
            for _ in 0..1_000_000 {
                *p = (*p).wrapping_add(3);
            }
        })
    }).collect();

    for handle in handles {
        handle.join().unwrap();
    }

    (arr[0], arr[1])
}

使用两组参数(0、1、2、3和0、16、32、48)对它进行基准测试,结果几乎相同:108.34和105.07微秒。

我将标准板条箱用作基准。我有一台配备Intel i5-5257U CPU(2.70GHz)的MacBook Pro 2015。我的系统报告具有64B个缓存行大小。

如果有人想查看我的完整基准测试代码,请点击以下链接: -lib.rs -cache_lines.rs

我想了解问题所在,并找到重现本文中类似结果的方法。

1 个答案:

答案 0 :(得分:6)

您的第一个问题是*p.wrapping_add(3)对指针而不是对整数进行算术运算。循环的第一次迭代是在p之后加载三个值并将其存储在p中,Rust正在将循环的其他999999次迭代优化为冗余。您的意思是(*p).wrapping_add(3)

更改后,Rust将1000000个添加项优化为3个,将一个添加项优化为3000000个。您可以使用read_volatilewrite_volatile来避免这种优化。

尽管这两项更改足以证明您在测试中寻找的效果,但请注意,使用不安全的操作来更改不可变借用的数组是undefined behavior。假设unsafe代码支持某些不变量,而Rust不支持该代码,则Rust可以进行优化,因此Rust完全within its rights可以用任何感觉替换代码。

您大概使用了不可变借位来解决在线程之间复制可变引用和可变指针的限制。我认为,这是解决该限制的一种不太明确的方法(尽管说实话,如果有人回答指出仍然存在这种错误的某种方式,我也不会感到惊讶)。

pub fn cache_line_sharing(arr: [i32; 128], pos: usize) -> (i32, i32) {
    struct SyncWrapper(UnsafeCell<[i32; 128]>);
    unsafe impl Sync for SyncWrapper {}

    assert_ne!(pos, 0);
    let arr = Arc::new(SyncWrapper(UnsafeCell::new(arr)));
    let handles: Vec<_> = (0..4)
        .map(|thread_number| {
            let arr = arr.clone();
            let pos = thread_number * pos;
            thread::spawn(move || unsafe {
                let p: *mut i32 = &mut (*arr.0.get())[pos];
                for _ in 0..1_000_000 {
                    p.write_volatile(p.read_volatile().wrapping_add(3));
                }
            })
        })
        .collect();

    for handle in handles {
        handle.join().unwrap();
    }

    let arr = unsafe { *arr.0.get() };
    (arr[0], arr[1])
}