关于在堆上分配数组(例如[i32]),堆栈溢出已经存在几个问题。一般建议是装箱,例如Box<[i32]>。但是,虽然装箱对较小的数组足够好,但问题是要装箱的数组必须首先在堆栈上分配。
因此,如果数组太大(比如1,000万个元素),即使是装箱,也会得到堆栈溢出(不太可能有那么大的堆栈)。
接下来的建议是使用Vec<T>,即我们的示例中的Vec<i32>。虽然这样做确实有效,但它确实会对性能产生影响。
考虑以下方案:
fn main() {
const LENGTH: usize = 10_000;
let mut a: [i32; LENGTH] = [0; LENGTH];
for j in 0..LENGTH {
for i in 0..LENGTH {
a[i] = j as i32;
}
}
}time告诉我,这个程序大约需要2.9秒才能运行。我在这个例子中使用了10,000,所以我可以在堆栈上分配它,但是我真的想要一个有1000万的。
现在考虑使用相同的程序,但是使用Vec<T>:
fn main() {
const LENGTH: usize = 10_000;
let mut a: Vec<i32> = vec![0; LENGTH];
for j in 0..LENGTH {
for i in 0..LENGTH {
a[i] = j as i32;
}
}
}time告诉我,这个程序大约需要5秒才能运行。现在time并不是非常精确,但是对于这样一个简单的程序来说,大约2秒的差异并不是一个微不足道的影响。
存储就是存储,当数组被装箱时,带有数组的程序同样快速。所以,不是堆减慢了Vec<T>版本,而是Vec<T>结构本身。
我还尝试使用HashMap (特别是HashMap<usize, i32>来模拟数组结构),但这比Vec<T>解决方案慢得多。
如果我的LENGTH是一千万,第一个版本甚至都不会运行。
如果这是不可能的,那么在堆中是否存在类似于数组(和Vec<T>)的结构,但是可以与数组的速度和性能相匹配?
发布于 2018-12-09 10:30:42
Summary:您的基准测试存在缺陷;只需使用 Vec (如描述的 这里),可能与 into_boxed_slice一起使用,因为它不太可能比堆分配的数组慢得多。
不幸的是,你的基准是有缺陷的。首先,您可能没有使用优化进行编译(--release用于cargo,-O用于rustc)。因为如果是这样的话,Rust编译器就会删除所有的代码。见这里的集会。为什么?因为你从来没有观察过向量/数组,所以一开始就不需要做所有的工作。
而且,您的基准测试并不是测试实际要测试的内容。您正在比较堆栈分配的数组和堆分配的向量。您应该将Vec与堆分配的数组进行比较。
不过,不要感到难过:编写基准测试是难以置信的困难,原因很多。记住:如果你对写基准不太了解,最好不要先问别人,就不要相信自己的基准。
我修复了您的基准测试,并包含了所有三种可能性:Vec、堆栈上的数组和堆上的数组。您可以找到完整的代码这里。研究结果如下:
running 3 tests
test array_heap ... bench: 9,600,979 ns/iter (+/- 1,438,433)
test array_stack ... bench: 9,232,623 ns/iter (+/- 720,699)
test vec_heap ... bench: 9,259,912 ns/iter (+/- 691,095)令人惊讶的是:版本之间的差异远远小于度量的差异。所以我们可以假设它们都相当快。
请注意,这个基准仍然非常糟糕。这两个循环可以替换为一个循环,将所有数组元素设置为LENGTH - 1。从快速查看程序集(以及从相当长的9ms时间来看),我认为LLVM不够聪明,无法真正执行此优化。但是像这样的事情是很重要的,我们应该意识到这一点。
最后,让我们讨论为什么这两种解决方案都应该同样快,以及速度是否有实际差异。
Vec<T>的数据部分与[T]具有完全相同的内存布局:只是内存中有许多连续的T。超级简单。这也意味着两者都表现出相同的缓存行为(具体来说,非常支持缓存)。
唯一的区别是,Vec可能比元素具有更大的容量。因此,Vec本身存储了(pointer, length, capacity)。这不仅仅是一个简单的(装箱的)切片(存储(pointer, length))。装箱数组不需要存储长度,因为它已经在类型中了,所以它只是一个简单的指针。当你有数以百万计的元素时,不管我们是否储存一个、两个或三个单词,都不是很重要。
访问一个元素对所有三个元素都是一样的:我们首先进行边界检查,然后通过base_pointer + size_of::<T>() * index计算目标指针。但是需要注意的是,将其长度存储在类型中的数组意味着优化器可以更容易地删除边界检查!这可能是一个真正的优势。
但是,边界检查通常已经由智能优化器删除。在我上面发布的基准代码中,程序集中没有边界检查。因此,虽然通过删除边界检查,装箱数组可能会更快一些,(a)这将是一个小的性能差异,(b)很难有很多情况下,绑定检查被删除的数组,而不是Vec/片。
发布于 2019-06-03 11:25:35
如果您真的想要一个堆分配数组,即Box<[i32; LENGTH]>,那么您可以使用:
fn main() {
const LENGTH: usize = 10_000_000;
let mut a = {
let mut v: Vec<i32> = Vec::with_capacity(LENGTH);
// Explicitly set length which is safe since the allocation is
// sized correctly.
unsafe { v.set_len(LENGTH); };
// While not required for this particular example, in general
// we want to initialize elements to a known value.
let mut slice = v.into_boxed_slice();
for i in &mut slice[..] {
*i = 0;
}
let raw_slice = Box::into_raw(slice);
// Using `from_raw` is safe as long as the pointer is
// retrieved using `into_raw`.
unsafe {
Box::from_raw(raw_slice as *mut [i32; LENGTH])
}
};
// This is the micro benchmark from the question.
for j in 0..LENGTH {
for i in 0..LENGTH {
a[i] = j as i32;
}
}
}它不会比使用向量更快,因为Rust甚至在数组上进行边界检查,但是它有一个更小的接口,在软件设计方面可能是有意义的。
https://stackoverflow.com/questions/53691012
复制相似问题