我正在尝试重现处理器缓存效果库的示例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微秒。
我使用标准板条箱作为基准。我有一台配备英特尔i5-5257U CPU(2.70GHz)的MacBook Pro 2015。我的系统报告有64B
缓存线大小。
如果有人想查看我的完整基准测试代码,这里有链接:-lib.rs-cache_lines.rs
我想了解这个问题,并找到重现文章中类似结果的方法。
您的第一个问题是*p.wrapping_add(3)
对指针而不是整数进行算术运算。循环的第一次迭代是在p
后三个空格加载值,并将其存储在p
中,Rust将循环的其他999999次迭代作为冗余进行优化。你的意思是(*p).wrapping_add(3)
。
在那次更改之后,Rust将1000000次添加优化为3000000次添加。您可以使用read_volatile
和write_volatile
来避免这种优化。
尽管这两个更改足以证明您在我的测试中所寻找的效果,但请注意,使用不安全的操作来变异免疫借用的数组是未定义的行为。Rust可以在假设unsafe
代码支持某些不变量的情况下进行优化,而这段代码没有,因此Rust完全有权用它感觉到的任何东西替换你的代码。
您可能使用了不可变借用来绕过在线程之间复制可变引用和可变指针的限制。我认为,有一种不那么明确的方法可以绕过这一限制(尽管说实话,如果有人回复指出这仍然是错误的,我不会太惊讶)。
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])
}