Estou lendo os slides Quebrando o limite de velocidade do Javascript com a V8 , e há um exemplo como o código abaixo. Não consigo descobrir por que <=
é mais lento do que <
neste caso, alguém pode explicar isso? Quaisquer comentários são apreciados.
Lento:
this.isPrimeDivisible = function(candidate) {
for (var i = 1; i <= this.prime_count; ++i) {
if (candidate % this.primes[i] == 0) return true;
}
return false;
}
(Dica: primes é uma matriz de comprimento prime_count)
Mais rápido:
this.isPrimeDivisible = function(candidate) {
for (var i = 1; i < this.prime_count; ++i) {
if (candidate % this.primes[i] == 0) return true;
}
return false;
}
[Mais informações] a melhoria da velocidade é significativa, no meu teste de ambiente local, os resultados são os seguintes:
V8 version 7.3.0 (candidate)
Lento:
time d8 prime.js
287107
12.71 user
0.05 system
0:12.84 elapsed
Mais rápido:
time d8 prime.js
287107
1.82 user
0.01 system
0:01.84 elapsed
javascript
v8
Leonardo Physh
fonte
fonte
<=
e<
é idêntica, tanto na teoria quanto na implementação real em todos os processadores (e intérpretes) modernos.main
código que chama essa função em um loop que executa25000
vezes, então você está fazendo muito menos iterações no geral fazendo essa alteração. Além disso, se uma matriz tiver um comprimento de 5, a tentativa de obterarray[5]
ficará fora do seu limite, fornecendo umundefined
valor, já que as matrizes começam a indexação0
.<=
mas age de forma idêntica à<
versão fazendo issoi <= this.prime_count - 1
. Isso resolve o problema da "iteração extra" e o problema "um após o final da matriz".Respostas:
Eu trabalho no V8 no Google e queria fornecer algumas dicas adicionais sobre as respostas e comentários existentes.
Para referência, veja o exemplo de código completo dos slides :
Em primeiro lugar, a diferença de desempenho não tem nada a ver com os operadores
<
e<=
diretamente. Então, por favor, não pule os aros apenas para evitar<=
no seu código, porque você lê no Stack Overflow que é lento - não é!Segundo, as pessoas apontaram que a matriz é "holey". Isso não ficou claro no snippet de código na postagem do OP, mas fica claro quando você olha para o código que inicializa
this.primes
:Isso resulta em uma matriz com um
HOLEY
tipo de elemento na V8, mesmo se a matriz acabar completamente preenchida / compactada / contígua. Em geral, as operações em matrizes holey são mais lentas do que as operações em matrizes compactadas, mas nesse caso a diferença é insignificante: equivale a 1 verificação Smi ( número inteiro pequeno ) adicional (para proteção contra buracos) cada vez que atingimosthis.primes[i]
o loop dentroisPrimeDivisible
. Nada demais!TL; DR A matriz
HOLEY
não é o problema aqui.Outros apontaram que o código lê fora dos limites. Geralmente, é recomendável evitar a leitura além do tamanho das matrizes e, nesse caso, teria realmente evitado a queda maciça no desempenho. Mas porque? O V8 pode lidar com alguns desses cenários fora dos limites com apenas um pequeno impacto no desempenho. O que há de tão especial nesse caso em particular, então?
A leitura fora dos limites resulta em
this.primes[i]
estarundefined
nesta linha:E isso nos leva ao problema real : o
%
operador agora está sendo usado com operandos não inteiros!integer % someOtherInteger
pode ser calculado com muita eficiência; Os mecanismos JavaScript podem produzir código de máquina altamente otimizado para este caso.integer % undefined
por outro lado, é menos eficienteFloat64Mod
, poisundefined
é representado como um duplo.O trecho de código pode realmente ser aprimorado alterando o
<=
para<
nesta linha:... não porque, de
<=
alguma forma, seja um operador superior a ele<
, mas apenas porque isso evita a leitura fora dos limites neste caso específico.fonte
Outras respostas e comentários mencionam que a diferença entre os dois loops é que o primeiro executa mais uma iteração que o segundo. Isso é verdade, mas em uma matriz que cresce para 25.000 elementos, uma iteração mais ou menos faria apenas uma diferença minúscula. Como estimativa, se assumirmos que o comprimento médio à medida que cresce é de 12.500, a diferença que podemos esperar deve estar em torno de 1 / 12.500, ou apenas 0,008%.
A diferença de desempenho aqui é muito maior do que seria explicada por uma iteração extra, e o problema é explicado no final da apresentação.
this.primes
é uma matriz contígua (cada elemento possui um valor) e os elementos são todos números.Um mecanismo JavaScript pode otimizar essa matriz para ser uma matriz simples de números reais, em vez de uma matriz de objetos que contêm números, mas que podem conter outros valores ou nenhum valor. O primeiro formato é muito mais rápido de acessar: é preciso menos código e a matriz é muito menor, para que caiba melhor no cache. Mas existem algumas condições que podem impedir que esse formato otimizado seja usado.
Uma condição seria se alguns dos elementos da matriz estivessem ausentes. Por exemplo:
Agora, qual é o valor
a[1]
? Não tem valor . (Nem é correto dizer que tem o valorundefined
- um elemento da matriz que contém oundefined
valor é diferente de um elemento da matriz que está totalmente ausente.)Não existe uma maneira de representar isso apenas com números; portanto, o mecanismo JavaScript é forçado a usar o formato menos otimizado. Se
a[1]
contivesse um valor numérico como os outros dois elementos, a matriz poderia potencialmente ser otimizada em apenas uma matriz de números.Outro motivo para uma matriz ser forçada para o formato desoptimizado pode ser se você tentar acessar um elemento fora dos limites da matriz, conforme discutido na apresentação.
O primeiro loop com
<=
tentativas de ler um elemento após o final da matriz. O algoritmo ainda funciona corretamente, porque na última iteração extra:this.primes[i]
avaliaundefined
porquei
já passou do final da matriz.candidate % undefined
(para qualquer valor decandidate
) é avaliado comoNaN
.NaN == 0
avalia comofalse
.return true
não é executado.Portanto, é como se a iteração extra nunca tivesse acontecido - não tem efeito sobre o resto da lógica. O código produz o mesmo resultado que faria sem a iteração extra.
Mas, para chegar lá, ele tentou ler um elemento inexistente após o final da matriz. Isso força a matriz a não ser otimizada - ou pelo menos ocorreu no momento desta palestra.
O segundo loop com
<
lê apenas os elementos existentes na matriz, permitindo uma matriz e um código otimizados.O problema está descrito nas páginas 90-91 da palestra, com discussões relacionadas nas páginas anteriores e posteriores.
Por acaso, participei dessa apresentação de E / S do Google e conversei com o palestrante (um dos autores do V8) depois. Eu estava usando uma técnica em meu próprio código que envolvia ler além do final de uma matriz como uma tentativa equivocada (em retrospectiva) de otimizar uma situação específica. Ele confirmou que, se você tentasse ler além do final de uma matriz, impediria que o formato otimizado simples fosse usado.
Se o que o autor da V8 disse ainda for verdadeiro, a leitura após o final da matriz impediria que ela fosse otimizada e teria que voltar ao formato mais lento.
Agora é possível que o V8 tenha sido aprimorado para lidar com esse caso com eficiência ou que outros mecanismos JavaScript o tratem de maneira diferente. Eu não sei de um jeito ou de outro sobre isso, mas essa desoptimização é o que a apresentação estava falando.
fonte
undefined
vez de um número que leva a um cálculo diferente.Object
), porque precisa suportar qualquer combinação de tipos de dados na matriz. Como mencionei acima, o código no loop que está sendo alimentadoundefined
não afeta a correção do algoritmo - ele não altera o cálculo (é como se a iteração extra nunca tivesse acontecido).Value
objetos que pode conter referências a valores de qualquer tipo. (Eu inventei o nomeValue
, mas o ponto é que os elementos da matriz não são apenas números simples, mas são objetos que os números de envoltório ou outros tipos.)HOLEY
porque foi criada usandonew Array(n)
(embora essa parte do código não estivesse visível no OP).HOLEY
matrizes permanecemHOLEY
para sempre na V8 , mesmo quando são preenchidas posteriormente. Dito isto, a matriz sendo holey não é a razão para o problema de perf neste caso; significa apenas que precisamos fazer uma verificação Smi extra em cada iteração (para proteger contra buracos), o que não é grande coisa.TL; DR O loop mais lento se deve ao acesso à matriz 'fora dos limites', que força o mecanismo a recompilar a função com menos ou mesmo sem otimizações OU a não compilar a função com nenhuma dessas otimizações para começar ( se o compilador (JIT-) detectou / suspeitou dessa condição antes da primeira compilação 'versão'), leia abaixo o porquê;
Alguém apenas tem que dizer isso (absolutamente espantado que ninguém já o fez):
Costumava haver um tempo em que o trecho do OP seria um exemplo de fato em um livro de programação para iniciantes, destinado a descrever / enfatizar que 'matrizes' em javascript são indexadas a partir de em 0, não 1 e, como tal, deve ser usado como exemplo de um 'erro de iniciante' comum (você não ama como evitei a frase 'erro de programação'
;)
): acesso à matriz fora dos limites .Exemplo 1:
a
Dense Array
(sendo contíguo (significa que não há intervalos entre os índices) E, na verdade, um elemento em cada índice) de 5 elementos usando a indexação baseada em 0 (sempre no ES262).Portanto, não estamos realmente falando sobre a diferença de desempenho entre
<
vs<=
(ou 'uma iteração extra'), mas estamos falando:'por que o snippet correto (b) é executado mais rápido que o snippet incorreto (a)'?
A resposta é dupla (embora da perspectiva do implementador da linguagem ES262 ambas sejam formas de otimização):
O item 1 é suficientemente (e corretamente IMHO) explicado pela resposta aceita , mas isso gasta apenas 2 palavras ('o código') no item 2: compilação .
Mais precisamente: compilação JIT e, mais importante ainda, compilação JIT- RE !
A especificação da linguagem é basicamente apenas uma descrição de um conjunto de algoritmos ('etapas a serem executadas para alcançar o resultado final definido'). O que, como se vê, é uma maneira muito bonita de descrever uma linguagem. E deixa o método real que um mecanismo usa para alcançar resultados especificados aberto aos implementadores, dando ampla oportunidade para encontrar maneiras mais eficientes de produzir resultados definidos. Um mecanismo de conformidade de especificações deve fornecer resultados de conformidade de especificações para qualquer entrada definida.
Agora, com o código javascript / bibliotecas / uso aumentando e lembrando quantos recursos (tempo / memória / etc) um compilador 'real' usa, fica claro que não podemos fazer com que os usuários que visitam uma página da Web esperem tanto tempo (e os exigem ter tantos recursos disponíveis).
Imagine a seguinte função simples:
Perfeitamente claro, certo? Não requer nenhum esclarecimento extra, certo? O tipo de retorno é
Number
, certo?Bem .. não, não e não ... Depende de qual argumento você passa para o parâmetro de função nomeado
arr
...Vê o problema? Em seguida, considere que isso apenas raspa as permutações possíveis em massa ... Nós nem sabemos que tipo de TIPO a função RETURN até terminarmos ...
Agora imagine esse mesmo código de função sendo usado em diferentes tipos ou mesmo variações de entrada, ambos completamente literalmente (no código-fonte) descritos e dinamicamente 'matrizes' geradas no programa.
Portanto, se você compilar a função
sum
APENAS UMA VEZ, a única maneira de sempre retornar o resultado definido por especificação para todo e qualquer tipo de entrada, obviamente, somente executando TODAS as principais etapas E sub-prescritas por especificação, poderá garantir resultados conformes às especificações (como um navegador pré-y2k sem nome). Não há otimizações (porque não há suposições) e a linguagem de script interpretada lenta morta.JIT-Compilation (JIT como Just In Time) é a solução popular atual.
Então, você começa a compilar a função usando suposições sobre o que ela faz, retorna e aceita.
você cria verificações o mais simples possível para detectar se a função pode começar a retornar resultados não conformes à especificação (como porque recebe entrada inesperada). Em seguida, jogue fora o resultado compilado anterior e recompile para algo mais elaborado, decida o que fazer com o resultado parcial que você já possui (é válido confiar ou computar novamente para ter certeza), vincule a função novamente ao programa e tente novamente. Em última análise, voltando à interpretação de script passo a passo, como nas especificações.
Tudo isso leva tempo!
Todos os navegadores funcionam em seus mecanismos, para cada sub-versão, você verá as coisas melhorarem e regredirem. Em algum momento da história, as cordas eram realmente imutáveis (portanto, array.join era mais rápido que a concatenação de cordas), agora usamos cordas (ou similares) que aliviam o problema. Ambos retornam resultados conformes às especificações e é isso que importa!
Para encurtar a história: apenas porque a semântica da linguagem javascript geralmente nos recupera (como com esse bug silencioso no exemplo do OP) não significa que erros 'estúpidos' aumentem nossas chances de o compilador cuspir código-máquina rápido. Ele pressupõe que escrevemos as instruções corretas 'geralmente': o mantra atual que nós 'usuários' (da linguagem de programação) devemos ter é: ajudar o compilador, descrever o que queremos, favorecer expressões comuns (use asm.js para obter um entendimento básico quais navegadores podem tentar otimizar e por quê).
Por causa disso, falar sobre desempenho é importante, mas TAMBÉM é um campo minado (e, por causa desse campo, eu realmente quero terminar apontando (e citando) algum material relevante:
Fonte:
"JITProf: Identificando o código JavaScript hostil para o JIT"
publicação de Berkeley, 2014, por Liang Gong, Michael Pradel, Koushik Sen.
http://software-lab.org/publications/jitprof_tr_aug3_2014.pdf
ASM.JS (também não gosta de acessar a matriz fora do limite):
http://asmjs.org/spec/latest/
e finalmente https://blogs.windows.com/msedgedev/2015/05/07/bringing-asm-js-to-chakra-microsoft-edge/
, houve uma pequena subseção sobre as melhorias de desempenho interno do mecanismo ao remover limites- check (enquanto apenas levantando os limites - check fora do loop já teve uma melhoria de 40%).
EDIT:
observe que várias fontes falam sobre diferentes níveis de recompilação JIT até a interpretação.
Exemplo teórico com base nas informações acima, em relação ao snippet do OP:
Portanto, o tempo foi então:
Primeira execução (falha no final) + fazendo todo o trabalho novamente usando código de máquina mais lento para cada iteração + a recompilação etc. .. claramente leva> 2 vezes mais neste exemplo teórico !
EDIT 2: (exoneração de responsabilidade: conjectura baseada nos fatos abaixo)
Quanto mais eu penso nisso, mais acho que essa resposta pode realmente explicar a razão mais dominante dessa 'penalidade' no trecho errado a (ou bônus de desempenho no trecho b) , dependendo de como você pensa sobre isso), precisamente por que eu sou um adágio em chamá-lo (snippet a) de erro de programação:
É bastante tentador supor que
this.primes
seja um numérico puro de 'matriz densa' que fossenew Array(/*size value*/)
) em ordem sequencial crescente (outro candidato conhecido há muito tempo para se tornar um array 'real').Também sabemos que o
primes
comprimento da matriz é armazenado em cache comoprime_count
! (indicando sua intenção e tamanho fixo).Também sabemos que a maioria dos mecanismos passa os Arrays inicialmente como copiar na modificação (quando necessário), o que os torna muito mais rápidos (se você não os alterar).
Portanto, é razoável supor que a matriz
primes
provavelmente já seja uma matriz otimizada internamente, que não será alterada após a criação (simples de saber para o compilador se não houver código que modifique a matriz após a criação) e, portanto, já seja (se aplicável a o mecanismo) armazenado de maneira otimizada, quase como se fosse umTyped Array
.Como tentei deixar claro com meu
sum
exemplo de função, os argumentos que são passados influenciam muito o que realmente precisa acontecer e, como tal, como esse código específico está sendo compilado no código de máquina. Passar umString
para asum
função não deve alterar a string, mas alterar como a função é compilada por JIT! Passar uma matriz parasum
deve compilar uma versão diferente (talvez adicional para esse tipo, ou 'forma' como eles chamam, de objeto que foi passado) do código de máquina.Como parece um pouco estranho converter o
primes
Array do tipo Typed_Array on-the-fly para algo mais, enquanto o compilador sabe que essa função nem sequer a modifica!Sob essas premissas, restam 2 opções:
Agora eu realmente me pergunto qual desses 2 é!
fonte
Para adicionar um pouco de cientificidade, aqui está um jsperf
https://jsperf.com/ints-values-in-out-of-array-bounds
Ele testa o caso de controle de uma matriz preenchida com ints e looping, fazendo aritmética modular enquanto permanece dentro dos limites. Possui 5 casos de teste:
new Array()
Isso mostra que os 4 primeiros casos são muito ruins para o desempenho. Fazer um loop fora dos limites é um pouco melhor que os outros 3, mas todos os 4 são aproximadamente 98% mais lentos que o melhor.
O
new Array()
case é quase tão bom quanto o array bruto, apenas um pouco mais lento.fonte