Pilha de chamadas JavaScript e mais magia





No início de abril, o artigo "JavaScript: Call stack e a magia de seu tamanho" foi publicado no Habré - seu autor chegou à conclusão de que cada stack frame ocupa (72 + 8 * número de_variáveis_locais) bytes: "Acontece que nós contamos tudo corretamente e podemos afirmar que o tamanho de um ExecutionStack vazio no Chrome é de 72 bytes, e o tamanho da pilha de chamadas é pouco menos de um megabyte. Bom trabalho! "



Para propagação - vamos mudar um pouco o código usado AxemaFr para experimentos:



{let i = 0;

const func = () => {
  i += 1.00000000000001;
  func();
};

try {
  func();
} catch (e) {
  console.log(i);
}}
      
      





Em vez de 1, agora a cada etapa adicionamos um pouco mais e, como resultado, em vez de 13951, obtemos 12556,000000000002 - como se uma variável local fosse adicionada à função!



Vamos repetir as perguntas feitas pelo Desenvolvedor Frontend Sênior AxemaFr: “Por que é assim? O que mudou? Como entender, olhando para a função, quantas vezes ela pode ser executada recursivamente?! "



Utensílios de cozinha



Na linha de comando do Chrome, você pode passar argumentos para o mecanismo JS; em particular, você pode alterar o tamanho da pilha de 984 KB para qualquer outra chave --js-flags=--stack-size=



.



Para descobrir quanta pilha cada função requer, a chave --print-bytecode



mencionada nos ajudará . Não foi mencionado que a saída de depuração é enviada para stdout, que o Chrome no Windows estupidamente não tem, porque é compilado como um aplicativo GUI. É fácil de corrigir: faça uma cópia de chrome.exe e em seu editor hexadecimal corrija o byte 0xD4



do valor 0x02



para 0x03



(para aqueles que não são amigos do editor hexadecimal, este byte ajudará a corrigir o script Python) Mas se você estiver lendo este artigo agora no Chrome e apenas executar o arquivo corrigido - digamos que você o nomeou cui_chrome.exe - uma nova janela será aberta em uma instância existente do navegador e o argumento --js-flags



será ignorado. Para iniciar uma nova instância do Chrome, você precisa passar uma nova --user-data-dir



:

cui_chrome.exe --no-sandbox --js-flags="--print-bytecode --print-bytecode-filter=func" --user-data-dir=\Windows\Temp





Sem ele, --print-bytecode-filter



você se afogará em despejos de bytecode de quilômetros de funções integradas ao Chrome.



Depois de iniciar o navegador, abra o console do desenvolvedor e digite o código usado AxemaFr:



{let i = 0;

const func = () => {
  i++;
  func();
};

func()}
      
      





Antes de pressionar Enter, um dump aparecerá na janela do console atrás do Chrome:

[bytecode gerado para a função: func (0x44db08635355 <função SharedFunctionInfo>)]
Contagem de parâmetro 1
Register count 1
Frame size 8
   36 S> 000044DB086355EE @    0 : 1a 02             LdaCurrentContextSlot [2]
         000044DB086355F0 @    2 : ac 00             ThrowReferenceErrorIfHole [0]
         000044DB086355F2 @    4 : 4d 00             Inc [0]
         000044DB086355F4 @    6 : 26 fa             Star r0
         000044DB086355F6 @    8 : 1a 02             LdaCurrentContextSlot [2]
   37 E> 000044DB086355F8 @   10 : ac 00             ThrowReferenceErrorIfHole [0]
         000044DB086355FA @   12 : 25 fa             Ldar r0
         000044DB086355FC @   14 : 1d 02             StaCurrentContextSlot [2]
   44 S> 000044DB086355FE @   16 : 1b 03             LdaImmutableCurrentContextSlot [3]
         000044DB08635600 @   18 : ac 01             ThrowReferenceErrorIfHole [1]
         000044DB08635602 @   20 : 26 fa             Star r0
   44 E> 000044DB08635604 @ 22: 5d fa 01 CallUndefinedReceiver0 r0, [1]
         000044DB08635607 @ 25: 0d LdaUndefined
   52 S> 000044DB08635608 @ 26: ab Retorno
Pool constante (tamanho = 2)
Tabela Handler (tamanho = 0)
Tabela de posição da fonte (tamanho = 12)


Como o dump mudará se a linha for i++;



substituída por i += 1.00000000000001;



?

[bytecode gerado para a função: func (0x44db0892d495 <função SharedFunctionInfo>)]
Contagem de parâmetro 1
Registrar contagem 2
Tamanho da moldura 16
   36 S> 000044DB0892D742 @ 0: 1a 02 LdaCurrentContextSlot [2]
         000044DB0892D744 @ 2: ac 00 ThrowReferenceErrorIfHole [0]
         000044DB0892D746 @ 4: 26 fa Star r0
         000044DB0892D748 @ 6: 12 01 LdaConstant [1]
         000044DB0892D74A @    8 : 35 fa 00          Add r0, [0]
         000044DB0892D74D @   11 : 26 f9             Star r1
         000044DB0892D74F @   13 : 1a 02             LdaCurrentContextSlot [2]
   37 E> 000044DB0892D751 @   15 : ac 00             ThrowReferenceErrorIfHole [0]
         000044DB0892D753 @   17 : 25 f9             Ldar r1
         000044DB0892D755 @   19 : 1d 02             StaCurrentContextSlot [2]
   60 S> 000044DB0892D757 @   21 : 1b 03             LdaImmutableCurrentContextSlot [3]
         000044DB0892D759 @   23 : ac 02             ThrowReferenceErrorIfHole [2]
         000044DB0892D75B @   25 : 26 fa             Star r0
   60 E> 000044DB0892D75D @   27 : 5d fa 01          CallUndefinedReceiver0 r0, [1]
         000044DB0892D760 @   30 : 0d                LdaUndefined
   68 S> 000044DB0892D761 @ 31: ab Retorno
Pool constante (tamanho = 3)
Tabela Handler (tamanho = 0)
Tabela de posição da fonte (tamanho = 12)


Agora vamos descobrir o que mudou e por quê.



Explorando exemplos



Todos os opcodes V8 são descritos em github.com/v8/v8/blob/master/src/interpreter/interpreter-generator.cc

O primeiro dump é decodificado assim:

LdaCurrentContextSlot [2]; a: = contexto [2]
ThrowReferenceErrorIfHole [0]; if (a === indefinido)
                                    ; throw ("ReferenceError:% s não está definido", const [0])
Inc [0]; a ++
Star r0; r0: = a
LdaCurrentContextSlot [2]; a: = contexto [2]
ThrowReferenceErrorIfHole [0]; if (a === indefinido)
                                    ; throw ("ReferenceError:% s não está definido", const [0])
Ldar r0; a: = r0
StaCurrentContextSlot [2]           ; context[2] := a
LdaImmutableCurrentContextSlot [3]  ; a := context[3]
ThrowReferenceErrorIfHole [1]       ; if (a === undefined)
                                    ;   throw("ReferenceError: %s is not defined", const[1])
Star r0                             ; r0 := a
CallUndefinedReceiver0 r0, [1]      ; r0()
LdaUndefined                        ; a := undefined
Return


O último argumento opta Inc



e CallUndefinedReceiver0



define o slot de feedback, no qual o otimizador coleta estatísticas sobre os tipos usados. Isso não afeta a semântica do bytecode, então hoje não estamos interessados.



Sob o dump, há um postscript: "Constant pool (size = 2)" - e de fato vemos que o bytecode usa duas linhas - "i"



e "func"



- para substituição na mensagem de exceção quando os símbolos com tais nomes são indefinidos. Há um postscript acima do dump: "Frame size 8" - de acordo com o fato de que a função usa um registrador de intérprete ( r0



).



O quadro de pilha da nossa função consiste em:



  • único argumento this



    ;
  • endereços de retorno;
  • o número de argumentos passados ​​( arguments.length



    );
  • referências a pool constante com strings usadas;
  • links para contexto com variáveis ​​locais;
  • mais três ponteiros necessários para o motor; e finalmente
  • espaço para um registro.


Total 9 * 8 = 72 bytes, como signor AxemaFre descobri.



Dos sete termos listados, teoricamente, três podem mudar - o número de argumentos, a presença de um pool constante e o número de registros. O que obtivemos na variante com 1.00000000000001?



LdaCurrentContextSlot [2]; a: = contexto [2]
ThrowReferenceErrorIfHole [0]; if (a === indefinido)
                               ; throw ("ReferenceError:% s não está definido", const [0])
Star r0; r0: = a
LdaConstant [1]; a: = const [1]
Adicione r0, [0]; a + = r0
Star r1; r1: = a
                               ; ... mais como antes


Primeiro, a constante adicionada assumiu o terceiro lugar no pool de constantes; em segundo lugar, mais um registro foi necessário para carregá-lo, de modo que o frame da pilha da função cresceu 8 bytes.



Se você não usar símbolos nomeados na função, poderá fazer sem o pool constante. Em github.com/v8/v8/blob/master/src/execution/frame-constants.h#L289 descreveu o formato de quadro de pilha V8 e afirmou que quando o pool constante não está em uso, o tamanho do quadro de pilha é reduzido em um ponteiro . Como você pode ter certeza disso? À primeira vista, parece que uma função que não usa símbolos nomeados não pode ser recursiva; mas dê uma olhada:



{let i = 0;

function func() {
  this()();
};

const helper = () => (i++, func.bind(helper));

try {
  helper()();
} catch (e) {
  console.log(i);
}}
      
      





[generated bytecode for function: func (0x44db0878e575 <SharedFunctionInfo func>)]
Parameter count 1
Register count 1
Frame size 8
   37 S> 000044DB0878E8DA @    0 : 5e 02 02 00       CallUndefinedReceiver1 <this>, <this>, [0]
         000044DB0878E8DE @    4 : 26 fa             Star r0
   43 E> 000044DB0878E8E0 @    6 : 5d fa 02          CallUndefinedReceiver0 r0, [2]
         000044DB0878E8E3 @    9 : 0d                LdaUndefined
   47 S> 000044DB0878E8E4 @   10 : ab                Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 8)


O objetivo - "Pool constante (tamanho = 0)" - foi alcançado; mas o estouro da pilha, como antes, ocorre por meio de 13951 chamadas. Isso significa que, mesmo quando o pool constante não é usado, o quadro de pilha da função ainda contém um ponteiro para ele.



É possível obter um tamanho de frame de pilha menor do que o calculado AxemaFrvalor mínimo? - sim, se nenhum registro for usado dentro da função:

{function func() {
  this();
};

let chain = ()=>null;
for(let i=0; i<15050; i++)
  chain = func.bind(chain);

chain()}
      
      





[bytecode gerado para a função: func (0x44db08c34059 <função SharedFunctionInfo>)]
Contagem de parâmetro 1
Registrar contagem 0
Tamanho da moldura 0
   25 S> 000044DB08C34322 @ 0: 5d 02 00 CallUndefinedReceiver0 <este>, [0]
         000044DB08C34325 @ 3: 0d LdaUndefined
   29 S> 000044DB08C34326 @ 4: ab Retorno
Pool constante (tamanho = 0)
Tabela Handler (tamanho = 0)
Tabela de posição da fonte (tamanho = 6)


(Neste caso, uma cadeia de chamadas de 15051 já leva a "RangeError: Tamanho máximo da pilha de chamadas excedido".)



Assim, a conclusão do signor AxemaFrque "o tamanho de um ExecutionStack vazio no Chrome é de 72 bytes" foi refutado com sucesso.



Refinando previsões



Podemos argumentar que o tamanho mínimo do frame da pilha para uma função JS no Chrome é de 64 bytes. Para isso, você precisa adicionar 8 bytes para cada parâmetro formal declarado, outros 8 bytes para cada parâmetro real em excesso do número de declarados e outros 8 bytes para cada registro usado. Um registro é alocado para cada variável local, para carregar constantes, para acessar variáveis ​​de um contexto externo, para passar parâmetros reais durante chamadas, etc. Dificilmente é possível determinar o número exato de registros usados ​​a partir do código-fonte em JS. É importante notar que o interpretador JS suporta um número ilimitado de registros - eles não estão relacionados aos registros do processador no qual o interpretador é executado.



Agora está claro o porquê:
  • (func = (x) => { i++; func(); };



    ) , ;
  • (func = () => { i++; func(1); };



    ) , — :
    [generated bytecode for function: func (0x44db08e12da1 <SharedFunctionInfo func>)]
    Parameter count 1
    Register count 2
    Frame size 16
       34 S> 000044DB08E12FE2 @    0 : 1a 02             LdaCurrentContextSlot [2]
             000044DB08E12FE4 @    2 : ac 00             ThrowReferenceErrorIfHole [0]
             000044DB08E12FE6 @    4 : 4d 00             Inc [0]
             000044DB08E12FE8 @    6 : 26 fa             Star r0
             000044DB08E12FEA @    8 : 1a 02             LdaCurrentContextSlot [2]
       35 E> 000044DB08E12FEC @   10 : ac 00             ThrowReferenceErrorIfHole [0]
             000044DB08E12FEE @   12 : 25 fa             Ldar r0
             000044DB08E12FF0 @   14 : 1d 02             StaCurrentContextSlot [2]
       39 S> 000044DB08E12FF2 @   16 : 1b 03             LdaImmutableCurrentContextSlot [3]
             000044DB08E12FF4 @   18 : ac 01             ThrowReferenceErrorIfHole [1]
             000044DB08E12FF6 @   20 : 26 fa             Star r0
             000044DB08E12FF8 @   22 : 0c 01             LdaSmi [1]
             000044DB08E12FFA @   24 : 26 f9             Star r1
       39 E> 000044DB08E12FFC @   26 : 5e fa f9 01       CallUndefinedReceiver1 r0, r1, [1]
             000044DB08E13000 @   30 : 0d                LdaUndefined
       48 S> 000044DB08E13001 @   31 : ab                Return
    Constant pool (size = 2)
    Handler Table (size = 0)
    Source Position Table (size = 12)
  • 1.00000000000001 — r1



    , .







All Articles