Inicialmente, este artigo foi concebido como uma pequena referência para seu próprio uso e, em geral, não foi planejado para ser um artigo, no entanto, durante o processo de medição, surgiram alguns recursos interessantes na implementação da arquitetura
JavaScript , que afetam fortemente o desempenho do código final em alguns casos. Sugiro que você se familiarize com os resultados obtidos, examinando incidentalmente também alguns tópicos relacionados: loops, ambiente (contexto de execução) e blocos.
No final do meu artigo
“Usando declarações de variáveis let e recursos dos fechamentos resultantes de JavaScript”, toquei brevemente na comparação de desempenho das declarações de variáveis
let (LexicalDeclaration) e
var (VarDeclaredNames) em loops. Para comparação, usamos o tempo de execução da classificação manual (sem a ajuda de
Array.prototype.sort () ), um dos métodos mais simples é a seleção por seleção, pois com um comprimento de matriz de 100.000, obtivemos pouco mais de 5 bilhões. iterações em dois ciclos (externo e aninhado) e esse valor deve permitir uma estimativa adequada no final.
Para
var, ele estava classificando a visualização:
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; }
E por
deixar :
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; }
Ao ver esses números, ao que parece, pode-se argumentar inequivocamente que
os anúncios ultrapassam completamente a velocidade. Mas, além dessa conclusão, a questão permaneceu no ar: o que acontecerá se colocarmos
as declarações de fora de loops?
Mas, antes de fazer isso, você precisa se aprofundar no trabalho do loop
for , guiado pela especificação atual do
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).
note: dois pontos após IterationStatements, na fonte não são enquadrados por apóstrofos - são adicionados aqui para que não haja formatação automática que estrague a legibilidade do texto.Aqui, como vemos, há três opções para chamar e mais trabalho do loop
for :
- com a instrução for (Expression; Expression; Expression)
ForBodyEvaluation (a segunda Expressão, a terceira Expressão, Instrução, "", labelSet) . - com a instrução for (varVariableDeclarationList; Expression; Expression)
ForBodyEvaluation (a primeira Expressão, a segunda Expressão, Instrução, "", labelSet). - Instrução at for (LexicalDeclarationExpression; Expression)
ForBodyEvaluation (a primeira Expressão, a segunda Expressão, Instrução, perIterationLets, labelSet)
Na última, terceira variante, ao contrário dos dois primeiros, o quarto parâmetro não está vazio -
perIterationLets - essas são, na verdade, as mesmas declarações de declaração no primeiro parâmetro passado para o loop
for . Eles são especificados no parágrafo 10:
- Se isConst for false , permita que perIterationLets seja boundNames; caso contrário, deixe perIterationLets ser "".Se uma constante foi passada para
for , mas não uma variável, o parâmetro
perIterationLets ficará vazio.
Além disso, na terceira opção, é necessário prestar atenção ao parágrafo 2:
- Deixe loopEnv ser 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.
Aqui, como o parâmetro
E , o ambiente do qual o loop
for foi chamado (global, qualquer função etc.) é obtido, e um novo ambiente é criado para executar o loop
for com referência ao ambiente externo que o criou (ponto 4). Estamos interessados nesse fato devido ao fato de o ambiente ser um contexto de execução.
E lembramos que as declarações de variáveis
let e
const estão contextualmente ligadas ao bloco em que são declaradas.
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).
note: como nas duas primeiras variantes de chamada do loop
for não havia tais declarações, não havia necessidade de criar um novo ambiente para elas.
Vamos além e consideramos o que
é a 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).
O que você deve primeiro prestar atenção:
- descrição dos parâmetros recebidos:
- teste : expressão verificada quanto à verdade antes da próxima iteração do corpo do loop (por exemplo: i <len );
- incremento : expressão avaliada no início de cada nova iteração (exceto a primeira) (por exemplo: i ++ );
- stmt : corpo do laço
- perIterationBindings : variáveis declaradas com let no primeiro para o parâmetro (por exemplo: let i = 0 || let i || let i, j );
- labelSet : rótulo do loop;
- ponto 2: aqui, se o parâmetro não vazio perIterationBindings for passado , um segundo ambiente será criado para executar a passagem inicial do loop;
- parágrafo 3.a: verificação de uma determinada condição para continuar a execução do ciclo;
- cláusula 3.b: execução do corpo do ciclo;
- ponto 3.e: criando um novo ambiente.
Bem, e, diretamente, o algoritmo para criar ambientes internos do loop
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, o primeiro parágrafo verifica a presença de quaisquer elementos no parâmetro passado, e o parágrafo 1 só é executado se houver anúncios de
permissão . Todos os novos ambientes são criados com referência ao mesmo contexto externo e recebem os valores mais recentes da iteração anterior (ambiente de trabalho anterior) como novas ligações de variáveis
let .
Como exemplo, considere uma expressão semelhante:
let arr = []; for (let i = 0; i < 3; i++) { arr.push(i); } console.log(arr);
E aqui está como ele pode ser decomposto sem o uso
de (com uma certa quantidade de convencionalidade):
let arr = [];
De fato, chegamos à conclusão de que, para cada contexto, e aqui temos cinco deles, fazemos novas ligações para variáveis
let declaradas como o primeiro parâmetro em (importante: isso não se aplica a
deixar declarações diretamente no corpo do loop).
Veja como, por exemplo, esse loop será exibido ao usar
var quando não houver ligações adicionais:
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);
E podemos chegar a uma conclusão aparentemente lógica de que, se durante a execução de nossos loops, não for necessário criar ligações separadas para cada iteração (
mais sobre situações nas quais isso, pelo contrário, pode fazer sentido ), devemos fazer a declaração de variáveis incrementais antes com um loop
for , que deve nos impedir de criar e excluir um grande número de contextos e, em teoria, melhorar o desempenho.
Vamos tentar fazer isso, usando a mesma classificação de uma matriz de 100.000 elementos como exemplo, e por uma questão de beleza, também definimos todas as outras variáveis 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; }
Resultado inesperado ... Exatamente o oposto do esperado, para ser preciso. O rebaixamento do
Firefox neste teste é particularmente impressionante.
Ok Isso não funcionou, vamos retornar a declaração das variáveis
iej de volta aos parâmetros dos ciclos correspondentes:
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; }
Hum. Parece que, tecnicamente, a única diferença entre o último exemplo e o exemplo no início do artigo são as declarações feitas das variáveis
min, mini e
len fora do loop
for e, embora a diferença ainda seja contextual, não é de particular interesse para nós agora e, além disso, nos livramos da necessidade de declarar essas variáveis 99.999 vezes no corpo do ciclo de nível superior, o que, teoricamente, deveria aumentar a produtividade em vez de reduzi-la em mais de um segundo.
Ou seja, acontece que, de alguma forma, trabalhar com variáveis declaradas no parâmetro ou no corpo do loop
for acontece muito mais rapidamente do que fora dele.
Porém, não parecemos ver nenhuma instrução “turbo” na especificação do loop
for que poderia nos levar a essa ideia. Portanto, não são os detalhes específicos do trabalho do loop
for , mas outra coisa ... Por exemplo, os recursos das declarações
let : qual é o principal recurso que distingue
let de
var ? Contexto de execução de bloco! E em nossos dois últimos exemplos, usamos anúncios fora do bloco. Mas, e se, em vez de retornar essas declarações
para, apenas selecionarmos um bloco separado para elas?
{ 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; } }
Voila! Acontece que o problema foi que
os anúncios ocorreram em um contexto global e, assim que alocamos um bloco separado para eles, todos os problemas desapareceram ali.
E aqui seria bom recordar outra maneira, ligeiramente imerecida, de declarar variáveis -
var .
No exemplo no início do artigo, o tempo de classificação usando
var mostrou um resultado extremamente deplorável em relação ao
let . Mas, se você der uma olhada mais de perto neste exemplo, poderá descobrir que, como
var não tinha ligações de blocos variáveis, o contexto real das variáveis era global. E nós, no exemplo do
let , já descobrimos como isso pode afetar o desempenho (e, o que é típico, ao usar o
let , o rebaixamento da velocidade se mostrou mais forte do que no caso do
var , especialmente no
Firefox ). Portanto, para ser justo, executaremos um exemplo com
var criando um novo contexto para variáveis:
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();
E obtivemos o resultado quase idêntico ao que estava ao usar
let .
Por fim, vamos verificar se a desaceleração ocorre lendo a variável global sem alterar seu valor.
deixar 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; }
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();
Os resultados indicam que a leitura da variável global não afetou o tempo de execução.
Resumir
- Mudar variáveis globais é muito mais lento que mudar as locais. Levando isso em conta, é possível otimizar o código em situações apropriadas criando um bloco ou função separada, inclusive para declarar variáveis, em vez de executar parte do código em um contexto global. Sim, em quase todos os livros didáticos, você pode encontrar recomendações para fazer o menor número possível de ligações globais, mas geralmente apenas um entupimento do espaço para nome global é indicado como motivo, e não uma palavra sobre possíveis problemas de desempenho.
- Apesar do fato de que a execução de loops com uma declaração let no primeiro parâmetro for criar um grande número de ambientes, isso quase não afeta o desempenho, ao contrário das situações em que as declarações são retiradas do bloco. No entanto, não se deve excluir a possibilidade da existência de situações exóticas quando esse fator afetará a produtividade de maneira mais significativa.
- O desempenho das variáveis var ainda não é inferior ao das variáveis let , no entanto, não as excede (novamente, no caso geral), o que nos leva à próxima conclusão de que não há razão para usar as declarações var , exceto para fins de compatibilidade. No entanto, se você precisar manipular variáveis globais alterando seus valores, a variante var será preferível em termos de desempenho (pelo menos por enquanto, se, em particular, for assumido que o script também possa ser executado no mecanismo Gecko).
Referências
ECMAScript 2019 (ECMA-262)Usando declarações let de variáveis e recursos dos fechamentos resultantes em JavaScript