Tại sao ràng buộc lại chậm hơn đóng cửa?


79

Một người đăng trước đã hỏi Function.bind vs Closure trong Javascript: làm thế nào để chọn?

và đã nhận được câu trả lời này một phần, có vẻ như cho thấy ràng buộc sẽ nhanh hơn so với đóng:

Truyền qua phạm vi có nghĩa là, khi bạn muốn lấy một giá trị (biến, đối tượng) tồn tại trong một phạm vi khác, do đó chi phí bổ sung được thêm vào (mã trở nên chậm hơn để thực thi).

Sử dụng ràng buộc, bạn đang gọi một hàm với một phạm vi hiện có, do đó việc truyền qua phạm vi không diễn ra.

Hai jsperfs gợi ý rằng ràng buộc thực sự chậm hơn rất nhiều so với việc đóng .

Điều này đã được đăng như một bình luận cho ở trên

Và, tôi quyết định viết jsperf của riêng mình

Vậy tại sao liên kết chậm hơn rất nhiều (70 +% trên crôm)?

Vì nó không nhanh hơn và việc đóng lại có thể phục vụ cùng một mục đích, nên tránh ràng buộc?


10
"Nên tránh ràng buộc" --- trừ khi bạn đang làm nó hàng nghìn lần một trang - bạn không nên quan tâm đến nó.
zerkms

1
Việc lắp ráp một nhiệm vụ phức tạp không đồng bộ từ các phần nhỏ có thể yêu cầu một cái gì đó trông giống hệt như vậy, trong nodejs, vì các lệnh gọi lại cần được căn chỉnh bằng cách nào đó.
Paul

Tôi đoán đó là bởi vì các trình duyệt đã không nỗ lực nhiều để tối ưu hóa nó. Xem mã của Mozilla ( developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… ) để triển khai nó theo cách thủ công. Có nhiều khả năng là các trình duyệt chỉ đang thực hiện điều đó trong nội bộ, việc này còn nhiều việc hơn là đóng nhanh.
Dave

1
Các lệnh gọi hàm gián tiếp ( apply/call/bind) nói chung chậm hơn nhiều so với các lệnh gọi hàm trực tiếp.
georg

@zerkms Và ai có thể nói rằng một người không làm điều đó hàng nghìn lần? Do chức năng mà nó cung cấp, tôi nghĩ bạn có thể ngạc nhiên về mức độ phổ biến của điều này.
Andrew

Câu trả lời:


142

Cập nhật Chrome 59: Như tôi đã dự đoán trong câu trả lời bên dưới, ràng buộc không còn chậm hơn với trình biên dịch tối ưu hóa mới. Đây là mã với chi tiết: https://codereview.chromium.org/2916063002/

Hầu hết thời gian nó không quan trọng.

Trừ khi bạn đang tạo một ứng dụng, đâu .bindlà nút thắt cổ chai, tôi sẽ không bận tâm. Khả năng đọc quan trọng hơn nhiều so với hiệu suất tuyệt đối trong hầu hết các trường hợp. Tôi nghĩ rằng sử dụng bản địa .bindthường cung cấp mã dễ đọc và dễ bảo trì hơn - đó là một điểm cộng lớn.

Tuy nhiên, có, khi nó quan trọng - .bindchậm hơn

Có, .bindchậm hơn đáng kể so với quá trình đóng - ít nhất là trong Chrome, ít nhất là theo cách hiện tại mà nó được triển khai v8. Cá nhân tôi đã phải chuyển đổi trong Node.JS vì các vấn đề về hiệu suất một số lần (nói chung, việc đóng cửa hơi chậm trong các tình huống chuyên sâu về hiệu suất).

Tại sao? Bởi vì .bindthuật toán phức tạp hơn rất nhiều so với việc bao hàm một hàm với một hàm khác và sử dụng .callhoặc .apply. (Thật thú vị, nó cũng trả về một hàm với chuỗi to được đặt thành [hàm gốc]).

Có hai cách để xem xét vấn đề này, từ quan điểm đặc tả và từ quan điểm triển khai. Hãy quan sát cả hai.

Đầu tiên, hãy xem thuật toán ràng buộc được xác định trong đặc tả :

  1. Đặt Target là giá trị này.
  2. Nếu IsCallable (Target) là sai, hãy ném một ngoại lệ TypeError.
  3. Cho A là một danh sách nội bộ mới (có thể trống) của tất cả các giá trị đối số được cung cấp sau thisArg (arg1, arg2, v.v.), theo thứ tự.

...

(21. Gọi phương thức bên trong [[DefineOwnProperty]] của F với các đối số là "đối số", PropertyDescriptor {[[Get]]: thrower, [[Set]]: thrower, [[Enumerable]]: false, [[Có thể cấu hình] ]: false} và false.

(22. F trở lại.

Có vẻ khá phức tạp, nhiều hơn chỉ là một cái bọc.

Thứ hai, hãy xem cách nó được triển khai trong Chrome .

Hãy kiểm tra FunctionBindmã nguồn 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;

Chúng ta có thể thấy rất nhiều thứ đắt tiền ở đây trong quá trình thực hiện. Cụ thể là %_IsConstructCall(). Điều này tất nhiên là cần thiết để tuân theo đặc điểm kỹ thuật - nhưng nó cũng làm cho nó chậm hơn so với một gói đơn giản trong nhiều trường hợp.


Một lưu ý khác, cách gọi .bindcũng hơi khác một chút, thông số kỹ thuật ghi chú "Các đối tượng hàm được tạo bằng Function.prototype.bind không có thuộc tính nguyên mẫu hoặc nội bộ [[Code]], [[FormalParameters]] và [[Phạm vi]] tính chất"


Nếu f = g.bind (thứ); f () có nên chậm hơn g (thứ) không? Tôi có thể tìm ra điều này khá nhanh, tôi chỉ tò mò nếu điều tương tự xảy ra mỗi khi chúng ta gọi một hàm bất kể hàm đó được khởi tạo là gì, hoặc nếu nó phụ thuộc vào vị trí của hàm đó.
Paul

4
@Paul Hãy lấy câu trả lời của tôi với một số hoài nghi. Tất cả điều này có thể được tối ưu hóa trong phiên bản tương lai của Chrome (/ V8). Tôi hiếm khi thấy mình phải tránh .bindtrong trình duyệt, mã dễ đọc và dễ hiểu quan trọng hơn nhiều trong hầu hết các trường hợp. Đối với tốc độ của các hàm bị ràng buộc - Có, các hàm bị ràng buộc sẽ chậm hơn tại thời điểm này , đặc biệt là khi thisgiá trị không được sử dụng trong một phần. Bạn có thể thấy điều này từ điểm chuẩn, từ đặc điểm kỹ thuật và / hoặc từ việc triển khai một cách độc lập (điểm chuẩn) .
Benjamin Gruenbaum,

Tôi tự hỏi nếu: 1) bất cứ điều gì đã thay đổi kể từ năm 2013 (đã hai năm rồi) 2) vì các hàm mũi tên có ràng buộc từ vựng này - các hàm mũi tên chậm hơn theo thiết kế.
Kuba Wyrostek

1
@KubaWyrostek 1) Không, 2) Không, vì liên kết không chậm hơn theo thiết kế, nó chỉ được triển khai không nhanh. Các hàm mũi tên vẫn chưa hạ cánh trong V8 (chúng đã hạ cánh và sau đó được hoàn nguyên) khi nào chúng ta sẽ thấy.
Benjamin Gruenbaum

1
Các lệnh gọi trong tương lai tới một hàm đã được áp dụng "ràng buộc" có chậm hơn không? Tức là a: function () {}. Bind (this) ... có phải các lệnh gọi trong tương lai tới a () chậm hơn so với khi tôi chưa bao giờ liên kết ngay từ đầu không?
wayofthefuture

1

Tôi chỉ muốn đưa ra một chút quan điểm ở đây:

Lưu ý rằng mặc dù bind()ing chậm nhưng việc gọi các hàm sau khi bị ràng buộc thì không!

Mã thử nghiệm của tôi trong Firefox 76.0 trên 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);

Vì vậy, mặc dù đúng là .bind()ing có thể chậm hơn ~ 2 lần so với không ràng buộc (tôi cũng đã thử nghiệm điều đó), đoạn mã trên mất cùng một khoảng thời gian cho cả 3 trường hợp (ràng buộc 0, 1 hoặc 2 biến).


Cá nhân tôi không quan tâm nếu .bind()ing chậm trong trường hợp sử dụng hiện tại của tôi, tôi quan tâm đến hiệu suất của mã được gọi khi các biến đó đã được liên kết với các hàm.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.