[javascript] 바인딩이 클로저보다 느린 이유는 무엇입니까?

이전 포스터 에서 Javascript의 Function.bind vs Closure : 선택 방법을 물었습니다 .

그리고 부분적 으로이 답변을 받았습니다. 바인드가 클로저보다 빠르다는 것을 나타냅니다.

범위 순회는 다른 범위에있는 값 (변수, 객체)을 잡기 위해 도달 할 때 추가 오버 헤드가 추가되는 것을 의미합니다 (코드 실행 속도가 느려짐).

bind를 사용하면 기존 범위로 함수를 호출하므로 범위 순회가 발생하지 않습니다.

두 개의 jsperfs는 bind가 실제로 클로저 보다 훨씬 느리다는 것을 암시합니다 .

이것은 위의 코멘트로 게시되었습니다

그리고 저는 제 jsperf 를 작성하기로 결정했습니다.

그렇다면 왜 바인딩이 훨씬 느릴까요 (크롬에서 70 + %)?

더 빠르지 않고 클로저가 동일한 목적을 수행 할 수 있으므로 바인딩을 피해야합니까?



답변

Chrome 59 업데이트 : 아래 답변에서 예상했듯이 새로운 최적화 컴파일러를 사용하면 bind가 더 이상 느려지지 않습니다. 세부 정보가있는 코드는 다음과 같습니다. https://codereview.chromium.org/2916063002/

대부분의 경우 중요하지 않습니다.

.bind병목 현상이 있는 응용 프로그램을 만드는 경우 가 아니라면 신경 쓰지 않을 것입니다. 가독성은 대부분의 경우 순수한 성능보다 훨씬 더 중요합니다. 네이티브를 사용하면 .bind일반적으로 더 읽기 쉽고 유지 관리가 가능한 코드가 제공 된다고 생각합니다 . 이는 큰 장점입니다.

그러나 예, 중요한 경우- .bind더 느립니다.

예, .bind클로저보다 상당히 느립니다. 적어도 Chrome에서는 v8. 개인적으로 성능 문제 때문에 Node.JS로 전환해야했습니다 (보다 일반적으로 성능 집약적 인 상황에서는 클로저가 다소 느립니다).

왜? 때문에 .bind알고리즘은 더 많은 다른 기능을 갖는 기능을 배치하고 사용하는 것보다 복잡 .call하거나 .apply. (재미있는 사실은 toString이 [native function]으로 설정된 함수도 반환합니다.)

스펙 관점과 구현 관점에서이를 보는 방법에는 두 가지가 있습니다. 둘 다 관찰합시다.

먼저 사양에 정의 된 바인딩 알고리즘을 살펴 보겠습니다 .

  1. Target을 this 값으로 둡니다.
  2. IsCallable (Target)이 false이면 TypeError 예외를 발생시킵니다.
  3. A를 thisArg (arg1, arg2 등) 다음에 제공되는 모든 인수 값의 새로운 (비어있을 수 있음) 내부 목록을 순서대로 지정합니다.

(21. 인수 “arguments”, PropertyDescriptor {[[Get]] : thrower, [[Set]] : thrower, [[Enumerable]] : false, [[Configurable])를 사용하여 F의 [[DefineOwnProperty]] 내부 메서드 호출) ] : false} 및 false.

(22. 반환 F.

랩보다 훨씬 복잡해 보입니다.

둘째, Chrome에서 어떻게 구현되는지 살펴 보겠습니다 .

FunctionBindv8 (Chrome JavaScript 엔진) 소스 코드를 확인해 보겠습니다 .

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;

여기 구현에서 많은 값 비싼 것들을 볼 수 있습니다. 즉 %_IsConstructCall(). 이것은 물론 사양을 준수하는 데 필요하지만 많은 경우 간단한 랩보다 느리게 만듭니다.


또 다른 메모에서 호출 .bind도 약간 다릅니다. 사양에 “Function.prototype.bind를 사용하여 생성 된 함수 개체에는 프로토 타입 속성이 없거나 [[Code]], [[FormalParameters]] 및 [[Scope]] 내부 속성 “


답변

여기에 약간의 관점을 제공하고 싶습니다.

반면 있습니다 bind()ING가 느린, 전화 결합이 아닌 한 번 기능을!

Linux에서 Firefox 76.0의 내 테스트 코드 :

//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);

따라서 .bind()ing이 바인딩하지 않는 것보다 약 2 배 더 느릴 수 있다는 것은 사실이지만 (저도 테스트했습니다) 위 코드는 3 가지 경우 (0, 1 또는 2 개의 변수 바인딩)에 대해 동일한 시간이 걸립니다.


개인적으로 .bind()현재 사용 사례에서 ing이 느린 지 여부는 신경 쓰지 않고 해당 변수가 이미 함수에 바인딩되면 호출되는 코드의 성능에 관심이 있습니다.


답변