por que a última função é 10% mais rápida, embora precise criar as variáveis ​​repetidamente?

14
var toSizeString = (function() {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

  return function(size) {
    var gbSize = size / GB,
        gbMod  = size % GB,
        mbSize = gbMod / MB,
        mbMod  = gbMod % MB,
        kbSize = mbMod / KB;

    if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
    } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
    } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
    } else {
      return size + 'B';
    }
  };
})();

E a função mais rápida: (observe que sempre deve calcular as mesmas variáveis ​​kb / mb / gb repetidas vezes). Onde ele ganha desempenho?

function toSizeString (size) {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

 var gbSize = size / GB,
     gbMod  = size % GB,
     mbSize = gbMod / MB,
     mbMod  = gbMod % MB,
     kbSize = mbMod / KB;

 if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
 } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
 } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
 } else {
      return size + 'B';
 }
};
Para o meu
fonte
3
Em qualquer linguagem de tipo estaticamente, as "variáveis" seriam compiladas como constantes. Talvez os mecanismos JS modernos sejam capazes de fazer a mesma otimização. Isso parece não funcionar se as variáveis ​​fizerem parte de um fechamento.
usr
6
este é um detalhe de implementação do mecanismo JavaScript que você está usando. O tempo e o espaço teóricos são os mesmos, é apenas a implementação de um determinado mecanismo JavaScript que os variará. Portanto, para responder sua pergunta corretamente, é necessário listar o mecanismo JavaScript específico com o qual você os mediu. Talvez alguém conheça os detalhes de sua implementação para dizer como / por que ela tornou uma mais ótima que a outra. Você também deve postar seu código de medição.
Jimmy Hoffa
você usa a palavra "computação" em referência a valores constantes; não há realmente nada para calcular no que você está referenciando. A aritmética de valores constantes é uma das otimizações mais simples e óbvias que os compiladores fazem; portanto, sempre que você vê uma expressão que possui apenas valores constantes, basta assumir que toda a expressão é otimizada para um único valor constante.
Jimmy Hoffa 15/03
@JimmyHoffa isso é verdade, mas por outro lado ele precisa criar 3 variáveis constantes cada chamada de função ...
Tomy
As constantes @Tomy não são variáveis. Eles não variam, portanto, não precisam ser recriados após a compilação. Uma constante é geralmente colocada na memória e todo alcance futuro dessa constante é direcionado para o mesmo local, não há necessidade de recriá-la porque seu valor nunca varia , portanto, não é uma variável. Os compiladores geralmente não emitem código que cria constantes, o compilador faz a criação e direciona todas as referências de código ao que foi criado.
Jimmy Hoffa

Respostas:

23

Os modernos mecanismos JavaScript fazem compilação just-in-time. Você não pode fazer nenhuma suposição sobre o que "deve criar repetidamente". Esse tipo de cálculo é relativamente fácil de otimizar, em ambos os casos.

Por outro lado, fechar sobre variáveis ​​constantes não é um caso típico para o qual você visaria a compilação JIT. Você normalmente cria um fechamento quando deseja poder alterar essas variáveis ​​em diferentes invocações. Você também está criando uma desreferência adicional de ponteiro para acessar essas variáveis, como a diferença entre acessar uma variável de membro e um int local no OOP.

Esse tipo de situação é o motivo pelo qual as pessoas jogam fora a linha da "otimização prematura". As otimizações fáceis já são feitas pelo compilador.

Karl Bielefeldt
fonte
Eu suspeito que é esse deslocamento de escopo para resolução variável que está causando a perda, como você mencionou. Parece razoável, mas quem realmente sabe o que está loucura em um motor de JavaScript JIT ...
Jimmy Hoffa
1
Possível expansão desta resposta: a razão pela qual um JIT ignoraria uma otimização fácil para um compilador offline é porque o desempenho de todo o compilador é mais importante do que em casos incomuns.
Leushenko
12

Variáveis ​​são baratas. Contextos de execução e cadeias de escopo são caros.

Existem várias respostas que se resumem basicamente a "porque fechamentos", e essas são essencialmente verdadeiras, mas o problema não está especificamente no fechamento, é o fato de você ter uma função que faz referência a variáveis ​​em um escopo diferente. Você teria o mesmo problema se essas fossem variáveis ​​globais no windowobjeto, em oposição às variáveis ​​locais dentro do IIFE. Experimente e veja.

Então, na sua primeira função, quando o mecanismo vê esta declaração:

var gbSize = size / GB;

É necessário seguir os seguintes passos:

  1. Procure uma variável sizeno escopo atual. (Encontrei.)
  2. Procure uma variável GBno escopo atual. (Não encontrado.)
  3. Procure uma variável GBno escopo pai. (Encontrei.)
  4. Faça o cálculo e atribua a gbSize.

O passo 3 é consideravelmente mais caro do que apenas alocar uma variável. Além disso, você faz isso cinco vezes , incluindo duas vezes para ambos GBe MB. Eu suspeito que, se você aliasse esses itens no início da função (por exemplo var gb = GB) e referenciasse o alias, isso produziria uma pequena aceleração, embora também seja possível que alguns mecanismos JS já executem essa otimização. E, é claro, a maneira mais eficaz de acelerar a execução é simplesmente não atravessar a cadeia de escopo.

Lembre-se de que o JavaScript não é como uma linguagem compilada estaticamente, em que o compilador resolve esses endereços variáveis ​​no momento da compilação. O mecanismo JS deve resolvê-los pelo nome , e essas pesquisas acontecem em tempo de execução, sempre. Então você quer evitá-los quando possível.

A atribuição de variáveis ​​é extremamente barata em JavaScript. Pode ser realmente a operação mais barata, embora eu não tenha nada para fazer backup dessa declaração. No entanto, é seguro dizer que quase nunca é uma boa idéia tentar evitar a criação de variáveis; quase todas as otimizações que você tenta fazer nessa área acabam piorando as coisas, em termos de desempenho.

Aaronaught
fonte
E mesmo se a "otimização" não afeta negativamente o desempenho, é quase certo que está indo afetar a legibilidade do código negativamente. O que, a menos que você esteja fazendo algumas coisas computacionais malucas, geralmente é uma troca ruim (aparentemente nenhuma âncora permanente), procure "2009-02-17 11:41"). Como segue o resumo: "Escolha clareza sobre a velocidade, se a velocidade não for absolutamente necessária".
a CVn
Mesmo ao escrever um intérprete muito básico para linguagens dinâmicas, o acesso variável durante o tempo de execução tende a ser uma operação O (1) e o deslocamento do escopo O (n) nem é necessário durante a compilação inicial. Em cada escopo, cada variável recém-declarada recebe um número atribuído, para var a, b, cque possamos acessar bcomo scope[1]. Todos os escopos são numerados e, se esse escopo estiver aninhado com cinco escopos, bserá totalmente endereçado pelo env[5][1]qual é conhecido durante a análise. No código nativo, os escopos correspondem aos segmentos da pilha. Os fechamentos são mais complicados, pois precisam fazer backup e substituir oenv
amon
@amon: Isso poderia ser como você gostaria idealmente como ele para trabalhar, mas não é como ele realmente funciona. Pessoas com muito mais conhecimento e experiência do que eu escrevi livros sobre isso; em particular, eu indicaria o JavaScript de alto desempenho de Nicholas C. Zakas. Aqui está um trecho , e ele também conversou com benchmarks para fazer o backup. Claro que ele certamente não é o único, apenas o mais conhecido. O JavaScript tem escopo lexical; portanto, os fechamentos na verdade não são tão especiais - essencialmente, tudo é um fechamento.
Aaronaught 17/03/2015
@Aaronaught Interessante. Como esse livro tem 5 anos, eu estava interessado em saber como um mecanismo JS atual lida com pesquisas variáveis ​​e analisou o back-end x64 do mecanismo V8. Durante a análise estática, a maioria das variáveis ​​é resolvida estaticamente e recebe um deslocamento de memória em seu escopo. Os escopos de funções são representados como listas vinculadas e o assembly é emitido como um loop não rotacionado para atingir o escopo correto. Aqui, obteríamos o equivalente ao código C *(scope->outer + variable_offset)para um acesso; cada nível de escopo de função extra custa uma desreferência adicional de ponteiro. Parece que nós dois estávamos certo :)
amon
2

Um exemplo envolve um fechamento, o outro não. A implementação de fechamentos é meio complicada, pois as variáveis ​​fechadas não funcionam como variáveis ​​normais. Isso é mais óbvio em uma linguagem de baixo nível como C, mas usarei JavaScript para ilustrar isso.

Um fechamento não consiste apenas de uma função, mas também de todas as variáveis ​​que ele encerrou. Quando queremos chamar essa função, também precisamos fornecer todas as variáveis ​​de fechamento. Podemos modelar um fechamento por uma função que recebe um objeto como primeiro argumento que representa essas variáveis ​​fechadas sobre:

function add(vars, y) {
  vars.x += y;
}

function getSum(vars) {
  return vars.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(adder, 2);
console.log(adder.getSum(adder));  //=> 42

Observe a convenção de chamada estranha que closure.apply(closure, ...realArgs)isso exige

O suporte a objetos embutidos do JavaScript permite omitir o varsargumento explícito e, em thisvez disso , vamos usar :

function add(y) {
  this.x += y;
}

function getSum() {
  return this.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

Esses exemplos são equivalentes a esse código, na verdade, usando closures:

function makeAdder(x) {
  return {
    add: function (y) { x += y },
    getSum: function () { return x },
  };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

Neste último exemplo, o objeto é usado apenas para agrupar as duas funções retornadas; a thisligação é irrelevante. Todos os detalhes de tornar possíveis os fechamentos - passando dados ocultos para a função real, alterando todos os acessos às variáveis ​​de fechamento para pesquisas nesses dados ocultos - são resolvidos pelo idioma.

Mas chamar encerramentos envolve a sobrecarga de passar esses dados extras, e executar um encerramento envolve a sobrecarga de pesquisas nesses dados extras - agravadas pela má localização do cache e, geralmente, por uma desreferência de ponteiro quando comparadas às variáveis ​​comuns -, portanto, não é surpreendente que uma solução que não depende de fechamentos tem melhor desempenho. Especialmente porque tudo o que seu fechamento lhe permite fazer são algumas operações aritméticas extremamente baratas, que podem até ser dobradas constantemente durante a análise.

amon
fonte