javascript - ¿Por qué atar es más lento que un cierre?
node.js performance (1)
Actualización de Chrome 59: como predije en la respuesta a continuación, el enlace ya no es más lento con el nuevo compilador de optimización. Aquí está el código con los detalles: https://codereview.chromium.org/2916063002/
La mayoría de las veces no importa.
A menos que estés creando una aplicación donde .bind
sea el cuello de botella, no me molestaría. La legibilidad es mucho más importante que el rendimiento puro en la mayoría de los casos. Creo que el uso de .bind
nativo generalmente proporciona un código más legible y mantenible, lo cual es una gran ventaja.
Sin embargo, sí, cuando importa - .bind
es más lento
Sí, .bind
es considerablemente más lento que un cierre, al menos en Chrome, al menos en la forma actual en que está implementado en v8
. Personalmente tuve que cambiar Node.JS por problemas de rendimiento algunas veces (de manera más general, los cierres son algo lentos en situaciones de rendimiento intensivo).
¿Por qué? Porque el algoritmo .bind
es mucho más complicado que envolver una función con otra función y usar .call
o .apply
. (De hecho, también devuelve una función con toString configurado en [función nativa]).
Hay dos maneras de ver esto, desde el punto de vista de la especificación, y desde el punto de vista de la implementación. Observemos ambos.
Primero, veamos el algoritmo de vinculación definido en la especificación :
- Deje que Target sea este valor.
- Si IsCallable (Target) es falso, ejecute una excepción TypeError.
- Deje que A sea una nueva lista interna (posiblemente vacía) de todos los valores de argumento proporcionados después de este Arg (arg1, arg2, etc.), en orden.
...
(21. Llame al método interno [[DefineOwnProperty]] de F con argumentos "arguments", PropertyDescriptor {[[Get]]: thrower, [[Set]]: thrower, [[Enumerable]]: false, [[Configurable] ]: falso} y falso.
(22. Regreso F.
Parece bastante complicado, mucho más que solo una envoltura.
En segundo lugar, veamos cómo se implementa en Chrome .
Vamos a verificar FunctionBind
en el código fuente v8 (chrome JavaScript engine):
function FunctionBind(this_arg) { // Length is 1.
if (!IS_SPEC_FUNCTION(this)) {
throw new $TypeError(''Bind must be called on a function'');
}
var boundFunction = function () {
// Poison .arguments and .caller, but is otherwise not detectable.
"use strict";
// This function must not use any object literals (Object, Array, RegExp),
// since the literals-array is being used to store the bound data.
if (%_IsConstructCall()) {
return %NewObjectFromBound(boundFunction);
}
var bindings = %BoundFunctionGetBindings(boundFunction);
var argc = %_ArgumentsLength();
if (argc == 0) {
return %Apply(bindings[0], bindings[1], bindings, 2, bindings.length - 2);
}
if (bindings.length === 2) {
return %Apply(bindings[0], bindings[1], arguments, 0, argc);
}
var bound_argc = bindings.length - 2;
var argv = new InternalArray(bound_argc + argc);
for (var i = 0; i < bound_argc; i++) {
argv[i] = bindings[i + 2];
}
for (var j = 0; j < argc; j++) {
argv[i++] = %_Arguments(j);
}
return %Apply(bindings[0], bindings[1], argv, 0, bound_argc + argc);
};
%FunctionRemovePrototype(boundFunction);
var new_length = 0;
if (%_ClassOf(this) == "Function") {
// Function or FunctionProxy.
var old_length = this.length;
// FunctionProxies might provide a non-UInt32 value. If so, ignore it.
if ((typeof old_length === "number") &&
((old_length >>> 0) === old_length)) {
var argc = %_ArgumentsLength();
if (argc > 0) argc--; // Don''t count the thisArg as parameter.
new_length = old_length - argc;
if (new_length < 0) new_length = 0;
}
}
// This runtime function finds any remaining arguments on the stack,
// so we don''t pass the arguments object.
var result = %FunctionBindArguments(boundFunction, this,
this_arg, new_length);
// We already have caller and arguments properties on functions,
// which are non-configurable. It therefore makes no sence to
// try to redefine these as defined by the spec. The spec says
// that bind should make these throw a TypeError if get or set
// is called and make them non-enumerable and non-configurable.
// To be consistent with our normal functions we leave this as it is.
// TODO(lrn): Do set these to be thrower.
return result;
Podemos ver un montón de cosas caras aquí en la implementación. Es decir, %_IsConstructCall()
. Por supuesto, esto es necesario para cumplir con la especificación, pero también lo hace más lento que una simple envoltura en muchos casos.
En otra nota, llamar .bind
también es ligeramente diferente, las notas de la especificación "Objetos de función creados con Function.prototype.bind no tienen una propiedad de prototipo o el [[Code]], [[FormalParameters]] y [[Scope] ] propiedades internas "
Un cartel anterior preguntó Function.bind vs Closure en Javascript: ¿cómo elegir?
y recibió esta respuesta en parte, lo que parece indicar que el enlace debe ser más rápido que un cierre:
Alcance transversal significa, cuando se está buscando alcanzar un valor (variable, objeto) que existe en un ámbito diferente, por lo tanto, se agrega una sobrecarga adicional (el código se vuelve más lento de ejecutar).
Usando bind, está llamando a una función con un alcance existente, para que no se produzca el recorrido del alcance.
Dos jsperfs sugieren que el enlace es en realidad mucho, mucho más lento que un closure .
Esto fue publicado como comentario a lo anterior
Y, decidí escribir mi propia jsperf
Entonces, ¿por qué el enlace es mucho más lento (70 +% en cromo)?
Dado que no es más rápido y los cierres pueden servir para el mismo propósito, debe evitarse?