Dependencia del rendimiento del código en el contexto de la declaración de variables en JavaScript


Inicialmente, este artículo fue concebido como un pequeño punto de referencia para su propio uso, y en general no fue planeado para ser un artículo, sin embargo, en el proceso de tomar medidas, surgieron algunas características interesantes en la implementación de la arquitectura JavaScript que afectan en gran medida el rendimiento del código final en algunos casos. Sugiero, y usted, familiarizarse con los resultados obtenidos, en el camino también analizando algunos temas relacionados: para bucles, entorno (contexto de ejecución) y bloques.

Al final de mi artículo "Uso de declaraciones de variables let y características de los cierres de JavaScript resultantes", mencioné brevemente la comparación de rendimiento de declaraciones variables let (LexicalDeclaration) y var (VarDeclaredNames) en bucles. A modo de comparación, utilizamos el tiempo de ejecución del manual (sin la ayuda de Array.prototype.sort () ) para ordenar la matriz, uno de los métodos más simples es ordenar por selección, ya que con una longitud de matriz de 100,000 obtuvimos un poco más de 5 mil millones. iteraciones en dos ciclos (externo y anidado), y, este número debería haber permitido una evaluación adecuada al final.

Para var, estaba ordenando la vista:

for (var i = 0, len = arr.length; i < len-1; i++) { var min, mini = i; for (var j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 9.082 . //   Chrome: 10.783 . 

Y para dejar :

 for (let i = 0, len = arr.length; i < len-1; i++) { let min, mini = i; for (let j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 5.261 . //   Chrome: 5.391 . 

Al ver estos números, al parecer, se puede argumentar inequívocamente que los anuncios superan por completo la velocidad de var . Pero, además de esta conclusión, la pregunta permaneció en el aire: ¿qué sucederá si colocamos declaraciones de dejar fuera de bucles?

Pero, antes de hacer esto, debe profundizar en el trabajo del ciclo for , guiado por la especificación actual de ECMAScript 2019 (ECMA-262) :

 13.7.4.7Runtime Semantics: LabelledEvaluation With parameter labelSet. IterationStatement':'for(Expression;Expression;Expression)Statement 1. If the first Expression is present, then a. Let exprRef be the result of evaluating the first Expression. b. Perform ? GetValue(exprRef). 2. Return ? ForBodyEvaluation(the second Expression, the third Expression, Statement, « », labelSet). IterationStatement':'for(varVariableDeclarationList;Expression;Expression)Statement 1. Let varDcl be the result of evaluating VariableDeclarationList. 2. ReturnIfAbrupt(varDcl). 3. Return ? ForBodyEvaluation(the first Expression, the second Expression, Statement, « », labelSet). IterationStatement':'for(LexicalDeclarationExpression;Expression)Statement 1. Let oldEnv be the running execution context's LexicalEnvironment. 2. Let loopEnv be NewDeclarativeEnvironment(oldEnv). 3. Let loopEnvRec be loopEnv's EnvironmentRecord. 4. Let isConst be the result of performing IsConstantDeclaration of LexicalDeclaration. 5. Let boundNames be the BoundNames of LexicalDeclaration. 6. For each element dn of boundNames, do a. If isConst is true, then i. Perform ! loopEnvRec.CreateImmutableBinding(dn, true). b. Else, i. Perform ! loopEnvRec.CreateMutableBinding(dn, false). 7. Set the running execution context's LexicalEnvironment to loopEnv. 8. Let forDcl be the result of evaluating LexicalDeclaration. 9. If forDcl is an abrupt completion, then a. Set the running execution context's LexicalEnvironment to oldEnv. b. Return Completion(forDcl). 10. If isConst is false, let perIterationLets be boundNames; otherwise let perIterationLets be « ». 11. Let bodyResult be ForBodyEvaluation(the first Expression, the second Expression, Statement, perIterationLets, labelSet). 12. Set the running execution context's LexicalEnvironment to oldEnv. 13. Return Completion(bodyResult). 
nota: los dos puntos después de IterationStatements, en la fuente no están enmarcados por apóstrofes, se agregan aquí para que no haya un formato automático que estropee la legibilidad del texto.

Aquí, como vemos, hay tres opciones para llamar y un trabajo adicional del ciclo for :
  • con la declaración for (Expression; Expression; Expression)
    ForBodyEvaluation (la segunda expresión, la tercera expresión, instrucción, "", labelSet) .
  • con la declaración for (varVariableDeclarationList; Expression; Expression)
    ForBodyEvaluation (la primera expresión, la segunda expresión, instrucción, "", labelSet).
  • en la declaración for (LexicalDeclarationExpression; Expression)
    ForBodyEvaluation (la primera expresión, la segunda expresión, declaración, perIterationLets, labelSet)

En la última, tercera variante, a diferencia de las dos primeras, el cuarto parámetro no está vacío, perIterationLets , estas son en realidad las mismas declaraciones de let en el primer parámetro pasado al bucle for . Se especifican en el párrafo 10:
- Si isConst es falso , deje que perIterationLets se limite a los nombres; de lo contrario, perIterationLets sea "".
Si se pasó una constante a for , pero no una variable, el parámetro perIterationLets queda vacío.

Además, en la tercera opción, es necesario prestar atención al párrafo 2:
- Deje que loopEnv sea NewDeclarativeEnvironment (oldEnv).

 8.1.2.2NewDeclarativeEnvironment ( E ) When the abstract operation NewDeclarativeEnvironment is called with a Lexical Environment as argument E the following steps are performed: 1. Let env be a new Lexical Environment. 2. Let envRec be a new declarative Environment Record containing no bindings. 3. Set env's EnvironmentRecord to envRec. 4. Set the outer lexical environment reference of env to E. 5. Return env. 

Aquí, como el parámetro E , se toma el entorno desde el que se llamó al bucle for (global, cualquier función, etc.) y se crea un nuevo entorno para ejecutar el bucle for con referencia al entorno externo que lo creó (punto 4). Estamos interesados ​​en este hecho debido al hecho de que el entorno es un contexto de ejecución.

Y recordamos que las declaraciones de variables let y const están ligadas contextualmente al bloque en el que se declaran.

 13.2.14Runtime Semantics: BlockDeclarationInstantiation ( code, env ) Note When a Block or CaseBlock is evaluated a new declarative Environment Record is created and bindings for each block scoped variable, constant, function, or class declared in the block are instantiated in the Environment Record. BlockDeclarationInstantiation is performed as follows using arguments code and env. code is the Parse Node corresponding to the body of the block. env is the Lexical Environment in which bindings are to be created. 1. Let envRec be env's EnvironmentRecord. 2. Assert: envRec is a declarative Environment Record. 3. Let declarations be the LexicallyScopedDeclarations of code. 4. For each element d in declarations, do a. For each element dn of the BoundNames of d, do i. If IsConstantDeclaration of d is true, then 1. Perform ! envRec.CreateImmutableBinding(dn, true). ii. Else, 1. Perform ! envRec.CreateMutableBinding(dn, false). b. If d is a FunctionDeclaration, a GeneratorDeclaration, an AsyncFunctionDeclaration, or an AsyncGeneratorDeclaration, then i. Let fn be the sole element of the BoundNames of d. ii. Let fo be the result of performing InstantiateFunctionObject for d with argument env. iii. Perform envRec.InitializeBinding(fn, fo). 

nota: dado que en las dos primeras variantes de llamar al bucle for no hubo tales declaraciones, no hubo necesidad de crear un nuevo entorno para ellas.

Vamos más allá y consideramos qué es ForBodyEvaluation :

 13.7.4.8Runtime Semantics: ForBodyEvaluation ( test, increment, stmt, perIterationBindings, labelSet ) The abstract operation ForBodyEvaluation with arguments test, increment, stmt, perIterationBindings, and labelSet is performed as follows: 1. Let V be undefined. 2. Perform ? CreatePerIterationEnvironment(perIterationBindings). 3. Repeat, a. If test is not [empty], then i. Let testRef be the result of evaluating test. ii. Let testValue be ? GetValue(testRef). iii. If ToBoolean(testValue) is false, return NormalCompletion(V). b. Let result be the result of evaluating stmt. c. If LoopContinues(result, labelSet) is false, return Completion(UpdateEmpty(result, V)). d. If result.[[Value]] is not empty, set V to result.[[Value]]. e. Perform ? CreatePerIterationEnvironment(perIterationBindings). f. If increment is not [empty], then i. Let incRef be the result of evaluating increment. ii. Perform ? GetValue(incRef). 

A lo que primero debe prestar atención:
  • Descripción de los parámetros entrantes:
    • prueba : la expresión verificó la verdad antes de la siguiente iteración del cuerpo del bucle (por ejemplo: i <len );
    • incremento : expresión evaluada al comienzo de cada nueva iteración (excepto la primera) (por ejemplo: i ++ );
    • stmt : cuerpo de bucle
    • perIterationBindings : variables declaradas con let en el primer parámetro (por ejemplo: let i = 0 || let i || let i, j );
    • labelSet : etiqueta del bucle;
  • punto 2: aquí, si se pasa el parámetro no vacío perIterationBindings , se crea un segundo entorno para realizar el paso inicial del bucle;
  • párrafo 3.a: verificar una condición dada para continuar la ejecución del ciclo;
  • cláusula 3.b: ejecución del cuerpo del ciclo;
  • punto 3.e: creando un nuevo entorno.

Bueno, y, directamente, el algoritmo para crear entornos internos del ciclo for :

 13.7.4.9Runtime Semantics: CreatePerIterationEnvironment ( perIterationBindings ) 1. The abstract operation CreatePerIterationEnvironment with argument perIterationBindings is performed as follows: 1. If perIterationBindings has any elements, then a. Let lastIterationEnv be the running execution context's LexicalEnvironment. b. Let lastIterationEnvRec be lastIterationEnv's EnvironmentRecord. c. Let outer be lastIterationEnv's outer environment reference. d. Assert: outer is not null. e. Let thisIterationEnv be NewDeclarativeEnvironment(outer). f. Let thisIterationEnvRec be thisIterationEnv's EnvironmentRecord. g. For each element bn of perIterationBindings, do i. Perform ! thisIterationEnvRec.CreateMutableBinding(bn, false). ii. Let lastValue be ? lastIterationEnvRec.GetBindingValue(bn, true). iii. Perform thisIterationEnvRec.InitializeBinding(bn, lastValue). h. Set the running execution context's LexicalEnvironment to thisIterationEnv. 2. Return undefined. 

Como podemos ver, el primer párrafo verifica la presencia de cualquier elemento en el parámetro pasado, y el párrafo 1 solo se realiza si hay anuncios de let . Todos los entornos nuevos se crean con referencia al mismo contexto externo y toman los últimos valores de la iteración anterior (entorno de trabajo anterior) como nuevos enlaces de variables let .

Como ejemplo, considere una expresión similar:

 let arr = []; for (let i = 0; i < 3; i++) { arr.push(i); } console.log(arr); // Array(3) [ 0, 1, 2 ] 

Y así es como se puede descomponer sin usar for (con una cierta cantidad de convencionalidad):

 let arr = []; //    { let i = 0; //     for } //   ,   { let i = 0; //    i    if (i < 3) arr.push(i); } //    { let i = 0; //    i    i++; if (i < 3) arr.push(i); } //    { let i = 1; //    i    i++; if (i < 3) arr.push(i); } //    { let i = 2; //    i    i++; if (i < 3) arr.push(i); } console.log(arr); // Array(3) [ 0, 1, 2 ] 

De hecho, llegamos a la conclusión de que para cada contexto, y aquí tenemos cinco de ellos, hacemos nuevos enlaces para las variables let declaradas como el primer parámetro en for (importante: esto no se aplica a las declaraciones let directamente en el cuerpo del bucle).

Así es como, por ejemplo, este bucle se verá cuando se use var cuando no haya enlaces adicionales:

 let arr2 = []; var i = 0; if (i < 3) arr.push(i); i++; if (i < 3) arr.push(i); i++; if (i < 3) arr.push(i); i++; if (i < 3) arr.push(i); console.log(arr); // Array(3) [ 0, 1, 2 ] 

Y podemos llegar a una conclusión aparentemente lógica de que si durante la ejecución de nuestros bucles no hay necesidad de crear enlaces separados para cada iteración ( más sobre situaciones en las que esto, por el contrario, puede tener sentido ), deberíamos hacer la declaración de variables incrementales antes con un bucle for , que debería salvarnos de crear y eliminar una gran cantidad de contextos y, en teoría, mejorar el rendimiento.

Tratemos de hacer esto, usando la misma clasificación de una matriz de 100,000 elementos como ejemplo, y en aras de la belleza, también hacemos la definición de todas las demás variables antes para :

 let i, j, min, mini, len = arr.length; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 34.246 . //   Chrome: 10.803 . 

Resultado inesperado ... Justo lo contrario de lo que se esperaba, para ser precisos. La reducción de Firefox en esta prueba es particularmente sorprendente.

Ok Esto no funcionó, regresemos la declaración de las variables i y j a los parámetros de los ciclos correspondientes:

 let min, mini, len = arr.length; for (let i = 0; i < len-1; i++) { mini = i; for (let j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 6.575 . //   Chrome: 6.749 . 

Hm. Parece que, técnicamente, la única diferencia entre el último ejemplo y el ejemplo al comienzo del artículo son las declaraciones hechas de las variables min, mini y len fuera del ciclo for , y aunque la diferencia sigue siendo contextual, no es muy interesante para nosotros ahora, y además Eliminamos la necesidad de declarar estas variables 99.999 veces en el cuerpo del ciclo del nivel superior, lo que, en teoría, debería aumentar la productividad en lugar de disminuirla en más de un segundo.

Es decir, de alguna manera, trabajar con variables declaradas en el parámetro o cuerpo del bucle for ocurre mucho más rápido que fuera de él.

Pero, no parecíamos ver ninguna instrucción "turbo" en la especificación del bucle for que pudiera llevarnos a tal idea. Por lo tanto, no son los detalles específicos del trabajo del ciclo for específicamente, sino algo más ... Por ejemplo, las características de las declaraciones let : ¿cuál es la característica principal que distingue let de var ? ¡Contexto de ejecución de bloque! Y en nuestros dos últimos ejemplos, utilizamos anuncios fuera del bloque. Pero, ¿qué pasa si en lugar de mover estas declaraciones de nuevo a , solo seleccionamos un bloque separado para ellas?

 { let i, j, min, mini, len = arr.length; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } } //   Firefox: 5.262 . //   Chrome: 5.405 . 

Voila! Resulta que la trampa fue que los anuncios tuvieron lugar en un contexto global, y tan pronto como les asignamos un bloque separado, todos los problemas desaparecieron allí mismo.

Y aquí sería bueno recordar otra forma, un poco mal merecida, de declarar variables: la var .

En el ejemplo al comienzo del artículo, el tiempo de clasificación usando var mostró un resultado extremadamente deplorable, en relación con let . Pero, si observa más de cerca este ejemplo, puede encontrar que, dado que var no tenía enlaces de bloques variables, el contexto real de las variables era global. Y nosotros, en el ejemplo de let , ya hemos descubierto cómo esto puede afectar el rendimiento (y, lo cual es típico, cuando se usa let , la reducción de velocidad resultó ser más fuerte que en el caso de var , especialmente en Firefox ). Por lo tanto, para ser justos, ejecutaremos un ejemplo con var creando un nuevo contexto para las variables:

 function test() { var i, j, min, mini, len = arr.length; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } } test(); //   Firefox: 5.255 . //   Chrome: 5.411 . 

Y obtuvimos el resultado casi idéntico a lo que era al usar let .

Finalmente, verifiquemos si la desaceleración ocurre leyendo la variable global sin cambiar su valor.

dejar

 let len = arr.length; for (let i = 0; i < len-1; i++) { let min, mini = i; for (let j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 5.262 . //   Chrome: 5.391 . 

var

 var len = arr.length; function test() { var i, j, min, mini; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } } test(); //   Firefox: 5.258 . //   Chrome: 5.439 . 

Los resultados indican que leer la variable global no afectó el tiempo de ejecución.

Para resumir


  1. Cambiar las variables globales es mucho más lento que cambiar las locales. Teniendo esto en cuenta, es posible optimizar el código en situaciones apropiadas mediante la creación de un bloque o función por separado, incluso para declarar variables, en lugar de ejecutar parte del código en un contexto global. Sí, en casi cualquier libro de texto puede encontrar recomendaciones para hacer la menor cantidad posible de enlaces globales, pero generalmente solo se indica una obstrucción del espacio de nombres global como una razón, y no una palabra sobre posibles problemas de rendimiento.
  2. A pesar del hecho de que la ejecución de bucles con una declaración let en el primer parámetro for crea una gran cantidad de entornos, esto casi no tiene efecto en el rendimiento, a diferencia de las situaciones en las que tomamos tales declaraciones fuera del bloque. Sin embargo, no se debe excluir la posibilidad de la existencia de situaciones exóticas cuando este factor afectará la productividad de manera más significativa.
  3. El rendimiento de las variables var todavía no es inferior al de las variables let , sin embargo, no las excede (nuevamente, en el caso general), lo que nos lleva a la siguiente conclusión de que no hay razón para usar declaraciones var excepto para fines de compatibilidad. Sin embargo, si es necesario manipular variables globales cambiando sus valores, la variante con var en términos de rendimiento será preferible (al menos por el momento, si, en particular, se supone que el script también se puede ejecutar en el motor Gecko).

Referencias


ECMAScript 2019 (ECMA-262)
Uso de declaraciones de variables y características de los cierres resultantes en JavaScript

Source: https://habr.com/ru/post/469523/


All Articles