Tại sao hàm sau nhanh hơn 10% mặc dù nó phải tạo các biến nhiều lần?


14
var toSizeString = (function() {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

  return function(size) {
    var gbSize = size / GB,
        gbMod  = size % GB,
        mbSize = gbMod / MB,
        mbMod  = gbMod % MB,
        kbSize = mbMod / KB;

    if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
    } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
    } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
    } else {
      return size + 'B';
    }
  };
})();

Và hàm nhanh hơn: (lưu ý rằng nó phải luôn luôn tính toán các biến giống nhau kb / mb / gb nhiều lần). Nó đạt được hiệu suất ở đâu?

function toSizeString (size) {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

 var gbSize = size / GB,
     gbMod  = size % GB,
     mbSize = gbMod / MB,
     mbMod  = gbMod % MB,
     kbSize = mbMod / KB;

 if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
 } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
 } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
 } else {
      return size + 'B';
 }
};

3
Trong bất kỳ ngôn ngữ gõ tĩnh nào, "biến" sẽ được biên dịch thành hằng số. Có thể các công cụ JS hiện đại có khả năng thực hiện tối ưu hóa tương tự. Điều này dường như không hoạt động nếu các biến là một phần của việc đóng cửa.
usr

6
đây là chi tiết triển khai của công cụ JavaScript mà bạn đang sử dụng. Thời gian và không gian lý thuyết là như nhau, chỉ có việc triển khai một công cụ JavaScript nhất định sẽ thay đổi những điều này. Vì vậy, để trả lời chính xác câu hỏi của bạn, bạn cần liệt kê công cụ JavaScript cụ thể mà bạn đã đo. Có lẽ ai đó biết chi tiết về việc triển khai để nói cách thức / lý do tại sao nó làm cho cái này tối ưu hơn cái kia. Ngoài ra, bạn nên đăng mã đo lường của bạn.
Jimmy Hoffa

bạn sử dụng từ "tính toán" để chỉ các giá trị không đổi; thực sự không có gì để tính toán ở đó trong những gì bạn đang tham khảo. Số học của các giá trị không đổi là một trong những trình biên dịch tối ưu hóa đơn giản và rõ ràng nhất, vì vậy bất cứ khi nào bạn thấy một biểu thức chỉ có giá trị không đổi, bạn có thể giả sử toàn bộ biểu thức được tối ưu hóa thành một giá trị không đổi duy nhất.
Jimmy Hoffa

@JimmyHoffa điều đó đúng, nhưng mặt khác, nó cần tạo 3 biến không đổi mỗi hàm gọi ...
Tomy

@Tomy hằng không biến. Chúng không thay đổi, do đó chúng không cần phải được tạo lại sau khi biên dịch. Một hằng số thường được đặt trong bộ nhớ và mọi tầm với trong tương lai của hằng số đó được hướng đến cùng một vị trí, không cần phải tạo lại nó bởi vì giá trị của nó sẽ không bao giờ thay đổi , do đó nó không phải là một biến. Trình biên dịch nói chung sẽ không phát ra mã tạo ra các hằng số, trình biên dịch thực hiện việc tạo và nó hướng tất cả các tham chiếu mã đến những gì nó tạo ra.
Jimmy Hoffa

Câu trả lời:


23

Tất cả các công cụ JavaScript hiện đại đều thực hiện việc biên dịch đúng lúc. Bạn không thể đưa ra bất kỳ giả định nào về những gì nó "phải tạo ra nhiều lần." Loại tính toán này tương đối dễ dàng để tối ưu hóa, trong cả hai trường hợp.

Mặt khác, việc đóng các biến không đổi không phải là trường hợp điển hình mà bạn sẽ nhắm mục tiêu biên dịch JIT. Bạn thường tạo một bao đóng khi bạn muốn có thể thay đổi các biến đó trên các lệnh khác nhau. Bạn cũng đang tạo một bổ sung con trỏ bổ sung để truy cập các biến đó, như sự khác biệt giữa truy cập một biến thành viên và int cục bộ trong OOP.

Loại tình huống này là lý do tại sao mọi người ném ra dòng "tối ưu hóa sớm". Việc tối ưu hóa dễ dàng đã được thực hiện bởi trình biên dịch.


Tôi nghi ngờ rằng phạm vi truyền tải cho độ phân giải thay đổi gây ra tổn thất như bạn đề cập. Có vẻ hợp lý, nhưng ai thực sự biết sự điên rồ nào nằm trong công cụ JIT JavaScript ...
Jimmy Hoffa

1
Có thể mở rộng câu trả lời này: lý do JIT sẽ bỏ qua tối ưu hóa dễ dàng cho trình biên dịch ngoại tuyến là vì hiệu suất của toàn bộ trình biên dịch quan trọng hơn các trường hợp bất thường.
Leushenko 15/03/2015

12

Biến có giá rẻ. Bối cảnh thực hiện và chuỗi phạm vi là đắt tiền.

Có nhiều câu trả lời khác nhau về cơ bản là "vì đóng cửa" và về cơ bản là đúng, nhưng vấn đề không phải là cụ thể với việc đóng cửa, thực tế là bạn có một hàm tham chiếu các biến trong một phạm vi khác. Bạn sẽ phải cùng một vấn đề nếu đây là những biến toàn cục trên windowđối tượng, như trái ngược với các biến cục bộ bên trong IIFE. Hãy thử nó và xem.

Vì vậy, trong chức năng đầu tiên của bạn, khi động cơ nhìn thấy tuyên bố này:

var gbSize = size / GB;

Nó phải thực hiện các bước sau:

  1. Tìm kiếm một biến sizetrong phạm vi hiện tại. (Tìm thấy rồi.)
  2. Tìm kiếm một biến GBtrong phạm vi hiện tại. (Không tìm thấy.)
  3. Tìm kiếm một biến GBtrong phạm vi cha. (Tìm thấy rồi.)
  4. Làm phép tính và gán cho gbSize.

Bước 3 đắt hơn đáng kể so với việc chỉ phân bổ một biến. Hơn nữa, bạn làm điều này năm lần , bao gồm hai lần cho cả hai GBMB. Tôi nghi ngờ rằng nếu bạn đặt bí danh cho chúng ở đầu hàm (ví dụ var gb = GB) và tham chiếu bí danh thay vào đó, nó thực sự sẽ tạo ra một tốc độ nhỏ, mặc dù cũng có thể một số công cụ JS đã thực hiện tối ưu hóa này. Và tất nhiên, cách hiệu quả nhất để tăng tốc độ thực thi chỉ đơn giản là không đi qua chuỗi phạm vi.

Hãy nhớ rằng JavaScript không giống như một ngôn ngữ được biên dịch, nhập tĩnh, trong đó trình biên dịch giải quyết các địa chỉ biến này tại thời gian biên dịch. Công cụ JS phải giải quyết chúng theo tên và các lần tra cứu này xảy ra trong thời gian chạy, mọi lúc. Vì vậy, bạn muốn tránh chúng khi có thể.

Chuyển nhượng biến cực kỳ rẻ trong JavaScript. Nó thực sự có thể là hoạt động rẻ nhất, mặc dù tôi không có gì để sao lưu tuyên bố đó. Tuy nhiên, thật an toàn khi nói rằng hầu như không bao giờ nên cố gắng tránh tạo biến; hầu như bất kỳ tối ưu hóa nào bạn cố gắng thực hiện trong lĩnh vực đó thực sự sẽ kết thúc làm cho mọi thứ trở nên tồi tệ hơn, hiệu quả hơn.


Và ngay cả khi "tối ưu hóa" không ảnh hưởng tiêu cực đến hiệu suất, thì gần như chắc chắn sẽ ảnh hưởng tiêu cực đến khả năng đọc mã. Mà, trừ khi bạn đang làm một số công cụ tính toán điên rồ, thường là một sự đánh đổi tồi tệ (dường như không có neo permalink nào đáng tiếc; tìm kiếm cho "2009/02/17 11:41"). Như tóm tắt: "Chọn sự rõ ràng về tốc độ, nếu tốc độ không thực sự cần thiết."
một CVn

Ngay cả khi viết trình thông dịch rất cơ bản cho các ngôn ngữ động, truy cập biến trong thời gian chạy có xu hướng là thao tác O (1) và truyền tải phạm vi O (n) thậm chí không cần thiết trong quá trình biên dịch ban đầu. Trong mỗi phạm vi, mỗi biến được khai báo mới được gán một số, do đó var a, b, cchúng ta có thể truy cập bdưới dạng scope[1]. Tất cả các phạm vi được đánh số và nếu phạm vi này được lồng sâu năm phạm vi, thì bsẽ được giải quyết đầy đủ bằng cách env[5][1]biết trong quá trình phân tích cú pháp. Trong mã gốc, phạm vi tương ứng với các phân đoạn ngăn xếp. Việc đóng cửa phức tạp hơn vì họ phải sao lưu và thay thếenv
amon

@amon: Đó có thể là cách bạn muốn lý tưởng như nó để làm việc, nhưng nó không phải là cách nó thực sự hoạt động. Những người hiểu biết và có nhiều kinh nghiệm hơn tôi đã viết sách về điều này; đặc biệt tôi sẽ hướng bạn đến JavaScript hiệu suất cao của Nicholas C. Zakas. Đây là một đoạn trích , và anh ấy cũng đã nói chuyện với điểm chuẩn để sao lưu nó. Tất nhiên anh ấy chắc chắn không phải là người duy nhất, chỉ là người nổi tiếng nhất. JavaScript có phạm vi từ vựng, vì vậy các bao đóng thực sự không thực sự đặc biệt - về cơ bản, mọi thứ đều là một bao đóng.
Aaronaught 17/03/2015

@Aaronaught Thú vị. Vì cuốn sách này đã được 5 tuổi, tôi đã quan tâm đến cách một công cụ JS hiện tại xử lý các tra cứu biến đổi và xem xét phụ trợ x64 của động cơ V8. Trong quá trình phân tích tĩnh, hầu hết các biến được giải quyết tĩnh và được gán một bộ nhớ bù trong phạm vi của chúng. Phạm vi chức năng được biểu diễn dưới dạng danh sách được liên kết và lắp ráp được phát ra dưới dạng một vòng lặp không được kiểm soát để đạt được phạm vi chính xác. Ở đây, chúng tôi sẽ nhận được mã C tương đương với *(scope->outer + variable_offset)quyền truy cập; mỗi cấp độ phạm vi chức năng bổ sung chi phí một bổ sung con trỏ bổ sung. Có vẻ như cả hai chúng tôi đều đúng :)
amon

2

Một ví dụ liên quan đến việc đóng cửa, cái kia thì không. Việc thực hiện đóng là khá khó khăn, vì đóng trên các biến không hoạt động như các biến thông thường. Điều này rõ ràng hơn trong một ngôn ngữ cấp thấp như C, nhưng tôi sẽ sử dụng JavaScript để minh họa điều này.

Một bao đóng không chỉ bao gồm một hàm, mà còn bao gồm tất cả các biến mà nó đóng. Khi chúng ta muốn gọi hàm đó, chúng ta cũng cần cung cấp tất cả các biến đóng. Chúng ta có thể mô hình hóa một bao đóng bởi một hàm nhận một đối tượng là đối số đầu tiên đại diện cho các biến đóng này:

function add(vars, y) {
  vars.x += y;
}

function getSum(vars) {
  return vars.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(adder, 2);
console.log(adder.getSum(adder));  //=> 42

Lưu ý quy ước gọi khó xử closure.apply(closure, ...realArgs)này yêu cầu

Hỗ trợ đối tượng dựng sẵn của JavaScript cho phép bỏ qua varsđối số rõ ràng và thisthay vào đó cho phép chúng tôi sử dụng :

function add(y) {
  this.x += y;
}

function getSum() {
  return this.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

Những ví dụ này tương đương với mã này thực sự sử dụng các bao đóng:

function makeAdder(x) {
  return {
    add: function (y) { x += y },
    getSum: function () { return x },
  };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

Trong ví dụ cuối cùng này, đối tượng chỉ được sử dụng để nhóm hai hàm được trả về; các thisràng buộc là không liên quan. Tất cả các chi tiết về việc đóng cửa có thể - chuyển dữ liệu ẩn sang hàm thực tế, thay đổi tất cả các truy cập thành biến đóng để tra cứu trong dữ liệu ẩn đó - được ngôn ngữ quan tâm.

Nhưng việc đóng các cuộc gọi liên quan đến chi phí truyền dữ liệu bổ sung đó và việc đóng cửa liên quan đến chi phí tìm kiếm trong dữ liệu bổ sung đó - trở nên tồi tệ hơn bởi địa phương bộ đệm xấu và thường là một con trỏ khi so sánh với các biến thông thường - vì vậy không có gì đáng ngạc nhiên một giải pháp không dựa vào việc đóng cửa thực hiện tốt hơn. Đặc biệt là tất cả mọi thứ mà việc đóng cửa của bạn giúp bạn thực hiện là một vài thao tác số học cực kỳ rẻ, thậm chí có thể được gấp lại liên tục trong quá trình phân tích cú pháp.

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.