¿Por qué el enlace es más lento que el cierre?

79

Un cartel anterior preguntaba Function.bind vs Closure en Javascript: ¿cómo elegir?

y recibió esta respuesta en parte, lo que parece indicar que el enlace debería ser más rápido que un cierre:

El traspaso del alcance significa, cuando está tratando de tomar un valor (variable, objeto) que existe en un alcance diferente, por lo tanto, se agrega una sobrecarga adicional (el código se vuelve más lento de ejecutar).

Al usar bind, está llamando a una función con un alcance existente, por lo que no se realiza el recorrido del alcance.

Dos jsperfs sugieren que bind es en realidad mucho, mucho más lento que un cierre .

Esto fue publicado como un comentario a lo anterior.

Y decidí escribir mi propio jsperf

Entonces, ¿por qué la unión es mucho más lenta (más del 70% en cromo)?

Dado que no es más rápido y los cierres pueden servir para el mismo propósito, ¿debería evitarse la unión?

Pablo
fuente
10
"Debería evitarse la encuadernación" --- a menos que lo esté haciendo miles de veces en una página, no debería preocuparse por ello.
zerkms
1
El ensamblaje de una tarea compleja asincrónica a partir de piezas pequeñas puede requerir algo que se vea exactamente así, en nodejs, porque las devoluciones de llamada deben alinearse de alguna manera.
Paul
Supongo que es porque los navegadores no se han esforzado tanto en optimizarlo. Consulte el código de Mozilla ( developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… ) para implementarlo manualmente. Existe la posibilidad de que los navegadores solo lo estén haciendo internamente, lo cual es mucho más trabajo que un cierre rápido.
Dave
1
Las llamadas a funciones indirectas ( apply/call/bind) son en general mucho más lentas que las directas.
georg
@zerkms ¿Y quién puede decir que uno no lo hace miles de veces? Debido a la funcionalidad que proporciona, creo que le sorprenderá lo común que puede ser.
Andrew

Respuestas:

142

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 detalles: https://codereview.chromium.org/2916063002/

La mayoría de las veces no importa.

A menos que esté creando una aplicación donde .bindestá el cuello de botella, no me molestaría. La legibilidad es mucho más importante que el puro rendimiento en la mayoría de los casos. Creo que el uso de nativo .bindgeneralmente proporciona un código más legible y fácil de mantener, lo cual es una gran ventaja.

Sin embargo, sí, cuando importa, .bindes más lento

Sí, .bindes considerablemente más lento que un cierre, al menos en Chrome, al menos en la forma actual en que se implementa v8. Personalmente, en ocasiones tuve que cambiar Node.JS por problemas de rendimiento (de manera más general, los cierres son algo lentos en situaciones de rendimiento intensivo).

¿Por qué? Porque el .bindalgoritmo es mucho más complicado que envolver una función con otra función y usar .callo .apply. (Dato curioso, también devuelve una función con toString establecido en [función nativa]).

Hay dos formas 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 :

  1. Deje que Target sea este valor.
  2. Si IsCallable (Target) es falso, lanza una excepción TypeError.
  3. Sea A una nueva lista interna (posiblemente vacía) de todos los valores de argumento proporcionados después de thisArg (arg1, arg2, etc.), en orden.

...

(21. Llame al método interno [[DefineOwnProperty]] de F con argumentos "argumentos", PropertyDescriptor {[[Get]]: lanzador, [[Set]]: lanzador, [[Enumerable]]: falso, [[Configurable] ]: falso} y falso.

(22. Regrese F.

Parece bastante complicado, mucho más que una simple envoltura.

En segundo lugar, veamos cómo se implementa en Chrome .

Revisemos el FunctionBindcódigo fuente v8 (motor JavaScript de Chrome):

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 un simple ajuste en muchos casos.


En otra nota, la llamada .bindtambién es ligeramente diferente, las notas de especificación "Los objetos de función creados con Function.prototype.bind no tienen una propiedad de prototipo o el [[Code]], [[FormalParameters]] y [[Scope]] internos propiedades "

Benjamin Gruenbaum
fuente
Si f = g.bind (cosas); ¿Debería f () ser más lento que g (cosas)? Puedo averiguar esto bastante rápido, solo tengo curiosidad si sucede lo mismo cada vez que llamamos a una función, sin importar qué instancia esa función, o si depende de dónde vino esa función.
Paul
4
@Paul Toma mi respuesta con cierto escepticismo. Todo esto podría optimizarse en una versión futura de Chrome (/ V8). Rara vez me he encontrado evitando .binden el navegador, el código legible y comprensible es mucho más importante en la mayoría de los casos. En cuanto a la velocidad de las funciones vinculadas, sí, las funciones vinculadas permanecerán más lentas en este momento , especialmente cuando el thisvalor no se usa en el parcial. Puede ver esto desde el punto de referencia, desde la especificación y / o desde la implementación de forma independiente (punto de referencia) .
Benjamin Gruenbaum
Me pregunto si: 1) algo ha cambiado desde 2013 (ya han pasado dos años) 2) ya que las funciones de flecha tienen este léxico limitado: son funciones de flecha más lentas por diseño.
Kuba Wyrostek
1
@KubaWyrostek 1) No, 2) No, dado que bind no es más lento por diseño, simplemente no se implementa tan rápido. Las funciones de flecha aún no han aterrizado en V8 (aterrizaron y luego se revertieron) cuando lo veremos.
Benjamin Gruenbaum
1
¿Serían más lentas las llamadas futuras a una función a la que ya se ha aplicado "bind"? Es decir, a: function () {}. Bind (this) ... ¿son las futuras llamadas a a () más lentas que si nunca hubiera enlazado en primer lugar?
wayofthefuture
1

Solo quiero dar un poco de perspectiva aquí:

Tenga en cuenta que mientras bind()ing es lento, llamar a las funciones una vez enlazadas no lo es.

Mi código de prueba en Firefox 76.0 en Linux:

//Set it up.
q = function(r, s) {

};
r = {};
s = {};
a = [];
for (let n = 0; n < 1000000; ++n) {
  //Tried all 3 of these.
  //a.push(q);
  //a.push(q.bind(r));
  a.push(q.bind(r, s));
}

//Performance-testing.
s = performance.now();
for (let x of a) {
  x();
}
e = performance.now();
document.body.innerHTML = (e - s);

Entonces, si bien es cierto que .bind()ing puede ser ~ 2 veces más lento que no vinculante (también lo probé), el código anterior toma la misma cantidad de tiempo para los 3 casos (vinculando 0, 1 o 2 variables).


Personalmente, no me importa si el .bind()ing es lento en mi caso de uso actual, me importa el rendimiento del código que se llama una vez que esas variables ya están vinculadas a las funciones.

Andrés
fuente