¿Cuál es el rendimiento de los objetos / matrices en JavaScript? (específicamente para Google V8)


El rendimiento asociado con Arrays y Objetos en JavaScript (especialmente Google V8) sería muy interesante de documentar. No encuentro ningún artículo completo sobre este tema en Internet.

Entiendo que algunos objetos usan clases como su estructura de datos subyacente. Si hay muchas propiedades, a veces se trata como una tabla hash?

También entiendo que las matrices a veces se tratan como matrices de C++ (es decir, indexación aleatoria rápida, eliminación lenta y redimensionar). Y, otras veces, se tratan más como Objetos (indexación rápida, inserción/eliminación rápida, más memoria). Y, tal vez a veces se almacenan como listas vinculadas (es decir, indexación aleatoria lenta, eliminación/inserción rápida al principio/final)

¿Cuál es el rendimiento preciso de las recuperaciones y manipulaciones de matrices/objetos en JavaScript? (específicamente para Google V8)

Más específicamente, cuál es el impacto en el rendimiento de:

  • Añadiendo una propiedad a un Objeto
  • Eliminar una propiedad de un objeto
  • Indexar una propiedad en un objeto
  • Agregar un elemento a un Array
  • Eliminar un elemento de una matriz
  • Indexar un elemento en una matriz
  • Llamando a Array.pop ()
  • Llamando a Array.push ()
  • Llamando a Array.shift ()
  • Llamando a Array.unshift ()
  • Llamando a Array.slice ()

Cualquier artículo o enlace para más detalles sería apreciado, también. :)

EDIT: Realmente me pregunto cómo funcionan los arrays y objetos de JavaScript bajo el capó. Además, ¿en qué contexto "sabe" el motor V8 "cambiar" a otra estructura de datos?

Por ejemplo, supongamos que creo una matriz con...

var arr = [];
arr[10000000] = 20;
arr.push(21);

¿Qué está pasando realmente aquí?

O... ¿qué hay de esto?..???

var arr = [];
//Add lots of items
for(var i = 0; i < 1000000; i++)
    arr[i] = Math.random();
//Now I use it like a queue...
for(var i = 0; i < arr.length; i++)
{
    var item = arr[i].shift();
    //Do something with item...
}

Para los arrays convencionales, el rendimiento sería terrible; mientras que, si se utiliza una lista de enlaces... no tan mal.

Author: BMiner, 2011-12-08

4 answers

ACTUALIZACIÓN: Tenga en cuenta que JSPref está actualmente fuera de servicio

(he guardado una copia del caso de prueba, y actualizaré la respuesta una vez que se arregle JSPref / se encuentre un sucesor)


Hmm... tal vez una exageración por la respuesta... pero creé un conjunto de pruebas, precisamente para explorar estos problemas (y más) (archived copy ).

Y en ese sentido, puede ver los problemas de rendimiento en este probador de casos de prueba 50+ (tomará mucho tiempo).

También como su nombre sugiere, explora el uso de la naturaleza de lista enlazada nativa de la estructura DOM.

(Actualmente abajo, reconstruido en progreso) Más detalles en mi blog con respecto a esto.

El resumen es el siguiente

  • La matriz V8 es Rápida, MUY RÁPIDA
  • Array push / pop / shift es ~aproximadamente 20x+ más rápido que cualquier objeto equivalente.
  • Sorprendentemente Array.shift() es rápido ~aproximadamente 6 veces más lento que una matriz pop, pero es aproximadamente 100 veces más rápido que la eliminación de un atributo de objeto.
  • Sorprendentemente, Array.push( data ); es más rápido que Array[nextIndex] = data por casi 20 (matriz dinámica) a 10 (matriz fija) veces.
  • Array.unshift(data) es más lento como se esperaba, y es aproximadamente 5 veces más lento que una adición de una nueva propiedad.
  • Anular el valor array[index] = null es más rápido que eliminarlo delete array[index] (indefinido) en una matriz aproximadamente 4x++ más rápido.
  • Sorprendentemente Anular un valor en un objeto es obj[attr] = null ~aproximadamente 2 veces más lento que simplemente eliminar atributo delete obj[attr]
  • Como era de esperar, mid array Array.splice(index,0,data) es lento, muy lento.
  • Sorprendentemente, Array.splice(index,1,data) ha sido optimizado (sin cambio de longitud) y es 100 veces más rápido que solo splice Array.splice(index,0,data)
  • como era de esperar, el divLinkedList es inferior a un array en todos los sectores, excepto dll.splice(index,1) removal (Donde rompió el sistema de prueba).
  • LA MAYOR SORPRESA de todo [como jjrv señaló], las escrituras de matriz V8 son ligeramente más rápidas que las lecturas de V8 = O

Nota: Estas métricas se aplican solo a grandes matrices/objetos que v8 no "optimiza completamente". Puede haber casos de rendimiento optimizado muy aislados para un tamaño de matriz / objeto menor que un tamaño arbitrario (24?). Más detalles se pueden ver ampliamente a través de varios videos de Google IO.

Nota 2: Estos maravillosos resultados de rendimiento no se comparten entre los navegadores, especialmente *cough* IE. También la prueba es enorme, por lo tanto todavía para analizar completamente y evalúe los resultados: por favor edítelo en =)

Nota actualizada (diciembre 2012): Los representantes de Google tienen videos en youtubes que describen el funcionamiento interno de Chrome en sí (como cuando cambia de una matriz linkedlist a una matriz fija, etc), y cómo optimizarlos. Ver GDC 2012: From Console to Chrome para más.

Nota actualizada (feb 2013): Thx @badunk, por proporcionar el enlace de video en el punto exacto

Nota actualizada (junio 2016): Thx @Benedikt, con respecto a la diferencia de rendimiento de empuje de matrices en matrices fijas / dinámicas.

 271
Author: PicoCreator,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-06-04 09:30:15

A un nivel básico que se mantiene dentro de los dominios de JavaScript, las propiedades de los objetos son entidades mucho más complejas. Puede crear propiedades con setters / getters, con diferentes enumerabilidad, escritura y configurabilidad. Un elemento en una matriz no se puede personalizar de esta manera: existe o no. En el nivel del motor subyacente, esto permite mucha más optimización en términos de organización de la memoria que representa la estructura.

En términos de identificación una matriz de un objeto (diccionario), los motores JS siempre han hecho líneas explícitas entre los dos. Es por eso que hay una multitud de artículos sobre métodos para tratar de hacer un objeto similar a una matriz semi-falsa que se comporta como uno pero permite otra funcionalidad. La razón por la que esta separación incluso existe es porque los motores JS almacenan los dos de manera diferente.

Las propiedades se pueden almacenar en un objeto array, pero esto simplemente demuestra cómo JavaScript insiste en hacer que todo sea un objeto. Los valores indexados en una matriz se almacenan de manera diferente a cualquier propiedad que decida establecer en el objeto de matriz que representa los datos de matriz subyacentes.

Cada vez que usted está utilizando un objeto de matriz de fiar y el uso de uno de los métodos estándar de manipulación de esa matriz que vas a estar golpeando los datos de la matriz subyacente. En V8 específicamente, estos son esencialmente los mismos que una matriz de C++ por lo que esas reglas se aplicarán. Si por alguna razón está trabajando con una matriz que el motor no puede determinar con confianza es una matriz, entonces estás en terreno mucho más inestable. Sin embargo, con las versiones recientes de V8 hay más espacio para trabajar. Por ejemplo, es posible crear una clase que tenga matriz.prototype como su prototype y aún así obtener un acceso eficiente a los diversos métodos de manipulación de matrices nativas. Pero este es un cambio reciente.

Los enlaces específicos a los cambios recientes en la manipulación de matrices pueden ser útiles aquí:

Como un poco extra, aquí está el Array Pop y el Array Push directamente desde la fuente de V8, ambos implementados en el propio JS:

function ArrayPop() {
  if (IS_NULL_OR_UNDEFINED(this) && !IS_UNDETECTABLE(this)) {
    throw MakeTypeError("called_on_null_or_undefined",
                        ["Array.prototype.pop"]);
  }

  var n = TO_UINT32(this.length);
  if (n == 0) {
    this.length = n;
    return;
  }
  n--;
  var value = this[n];
  this.length = n;
  delete this[n];
  return value;
}


function ArrayPush() {
  if (IS_NULL_OR_UNDEFINED(this) && !IS_UNDETECTABLE(this)) {
    throw MakeTypeError("called_on_null_or_undefined",
                        ["Array.prototype.push"]);
  }

  var n = TO_UINT32(this.length);
  var m = %_ArgumentsLength();
  for (var i = 0; i < m; i++) {
    this[i+n] = %_Arguments(i);
  }
  this.length = n + m;
  return this.length;
}
 5
Author: Peter O.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-12-17 16:03:55

Mientras se ejecuta bajo nodo.js 0.10 (construido en v8) Estaba viendo el uso de la CPU que parecía excesivo para la carga de trabajo. Rastreé un problema de rendimiento a una función que estaba comprobando la existencia de una cadena en una matriz. Así que hice algunas pruebas.

  • cargado 90,822 hosts
  • la configuración de carga tomó 0.087 segundos (matriz)
  • la configuración de carga tomó 0.152 segundos (objeto)

Cargar entradas 91k en una matriz (con validate & push) es más rápido que configurar obj[clave]=valor.

En la siguiente prueba, busqué cada nombre de host en la lista una vez (iteraciones de 91k, para promediar el tiempo de búsqueda):

  • la configuración de búsqueda tomó 87.56 segundos (array)
  • la configuración de búsqueda tomó 0.21 segundos (objeto)

La aplicación aquí es Haraka (un servidor SMTP) y carga la host_list una vez al inicio (y después de los cambios) y posteriormente realiza esta búsqueda millones de veces durante la operación. Cambiar a un objeto fue un enorme rendimiento ganar.

 0
Author: Matt Simerson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-06-28 19:28:36

Me gustaría complementar las respuestas existentes con una investigación a la pregunta de cómo se comportan las implementaciones con respecto a los arrays en crecimiento: Si los implementan de la manera "habitual", uno vería muchos empujones rápidos con empujones lentos raros e intercalados en el momento en que la implementación copia la representación interna del array de un búfer a uno más grande.

Puedes ver este efecto muy bien, esto es de Chrome:

16: 4ms
40: 8ms 2.5
76: 20ms 1.9
130: 31ms 1.7105263157894737
211: 14ms 1.623076923076923
332: 55ms 1.5734597156398105
514: 44ms 1.5481927710843373
787: 61ms 1.5311284046692606
1196: 138ms 1.5196950444726811
1810: 139ms 1.5133779264214047
2731: 299ms 1.5088397790055248
4112: 341ms 1.5056755767118273
6184: 681ms 1.5038910505836576
9292: 1324ms 1.5025873221216042

Aunque cada push está perfilado, la salida contiene solo aquellos que toman tiempo por encima de un cierto umbral. Para cada prueba personalizé el umbral para excluir todos los empujes que parecen representar los empujes rápidos.

Así que el primer número representa qué elemento se ha insertado (la primera línea es para el elemento 17), el segundo es cuánto tiempo tomó (para muchos arrays el punto de referencia se realiza en paralelo), y el último valor es la división del primer número por el de la línea anterior.

Todas las líneas que tienen menos de 2ms tiempo de ejecución se excluyen para Chrome.

Se puede ver que Chrome aumenta el tamaño de la matriz en potencias de 1.5, además de algunos desplazamientos para dar cuenta de pequeños arreglos.

Para Firefox, es una potencia de dos:

126: 284ms
254: 65ms 2.015873015873016
510: 28ms 2.0078740157480315
1022: 58ms 2.003921568627451
2046: 89ms 2.0019569471624266
4094: 191ms 2.0009775171065494
8190: 364ms 2.0004885197850513

Tuve que poner el umbral hasta un poco en Firefox, es por eso que empezamos en #126.

Con IE, obtenemos una mezcla:

256: 11ms 256
512: 26ms 2
1024: 77ms 2
1708: 113ms 1.66796875
2848: 154ms 1.6674473067915691
4748: 423ms 1.6671348314606742
7916: 944ms 1.6672283066554338

Es una potencia de dos al principio y luego se mueve a potencias de cinco tercios.

Así que todas las implementaciones comunes utilice la forma "normal" para los arrays (en lugar de volverse loco con cuerdas, por ejemplo).

Aquí está el código de referencia y aquí está el violín en el que está.

var arrayCount = 10000;

var dynamicArrays = [];

for(var j=0;j<arrayCount;j++)
    dynamicArrays[j] = [];

var lastLongI = 1;

for(var i=0;i<10000;i++)
{
    var before = Date.now();
    for(var j=0;j<arrayCount;j++)
        dynamicArrays[j][i] = i;
    var span = Date.now() - before;
    if (span > 10)
    {
      console.log(i + ": " + span + "ms" + " " + (i / lastLongI));
      lastLongI = i;
    }
}
 0
Author: John,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-09-16 13:54:28