无法在 Rust 中重现错误的缓存行共享问题
Can't reproduce false cache line sharing problem in Rust
我正在尝试重现 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 微秒。
我将 criterion crate 用于基准测试。我有一台配备 Intel 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
and write_volatile
来避免该优化。
虽然这两个更改足以证明您在我的测试中寻找的效果,但请注意,使用不安全操作来改变不可变借用的数组是 undefined behavior. Rust is allowed to optimize under the assumption that unsafe
code upholds certain invariants, which this code does not, so Rust would be entirely 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])
}
我正在尝试重现 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 微秒。
我将 criterion crate 用于基准测试。我有一台配备 Intel 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
and write_volatile
来避免该优化。
虽然这两个更改足以证明您在我的测试中寻找的效果,但请注意,使用不安全操作来改变不可变借用的数组是 undefined behavior. Rust is allowed to optimize under the assumption that unsafe
code upholds certain invariants, which this code does not, so Rust would be entirely 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])
}