Hiệu suất của Đối tượng / Mảng trong JavaScript là gì? (đặc biệt cho Google V8)


105

Hiệu suất liên quan đến Mảng và Đối tượng trong JavaScript (đặc biệt là Google V8) sẽ rất thú vị để ghi lại. Tôi không tìm thấy bài viết toàn diện về chủ đề này ở bất kỳ đâu trên Internet.

Tôi hiểu rằng một số Đối tượng sử dụng các lớp làm cấu trúc dữ liệu cơ bản của chúng. Nếu có rất nhiều thuộc tính, đôi khi nó được coi như một bảng băm?

Tôi cũng hiểu rằng Mảng đôi khi được xử lý giống Mảng C ++ (tức là lập chỉ mục ngẫu nhiên nhanh, xóa chậm và thay đổi kích thước). Và, những lần khác, chúng được coi như Đối tượng hơn (lập chỉ mục nhanh, chèn / xóa nhanh, nhiều bộ nhớ hơn). Và, có thể đôi khi chúng được lưu trữ dưới dạng danh sách được liên kết (tức là lập chỉ mục ngẫu nhiên chậm, loại bỏ / chèn nhanh ở đầu / cuối)

Hiệu suất chính xác của các thao tác và truy xuất Mảng / Đối tượng trong JavaScript là gì? (đặc biệt cho Google V8)

Cụ thể hơn, tác động đến hiệu suất của nó là gì:

  • Thêm thuộc tính vào đối tượng
  • Xóa một thuộc tính khỏi một đối tượng
  • Lập chỉ mục một thuộc tính trong một đối tượng
  • Thêm một mục vào một mảng
  • Xóa một mục khỏi Mảng
  • Lập chỉ mục một mục trong Mảng
  • Gọi Array.pop ()
  • Gọi Array.push ()
  • Gọi Array.shift ()
  • Gọi Array.unshift ()
  • Gọi Array.slice ()

Bất kỳ bài báo hoặc liên kết để biết thêm chi tiết cũng sẽ được đánh giá cao. :)

CHỈNH SỬA: Tôi thực sự tự hỏi làm thế nào các mảng và đối tượng JavaScript hoạt động dưới mui xe. Ngoài ra, động cơ V8 "biết" để "chuyển đổi" sang cấu trúc dữ liệu khác trong ngữ cảnh nào?

Ví dụ: giả sử tôi tạo một mảng với ...

var arr = [];
arr[10000000] = 20;
arr.push(21);

Điều gì thực sự đang xảy ra ở đây?

Hay ... cái này thì sao ... ???

var arr = [];
//Add lots of items
for(var i = 0; i < 1000000; i++)
    arr[i] = Math.random();
//Now I use it like a queue...
for(var i = 0; i < arr.length; i++)
{
    var item = arr[i].shift();
    //Do something with item...
}

Đối với các mảng thông thường, hiệu suất sẽ rất khủng khiếp; trong khi đó, nếu một LinkedList được sử dụng ... không quá tệ.


2
Truy cập jsperf.com và tạo các trường hợp thử nghiệm.
Rob W

2
@RobW Ở đây có nhiều điều hơn là các bài kiểm tra đơn giản có thể giải thích, đòi hỏi kiến ​​thức về cách hoạt động của trình biên dịch JIT và những gì đang được thực hiện với dữ liệu. Nếu tôi tìm thấy một chút thời gian, tôi sẽ thêm câu trả lời, nhưng hy vọng ai đó sẽ có thời gian để đi sâu vào vấn đề. Ngoài ra, tôi chỉ muốn để lại liên kết này ở đây
Incognito

Những thứ JIT tôi đang nói đến là những thứ như "hình dạng" của một đối tượng hoặc các mảng có giá trị không xác định giữa các phần tử được xác định, cũng như các tính năng chuyên về kiểu được thử nghiệm gần đây ... các phương pháp dành riêng cho mảng có thể tùy thuộc vào việc sử dụng như cũng như nguyên mẫu đã được chế tác hay chưa. Không có cái gọi là "biết để" chuyển sang kiểu dữ liệu khác AFAIK.
Ẩn danh

1
Có đại diện của Google thảo luận về cách hoạt động của các trình tối ưu hóa khác nhau và hệ thống nội bộ. Và làm thế nào để tối ưu hóa cho chúng. (dành cho trò chơi!) youtube.com/watch?v=XAqIpGU8ZZk
PicoCreator

Câu trả lời:


279

Tôi đã tạo một bộ thử nghiệm, chính xác là để khám phá những vấn đề này (và hơn thế nữa) ( bản sao lưu trữ ).

Và theo nghĩa đó, bạn có thể thấy các vấn đề về hiệu suất trong hơn 50 trình kiểm tra trường hợp thử nghiệm này (sẽ mất nhiều thời gian).

Cũng như tên gọi của nó, nó khám phá cách sử dụng bản chất danh sách liên kết gốc của cấu trúc DOM.

(Hiện đang ngừng hoạt động, đang được xây dựng lại) Thêm chi tiết trên blog của tôi về điều này .

Tóm tắt như sau

  • Mảng V8 nhanh, RẤT NHANH
  • Đẩy / bật / chuyển mảng nhanh hơn khoảng 20 lần so với bất kỳ đối tượng nào tương đương.
  • Thật ngạc nhiên Array.shift() là nhanh hơn ~ khoảng 6 lần so với cửa sổ bật lên mảng, nhưng nhanh hơn khoảng 100 lần so với xóa thuộc tính đối tượng.
  • Thật thú vị, Array.push( data );nhanh hơn Array[nextIndex] = datagần 20 lần (mảng động) đến 10 lần (mảng cố định).
  • Array.unshift(data) chậm hơn như mong đợi và chậm hơn khoảng 5x so với việc thêm thuộc tính mới.
  • Nulling giá trị array[index] = nullnhanh hơn xóa nó delete array[index](không xác định) trong một mảng nhanh hơn khoảng 4x ++.
  • Đáng ngạc nhiên là Nulling một giá trị trong một đối tượng obj[attr] = nullchậm hơn khoảng 2 lần so với chỉ xóa thuộc tínhdelete obj[attr]
  • Không có gì ngạc nhiên khi mảng giữa Array.splice(index,0,data)chậm, rất chậm.
  • Đáng ngạc nhiên, Array.splice(index,1,data)đã được tối ưu hóa (không thay đổi độ dài) và nhanh hơn 100 lần so với chỉ mối nốiArray.splice(index,0,data)
  • không có gì đáng ngạc nhiên, divLinkedList kém hơn một mảng trên tất cả các lĩnh vực, ngoại trừ dll.splice(index,1) loại bỏ (Nơi nó đã phá vỡ hệ thống thử nghiệm).
  • LỚN NHẤT trong số đó [như jjrv đã chỉ ra], ghi mảng V8 nhanh hơn một chút so với đọc V8 = O

Lưu ý: Các chỉ số này chỉ áp dụng cho mảng / đối tượng lớn mà v8 không "tối ưu hóa hoàn toàn". Có thể có các trường hợp hiệu suất được tối ưu hóa rất cô lập cho kích thước mảng / đối tượng nhỏ hơn kích thước tùy ý (24?). Các chi tiết khác có thể được nhìn thấy rộng rãi trên một số video IO của google.

Lưu ý 2: Những kết quả hiệu suất tuyệt vời này không được chia sẻ trên các trình duyệt, đặc biệt là *cough*IE. Ngoài ra, bài kiểm tra rất lớn, do đó tôi chưa phân tích và đánh giá đầy đủ kết quả: vui lòng chỉnh sửa nó trong =)

Lưu ý cập nhật (tháng 12 năm 2012): Đại diện của Google có video trên youtuber mô tả hoạt động bên trong của chính chrome (như khi nó chuyển từ mảng danh sách liên kết sang mảng cố định, v.v.) và cách tối ưu hóa chúng. Xem GDC 2012: Từ Bảng điều khiển đến Chrome để biết thêm.


2
Một số kết quả trông rất kỳ lạ. Ví dụ, trong Chrome, ghi mảng nhanh hơn khoảng 10 lần so với đọc, trong khi trong Firefox thì ngược lại. Bạn có chắc chắn rằng trình duyệt JIT không tối ưu hóa toàn bộ thử nghiệm của bạn trong một số trường hợp không?
jjrv

1
@jjrv good gosh = O bạn nói đúng ... Tôi thậm chí đã cập nhật từng trường hợp ghi để tăng dần độ độc đáo, để ngăn chặn JIT ... Và thành thật mà nói, trừ khi tối ưu hóa JIT tốt đến mức đó (điều mà tôi cảm thấy khó tin), nó có thể chỉ là một trường hợp đọc được tối ưu hóa kém hoặc ghi được tối ưu hóa nhiều (ghi vào bộ đệm ngay lập tức?) ... điều này đáng để điều tra: lol
PicoCreator

2
chỉ muốn thêm điểm chính xác trong các cuộc thảo luận video trên mảng: youtube.com/...
badunk

1
Trang web JsPerf không tồn tại nữa :(
JustGoscha

1
@JustGoscha ok, thx cho thông tin: Tôi đã sửa nó sao lưu bằng cách tạo lại nó từ bộ nhớ cache của google.
PicoCreator

5

Ở cấp độ cơ bản nằm trong lĩnh vực JavaScript, các thuộc tính trên các đối tượng là các thực thể phức tạp hơn nhiều. Bạn có thể tạo thuộc tính với setters / getters, với khả năng liệt kê, khả năng ghi và khả năng cấu hình khác nhau. Một mục trong một mảng không thể được tùy chỉnh theo cách này: nó tồn tại hoặc không. Ở cấp công cụ cơ bản, điều này cho phép tối ưu hóa hơn nhiều về mặt tổ chức bộ nhớ đại diện cho cấu trúc.

Về mặt xác định một mảng từ một đối tượng (từ điển), các công cụ JS luôn tạo ra các đường rõ ràng giữa hai đối tượng. Đó là lý do tại sao có vô số bài viết về các phương pháp cố gắng tạo một đối tượng giống Array bán giả mạo hoạt động giống như một đối tượng nhưng cho phép chức năng khác. Lý do mà sự tách biệt này thậm chí còn tồn tại là vì bản thân các công cụ JS lưu trữ hai thứ khác nhau.

Các thuộc tính có thể được lưu trữ trên một đối tượng mảng nhưng điều này chỉ đơn giản là chứng minh cách JavaScript kiên quyết biến mọi thứ trở thành đối tượng. Các giá trị được lập chỉ mục trong một mảng được lưu trữ khác với bất kỳ thuộc tính nào bạn quyết định đặt trên đối tượng mảng đại diện cho dữ liệu mảng bên dưới.

Bất cứ khi nào bạn đang sử dụng một đối tượng mảng hợp pháp và sử dụng một trong những phương pháp chuẩn để thao tác với mảng đó, bạn sẽ đánh vào dữ liệu mảng bên dưới. Cụ thể trong V8, chúng về cơ bản giống như một mảng C ++ nên những quy tắc đó sẽ được áp dụng. Nếu vì lý do nào đó, bạn đang làm việc với một mảng mà công cụ không thể xác định chắc chắn là một mảng, thì bạn đang ở trên một mặt bằng tồi tệ hơn nhiều. Với các phiên bản V8 gần đây, có nhiều chỗ hơn để hoạt động. Ví dụ: có thể tạo một lớp có Array.prototype làm nguyên mẫu của nó và vẫn có quyền truy cập hiệu quả vào các phương thức thao tác mảng gốc khác nhau. Nhưng đây là một thay đổi gần đây.

Các liên kết cụ thể đến những thay đổi gần đây đối với thao tác mảng có thể hữu ích tại đây:

Ngoài ra, đây là Array Pop và Array Push trực tiếp từ nguồn của V8, cả hai đều được triển khai trong chính JS:

function ArrayPop() {
  if (IS_NULL_OR_UNDEFINED(this) && !IS_UNDETECTABLE(this)) {
    throw MakeTypeError("called_on_null_or_undefined",
                        ["Array.prototype.pop"]);
  }

  var n = TO_UINT32(this.length);
  if (n == 0) {
    this.length = n;
    return;
  }
  n--;
  var value = this[n];
  this.length = n;
  delete this[n];
  return value;
}


function ArrayPush() {
  if (IS_NULL_OR_UNDEFINED(this) && !IS_UNDETECTABLE(this)) {
    throw MakeTypeError("called_on_null_or_undefined",
                        ["Array.prototype.push"]);
  }

  var n = TO_UINT32(this.length);
  var m = %_ArgumentsLength();
  for (var i = 0; i < m; i++) {
    this[i+n] = %_Arguments(i);
  }
  this.length = n + m;
  return this.length;
}

1

Tôi muốn bổ sung các câu trả lời hiện có bằng một cuộc điều tra về câu hỏi cách triển khai hoạt động như thế nào đối với các mảng đang phát triển: Nếu chúng triển khai chúng theo cách "thông thường", người ta sẽ thấy nhiều lần đẩy nhanh với các lần đẩy chậm hiếm hoi xen kẽ tại thời điểm triển khai sao chép biểu diễn bên trong của mảng từ một bộ đệm đến một bộ đệm lớn hơn.

Bạn có thể thấy hiệu ứng này rất độc đáo, đây là từ Chrome:

16: 4ms
40: 8ms 2.5
76: 20ms 1.9
130: 31ms 1.7105263157894737
211: 14ms 1.623076923076923
332: 55ms 1.5734597156398105
514: 44ms 1.5481927710843373
787: 61ms 1.5311284046692606
1196: 138ms 1.5196950444726811
1810: 139ms 1.5133779264214047
2731: 299ms 1.5088397790055248
4112: 341ms 1.5056755767118273
6184: 681ms 1.5038910505836576
9292: 1324ms 1.5025873221216042

Mặc dù mỗi lần đẩy đều được định hình, đầu ra chỉ chứa những lần đẩy có thời gian trên một ngưỡng nhất định. Đối với mỗi thử nghiệm, tôi đã tùy chỉnh ngưỡng để loại trừ tất cả các lần đẩy dường như đại diện cho các lần đẩy nhanh.

Vì vậy, số đầu tiên đại diện cho phần tử nào đã được chèn (dòng đầu tiên là phần tử thứ 17), số thứ hai là thời gian mất bao lâu (đối với nhiều mảng, điểm chuẩn được thực hiện song song) và giá trị cuối cùng là phép chia của số đầu tiên bằng số của một trong những dòng cũ.

Tất cả các dòng có thời gian thực thi dưới 2ms đều bị loại trừ đối với Chrome.

Bạn có thể thấy rằng Chrome tăng kích thước mảng theo lũy thừa 1,5, cộng với một số bù đắp để tính cho các mảng nhỏ.

Đối với Firefox, đó là sức mạnh của hai:

126: 284ms
254: 65ms 2.015873015873016
510: 28ms 2.0078740157480315
1022: 58ms 2.003921568627451
2046: 89ms 2.0019569471624266
4094: 191ms 2.0009775171065494
8190: 364ms 2.0004885197850513

Tôi đã phải đặt ngưỡng này lên khá nhiều trong Firefox, đó là lý do tại sao chúng tôi bắt đầu ở vị trí # 126.

Với IE, chúng tôi nhận được sự kết hợp:

256: 11ms 256
512: 26ms 2
1024: 77ms 2
1708: 113ms 1.66796875
2848: 154ms 1.6674473067915691
4748: 423ms 1.6671348314606742
7916: 944ms 1.6672283066554338

Lúc đầu nó là lũy thừa của hai và sau đó chuyển sang lũy ​​thừa của năm phần ba.

Vì vậy, tất cả các triển khai thông thường sử dụng cách "bình thường" cho các mảng ( ví dụ: thay vì điên cuồng với dây thừng ).

Đây là mã điểm chuẩn và đây là cơ hội mà nó có.

var arrayCount = 10000;

var dynamicArrays = [];

for(var j=0;j<arrayCount;j++)
    dynamicArrays[j] = [];

var lastLongI = 1;

for(var i=0;i<10000;i++)
{
    var before = Date.now();
    for(var j=0;j<arrayCount;j++)
        dynamicArrays[j][i] = i;
    var span = Date.now() - before;
    if (span > 10)
    {
      console.log(i + ": " + span + "ms" + " " + (i / lastLongI));
      lastLongI = i;
    }
}

0

Trong khi chạy dưới node.js 0.10 (được xây dựng trên v8), tôi thấy việc sử dụng CPU dường như quá mức đối với khối lượng công việc. Tôi đã tìm ra một vấn đề hiệu suất cho một hàm đang kiểm tra sự tồn tại của một chuỗi trong một mảng. Vì vậy, tôi đã chạy một số thử nghiệm.

  • đã tải 90.822 máy chủ
  • tải cấu hình mất 0,087 giây (mảng)
  • tải cấu hình mất 0,152 giây (đối tượng)

Tải 91k mục nhập vào một mảng (với xác thực và đẩy) nhanh hơn so với đặt giá trị obj [key] =.

Trong thử nghiệm tiếp theo, tôi đã tra cứu mọi tên máy chủ trong danh sách một lần (91 nghìn lần lặp, để tính trung bình thời gian tra cứu):

  • tìm kiếm cấu hình mất 87,56 giây (mảng)
  • tìm kiếm cấu hình mất 0,21 giây (đối tượng)

Ứng dụng ở đây là Haraka (một máy chủ SMTP) và nó tải host_list một lần khi khởi động (và sau khi thay đổi) và sau đó thực hiện tra cứu này hàng triệu lần trong quá trình hoạt động. Chuyển sang một đối tượng là một chiến thắng hiệu suất rất lớn.

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.