Solución:
Resumen: su punto de referencia es defectuoso; solo usa un Vec
(como se describe aquí), posiblemente con into_boxed_slice
, ya que es increíblemente improbable que sea más lento que una matriz asignada al montón.
Desafortunadamente, sus puntos de referencia son defectuosos. En primer lugar, probablemente no compiló con optimizaciones (--release
para carga, -O
para rustc). Porque si lo hubiera hecho, el compilador de Rust habría eliminado todo su código. Vea el montaje aquí. ¿Por qué? Debido a que nunca observa el vector / matriz, no hay necesidad de hacer todo ese trabajo en primer lugar.
Además, su punto de referencia no está probando lo que realmente desea probar. Está comparando una matriz asignada por pila con un vector asignado por pila. Deberías comparar el Vec
a una matriz asignada al montón.
Sin embargo, no se sienta mal: escribir puntos de referencia es increíblemente difícil por muchas razones. Solo recuerde: si no sabe mucho sobre cómo escribir puntos de referencia, es mejor que no confíe en sus propios puntos de referencia sin preguntar a los demás primero.
Arreglé su punto de referencia e incluí las tres posibilidades: Vec
, matriz en la pila y matriz en el montón. Puedes encontrar el código completo aquí. Los resultados son:
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)
Sorpresa: la diferencia entre las versiones es mucho menor que la varianza de la medida. Entonces podemos asumir que todos son igualmente rápidos.
Tenga en cuenta que este punto de referencia es todavía bastante mal. Los dos bucles pueden ser reemplazados por un bucle configurando todos los elementos de la matriz en LENGTH - 1
. Al echar un vistazo rápido al ensamblaje (y al tiempo bastante largo de 9 ms), creo que LLVM no es lo suficientemente inteligente como para realizar esta optimización. Pero cosas como esta son importantes y uno debe ser consciente de ello.
Finalmente, analicemos por qué ambas soluciones deberían ser igualmente rápidas y si realmente existen diferencias en la velocidad.
La sección de datos de un Vec<T>
tiene exactamente el mismo diseño de memoria que un [T]
: solo muchos T
s contiguamente en la memoria. Súper simple. Esto también significa que ambos exhiben el mismo comportamiento de almacenamiento en caché (específicamente, siendo muy amigables con el caché).
La única diferencia es que un Vec
puede tener más capacidad que los elementos. Entonces Vec
se almacena (pointer, length, capacity)
. Esa es una palabra más que una simple rebanada (en caja) (que almacena (pointer, length)
). Una matriz en caja no necesita almacenar la longitud, ya que ya está en el tipo, por lo que es solo un puntero simple. Si almacenamos o no una, dos o tres palabras no es realmente importante cuando de todos modos tendrá millones de elementos.
Acceder a un elemento es el mismo para los tres: primero hacemos una verificación de límites y luego calculamos el puntero de destino a través de base_pointer + size_of::<T>() * index
. ¡Pero es importante tener en cuenta que la matriz que almacena su longitud en el tipo significa que el optimizador puede eliminar la verificación de límites más fácilmente! Esto puede ser una ventaja real.
Sin embargo, el optimizador inteligente ya suele eliminar las comprobaciones de límites. En el código de referencia que publiqué anteriormente, no hay controles de límites en el ensamblaje. Entonces, si bien una matriz en caja podría ser un poco más rápida mediante las verificaciones de límites eliminadas, (a) esto será una diferencia de rendimiento menor y (b) es muy poco probable que tenga muchas situaciones en las que se elimine la verificación vinculada para la matriz, pero no para el Vec / slice.
Si realmente desea una matriz asignada al montón, es decir Box<[i32; LENGTH]>
entonces puedes usar:
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;
}
}
}
No será más rápido que usar un vector, ya que Rust verifica los límites incluso en matrices, pero tiene una interfaz más pequeña que podría tener sentido en términos de diseño de software.