código é mostrado:
const fn = (length, label, callback) => {
console.time(label);
for (let i = 0; i < length; i++) {
callback && callback(i);
}
console.timeEnd(label);
};
const length = 100000000;
fn(length, "1", () => {}) // very few intervals
fn(length, "2", () => {}) // regular
fn(length, "3", () => {}) // regular
e então removi o terceiro argumento callback
, e seus tempos de execução estão muito próximos.
const fn = (length, label, callback) => {
console.time(label);
for (let i = 0; i < length; i++) {
callback && callback(i);
}
console.timeEnd(label);
};
const length = 100000000;
fn(length, "1") // regular
fn(length, "2") // regular
fn(length, "3") // regular
por que???
Resumindo: é devido ao inlining.
Quando uma chamada como a
callback()
viu apenas uma função de destino sendo chamada, e a função que a contém ("fn
" neste caso) é otimizada, o compilador de otimização (geralmente) decidirá incorporar esse destino de chamada. Portanto, na versão rápida, nenhuma chamada real é executada; em vez disso, a função vazia é incorporada.Quando você chama retornos de chamada diferentes, o código otimizado antigo precisa ser descartado ("desotimizado"), porque agora está incorreto (se o novo retorno de chamada tiver comportamento diferente) e, após a reotimização um pouco mais tarde, a heurística inlining decide que incorporar vários alvos possíveis provavelmente não vale o custo (porque o alinhamento, embora às vezes permita grandes benefícios de desempenho, também tem certos custos), portanto, não incorpora nada. Em vez disso, o código otimizado gerado agora realizará chamadas reais e você verá o custo disso.
Como @0stone0 observou, quando você passa o mesmo retorno de chamada na segunda chamada para
fn
, a desotimização não é necessária, portanto, o código otimizado gerado originalmente (que incorporou esse retorno de chamada) pode continuar a ser usado. Definir três retornos de chamada diferentes, todos com o mesmo código-fonte (vazio), não conta como "o mesmo retorno de chamada".FWIW, esse efeito é mais pronunciado em microbenchmarks; embora às vezes também seja visível em códigos mais reais. É certamente uma armadilha comum em que microbenchmarks caem e produzem resultados confusos/enganosos.
No segundo experimento, quando não houver
callback
, é claro que acallback &&
parte da expressão já será resgatada e nenhuma das três chamadas tofn
chamará (ou incorporará) qualquer retorno de chamada, porque não há retornos de chamada.