¿Por qué array.push a veces es más rápido que array[n] = valor?

Resuelto KooiInc asked hace 15 años • 5 respuestas

Como resultado secundario de probar algún código, escribí una pequeña función para comparar la velocidad de uso del array.push(value)método frente al direccionamiento directo array[n] = value. Para mi sorpresa, el método push a menudo resultó ser más rápido, especialmente en Firefox y, a veces, en Chrome. Sólo por curiosidad: ¿alguien tiene una explicación para esto?

Aquí está la prueba ( nota : reescrita el 10/02/2023)

const arrLen = 10_000;
const x = [...Array(10)].map( (_, i) => testArr(arrLen, i));
console.log(`Array length: ${arrLen}\n--------\n${x.join(`\n`)}`);

function testArr(n, action) {
  let arr = [];
  const perfStart = performance.now();
  const methods = 
    ` for (; n; n--) arr.push(n)
      for (; i < n; i += 1) { arr[i] = i; }
      for (; i < n; i += 1) arr.push(i)
      while (--n) arr.push(n)
      while (i++ < n) arr.push(n)
      while (--n) arr.splice(0, 0, n)
      while (--n) arr.unshift(n)
      while (++i < n) arr.unshift(i)
      while (--n) arr.splice(n - 1, 0, n)
      while (n--) arr[n] = n`.split(`\n`).map(v => v.trim());
  const report =  i => `${methods[i]}: ${
    (performance.now() - perfStart).toFixed(2)} milliseconds`;
  let i = 0;
  
  switch (action) {
    case 0: for (; n; n--) arr.push(n)
    case 1: for (; i < n; i += 1) { arr[i] = i; } break;
    case 2: for (let i = 0; i < n; i += 1) arr.push(i); break;
    case 3: while (--n) arr.push(n); break;
    case 4: while (i++ < n) arr.push(n); break;
    case 5: while (--n) arr.splice(0, 0, n); break;
    case 6: while (--n) arr.unshift(n)
    case 7: while (++i < n) arr.unshift(i); break;
    case 8: while (--n) arr.splice(n - 1, 0, n); break;
    default: while (n--) arr[n] = n;
  }
  
  return report(action);
}
.as-console-wrapper {
    max-height: 100% !important;
}
Expandir fragmento

KooiInc avatar Mar 05 '09 16:03 KooiInc
Aceptado

Entran en juego todo tipo de factores, la mayoría de las implementaciones de JS utilizan una matriz plana que se convierte en almacenamiento escaso si es necesario más adelante.

Básicamente, la decisión de volverse disperso es una heurística basada en qué elementos se están configurando y cuánto espacio se desperdiciaría para permanecer plano.

En su caso, está configurando primero el último elemento, lo que significa que el motor JS verá una matriz que debe tener una longitud de nun solo elemento. Si nes lo suficientemente grande, esto inmediatamente convertirá la matriz en una matriz dispersa; en la mayoría de los motores, esto significa que todas las inserciones posteriores tomarán el caso de matriz dispersa lenta.

Debería agregar una prueba adicional en la que llene la matriz desde el índice 0 hasta el índice n-1; debería ser mucho, mucho más rápido.

En respuesta a @Christoph y por deseo de posponer las cosas, aquí hay una descripción de cómo se implementan (generalmente) las matrices en JS: los detalles varían de un motor JS a otro, pero el principio general es el mismo.

Todos Objectlos JS (por lo tanto, no cadenas, números, verdadero, falso undefinedo null) heredan de un tipo de objeto base; la implementación exacta varía, podría ser herencia de C++ o manualmente en C (hay beneficios al hacerlo de cualquier manera). ) - el tipo de objeto base define los métodos de acceso a la propiedad predeterminados, por ejemplo.

interface Object {
    put(propertyName, value)
    get(propertyName)
private:
    map properties; // a map (tree, hash table, whatever) from propertyName to value
}

Este tipo de Objeto maneja toda la lógica de acceso a propiedades estándar, la cadena de prototipos, etc. Luego, la implementación de Array se convierte en

interface Array : Object {
    override put(propertyName, value)
    override get(propertyName)
private:
    map sparseStorage; // a map between integer indices and values
    value[] flatStorage; // basically a native array of values with a 1:1
                         // correspondance between JS index and storage index
    value length; // The `length` of the js array
}

Ahora, cuando crea una matriz en JS, el motor crea algo parecido a la estructura de datos anterior. Cuando inserta un objeto en la instancia de Array, el método put de Array verifica si el nombre de la propiedad es un número entero (o se puede convertir en un número entero, por ejemplo, "121", "2341", etc.) entre 0 y 2^32. -1 (o posiblemente 2^31-1, lo olvido exactamente). Si no es así, entonces el método put se reenvía a la implementación del objeto base y se realiza la lógica [[Put]] estándar. De lo contrario, el valor se coloca en el propio almacenamiento de la matriz; si los datos son lo suficientemente compactos, entonces el motor utilizará el almacenamiento de matriz plana, en cuyo caso la inserción (y recuperación) es solo una operación de indexación de matriz estándar; de lo contrario, el motor convertirá la matriz. para almacenamiento escaso y poner/obtener uso de un mapa para ir desde el nombre de propiedad hasta la ubicación del valor.

Sinceramente, no estoy seguro de si algún motor JS actualmente convierte de almacenamiento disperso a plano después de que se produce esa conversión.

De todos modos, esa es una descripción general de nivel bastante alto de lo que sucede y omite algunos de los detalles más repugnantes, pero ese es el patrón de implementación general. Los detalles de cómo se envía el almacenamiento adicional y cómo se envían/obtienen difieren de un motor a otro, pero esto es lo más claro que puedo describir el diseño/implementación.

Un punto adicional menor, aunque la especificación ES se refiere a propertyNameuna cadena, los motores JS también tienden a especializarse en búsquedas de números enteros, por lo que someObject[someInteger]no convertirán el número entero en una cadena si está mirando un objeto que tiene propiedades de número entero, por ejemplo. Tipos de matriz, cadena y DOM ( NodeLists, etc.).

olliej avatar Mar 05 '2009 10:03 olliej

Estos son los resultados que obtengo con tu prueba.

en Safari:

  • Array.push(n) 1.000.000 valores: 0,124 segundos
  • Matriz[n .. 0] = valor (descendente) 1.000.000 valores: 3,697 seg
  • Matriz[0 .. n] = valor (ascendente) 1.000.000 valores: 0,073 seg

en Firefox:

  • Array.push(n) 1.000.000 valores: 0,075 segundos
  • Matriz[n .. 0] = valor (descendente) 1.000.000 valores: 1,193 seg
  • Matriz[0 .. n] = valor (ascendente) 1.000.000 valores: 0,055 seg

en IE7:

  • Array.push(n) 1.000.000 valores: 2,828 segundos
  • Matriz[n .. 0] = valor (descendente) 1.000.000 valores: 1,141 seg
  • Matriz[0 .. n] = valor (ascendente) 1.000.000 valores: 7,984 seg

Según su prueba, el método push parece ser mejor en IE7 (gran diferencia), y dado que en otros navegadores la diferencia es pequeña, parece ser el método push realmente la mejor manera de agregar elementos a una matriz.

Pero creé otro script de prueba simple para verificar qué método es rápido para agregar valores a una matriz, los resultados realmente me sorprendieron, usar Array.length parece ser mucho más rápido en comparación con usar Array.push , así que realmente no sé qué decir o pensar más, no tengo ni idea.

Por cierto: en mi IE7 el script se detiene y los navegadores me preguntan si quiero dejarlo continuar (ya conoces el mensaje típico de IE que dice: "¿Dejar de ejecutar este script? ...") Recomendaría reducir un poco los bucles. .

Marco Demaio avatar Mar 02 '2010 19:03 Marco Demaio