Sự bất biến có ảnh hưởng đến hiệu suất trong JavaScript không?


88

Dường như có một xu hướng gần đây trong JavaScript đối với việc coi các cấu trúc dữ liệu là bất biến. Ví dụ: nếu bạn cần thay đổi một thuộc tính của một đối tượng, tốt hơn là chỉ tạo một đối tượng hoàn toàn mới với thuộc tính mới và chỉ sao chép tất cả các thuộc tính khác từ đối tượng cũ và để đối tượng cũ được thu gom rác. (Dù sao đó cũng là sự hiểu biết của tôi.)

Phản ứng ban đầu của tôi là, có vẻ như nó sẽ không tốt cho hiệu suất.

Nhưng sau đó, các thư viện như Immutable.jsRedux.js được viết bởi những người thông minh hơn tôi và dường như có mối quan tâm lớn về hiệu suất, vì vậy tôi tự hỏi liệu sự hiểu biết của tôi về rác (và tác động hiệu suất của nó) có sai không.

Có những lợi ích về hiệu suất đối với tính bất biến mà tôi đang thiếu, và chúng có vượt trội hơn những nhược điểm của việc tạo ra quá nhiều rác không?


8
Họ có một mối quan tâm mạnh mẽ đối với hiệu suất một phần vì tính bất biến (đôi khi) có chi phí hiệu suất và họ muốn giảm thiểu chi phí hiệu suất đó càng nhiều càng tốt. Tính không thay đổi, tự nó, chỉ có lợi ích hiệu suất theo nghĩa là nó giúp viết mã đa luồng dễ dàng hơn.
Robert Harvey

8
Theo kinh nghiệm của tôi, hiệu suất chỉ là mối quan tâm hợp lệ đối với hai kịch bản - một, khi một hành động được thực hiện hơn 30 lần trong một giây và hai - khi hiệu ứng của nó tăng lên với mỗi lần thực thi (Windows XP từng phát hiện ra lỗi trong đó Windows Update mất thời gian O(pow(n, 2))cho mọi cập nhật trong lịch sử của nó .) Hầu hết các mã khác là phản hồi ngay lập tức cho một sự kiện; một lần nhấp, yêu cầu API hoặc tương tự và miễn là thời gian thực hiện là không đổi, việc dọn dẹp bất kỳ số lượng đối tượng nào hầu như không thành vấn đề.
Katana314

4
Ngoài ra, xem xét rằng có tồn tại hiệu quả của các cấu trúc dữ liệu bất biến. Có thể những thứ này không hiệu quả như những thứ có thể thay đổi, nhưng có lẽ vẫn hiệu quả hơn một cách thực hiện ngây thơ. Xem ví dụ Cấu trúc dữ liệu chức năng thuần túy của Chris Okasaki
Giorgio

1
@ Katana314: Hơn 30 lần đối với tôi vẫn không đủ để biện minh cho việc lo lắng về hiệu suất. Tôi đã chuyển một trình giả lập CPU nhỏ mà tôi đã viết cho node.js và nút thực thi CPU ảo ở khoảng 20 MHz (tức là 20 triệu lần mỗi giây). Vì vậy, tôi chỉ lo lắng về hiệu suất nếu tôi đang làm gì đó hơn 1000 lần mỗi giây (thậm chí sau đó, tôi sẽ không thực sự lo lắng cho đến khi tôi thực hiện 1000000 thao tác mỗi giây vì tôi biết tôi có thể thoải mái thực hiện hơn 10 trong số chúng cùng một lúc) .
slebetman

2
@RobertHarvey "Bản thân tính bất biến, chỉ có lợi ích hiệu năng theo nghĩa là nó giúp việc viết mã đa luồng dễ dàng hơn." Điều đó không hoàn toàn đúng, sự bất biến cho phép chia sẻ rất phổ biến mà không có hậu quả thực sự. Điều này rất không an toàn trong một môi trường đột biến. Điều này cho bạn suy nghĩ như O(1)cắt mảng và O(log n)chèn vào cây nhị phân trong khi vẫn có thể sử dụng cây cũ một cách tự do, và một ví dụ khác là tailstất cả các đuôi của danh sách tails [1, 2] = [[1, 2], [2], []]chỉ mất O(n)thời gian và không gian, nhưng nằm O(n^2)trong phần tử
dấu chấm phẩy

Câu trả lời:


59

Ví dụ: nếu bạn cần thay đổi một thuộc tính của một đối tượng, tốt hơn là chỉ tạo một đối tượng hoàn toàn mới với thuộc tính mới và chỉ sao chép tất cả các thuộc tính khác từ đối tượng cũ và để đối tượng cũ được thu gom rác.

Nếu không có sự bất biến, bạn có thể phải vượt qua một đối tượng giữa các phạm vi khác nhau và bạn không biết trước nếu và khi nào đối tượng sẽ được thay đổi. Vì vậy, để tránh các tác dụng phụ không mong muốn, bạn bắt đầu tạo một bản sao đầy đủ của đối tượng "chỉ trong trường hợp" và chuyển bản sao đó xung quanh, ngay cả khi hóa ra không có thuộc tính nào phải thay đổi. Điều đó sẽ để lại nhiều rác hơn trong trường hợp của bạn.

Điều này chứng tỏ là - nếu bạn tạo ra kịch bản giả thuyết đúng, bạn có thể chứng minh bất cứ điều gì, đặc biệt là khi nói đến hiệu suất. Ví dụ của tôi, tuy nhiên, không phải là giả thuyết như nó có thể nghe. Tháng trước tôi đã làm việc trong một chương trình mà chúng tôi vấp phải chính xác vấn đề đó bởi vì ban đầu chúng tôi quyết định không sử dụng cấu trúc dữ liệu bất biến, và do dự để cấu trúc lại điều này sau đó vì nó có vẻ không đáng phiền phức.

Vì vậy, khi bạn xem xét các trường hợp như thế này từ một bài viết SO cũ hơn , câu trả lời cho câu hỏi của bạn có thể trở nên rõ ràng - nó phụ thuộc . Đối với một số trường hợp, tính không thay đổi sẽ ảnh hưởng đến hiệu suất, đối với một số trường hợp ngược lại có thể đúng, đối với nhiều trường hợp, nó sẽ phụ thuộc vào mức độ thực hiện của bạn thông minh và đối với nhiều trường hợp, sự khác biệt sẽ không đáng kể.

Lưu ý cuối cùng: một vấn đề trong thế giới thực mà bạn có thể gặp phải là bạn cần quyết định sớm hoặc chống lại sự bất biến đối với một số cấu trúc dữ liệu cơ bản. Sau đó, bạn xây dựng rất nhiều mã theo đó, và vài tuần hoặc vài tháng sau bạn sẽ thấy quyết định đó là tốt hay xấu.

Nguyên tắc cá nhân của tôi cho tình huống này là:

  • Nếu bạn thiết kế cấu trúc dữ liệu chỉ với một vài thuộc tính dựa trên các kiểu nguyên thủy hoặc bất biến khác, trước tiên hãy thử tính bất biến.
  • Nếu bạn muốn thiết kế một kiểu dữ liệu trong đó các mảng có kích thước lớn (hoặc không xác định), có thể truy cập ngẫu nhiên và thay đổi nội dung, hãy sử dụng tính biến đổi.

Đối với các tình huống giữa hai thái cực này, sử dụng phán đoán của bạn. Nhưng YMMV.


8
That will leave a lot more garbage than in your case.và để làm cho vấn đề tồi tệ hơn, thời gian chạy của bạn có thể sẽ không thể phát hiện sự trùng lặp vô nghĩa, và do đó (không giống như một đối tượng bất biến đã hết hạn không ai đang sử dụng), nó thậm chí sẽ không đủ điều kiện để thu thập.
Jacob Raihle

37

Trước hết, đặc tính của bạn về cấu trúc dữ liệu bất biến là không chính xác. Nói chung, hầu hết cấu trúc dữ liệu không được sao chép mà được chia sẻ và chỉ các phần thay đổi được sao chép. Nó được gọi là cấu trúc dữ liệu liên tục . Hầu hết các triển khai có thể tận dụng các cấu trúc dữ liệu liên tục trong hầu hết thời gian. Hiệu suất này đủ gần với các cấu trúc dữ liệu có thể thay đổi mà các lập trình viên chức năng thường coi là không đáng kể.

Thứ hai, tôi thấy rất nhiều người có một ý tưởng khá không chính xác về thời gian tồn tại điển hình của các đối tượng trong các chương trình mệnh lệnh điển hình. Có lẽ điều này là do sự phổ biến của các ngôn ngữ được quản lý bộ nhớ. Thỉnh thoảng ngồi xuống và thực sự xem có bao nhiêu đối tượng tạm thời và bản sao phòng thủ bạn tạo ra so với cấu trúc dữ liệu thực sự tồn tại lâu dài. Tôi nghĩ bạn sẽ ngạc nhiên về tỷ lệ này.

Tôi đã có người nhận xét trong các lớp lập trình chức năng Tôi dạy về thuật toán tạo ra bao nhiêu rác, sau đó tôi hiển thị phiên bản mệnh lệnh điển hình của cùng một thuật toán tạo ra nhiều như vậy. Chỉ vì một số lý do mà mọi người không chú ý đến nó nữa.

Bằng cách khuyến khích chia sẻ và không khuyến khích tạo các biến cho đến khi bạn có giá trị hợp lệ để đặt vào chúng, tính không thay đổi có xu hướng khuyến khích thực hành mã hóa sạch hơn và cấu trúc dữ liệu tồn tại lâu hơn. Điều này thường dẫn đến so sánh nếu không phải mức rác thấp hơn, tùy thuộc vào thuật toán của bạn.


8
"... sau đó tôi chỉ ra phiên bản mệnh lệnh điển hình của cùng một thuật toán tạo ra nhiều như vậy." Điều này. Ngoài ra, những người mới làm quen với phong cách này, và đặc biệt nếu họ chưa quen với phong cách chức năng nói chung, ban đầu có thể tạo ra các triển khai chức năng dưới tối ưu.
wberry

1
"Không khuyến khích tạo các biến" Không phải chỉ hợp lệ đối với các ngôn ngữ nơi hành vi mặc định được sao chép khi gán / xây dựng ngầm? Trong JavaScript, một biến chỉ là một định danh; nó không phải là một vật thể theo đúng nghĩa của nó. Nó vẫn chiếm không gian ở đâu đó, nhưng không đáng kể (đặc biệt là hầu hết các triển khai JavaScript, theo hiểu biết của tôi, vẫn sử dụng ngăn xếp cho các lệnh gọi hàm, nghĩa là trừ khi bạn có nhiều đệ quy cuối cùng bạn sẽ sử dụng lại cùng một không gian ngăn xếp biến tạm thời). Bất biến không liên quan đến khía cạnh đó.
JAB

33

Người đến muộn với câu hỏi và trả lời này với những câu trả lời tuyệt vời, nhưng tôi muốn xâm nhập như một người nước ngoài thường nhìn vào mọi thứ từ quan điểm cấp thấp hơn của bit và byte trong bộ nhớ.

Tôi rất hào hứng với những thiết kế bất di bất dịch, thậm chí đến từ góc độ C và từ góc độ tìm kiếm những cách mới để lập trình hiệu quả phần cứng quái thú này mà chúng ta có ngày nay.

Chậm / nhanh hơn

Đối với câu hỏi liệu nó làm cho mọi thứ chậm hơn, một câu trả lời robot sẽ được yes. Ở mức độ khái niệm kỹ thuật rất cao này, sự bất biến chỉ có thể làm cho mọi thứ chậm hơn. Phần cứng hoạt động tốt nhất khi nó không phân bổ bộ nhớ lẻ tẻ và chỉ có thể sửa đổi bộ nhớ hiện có (tại sao chúng ta có các khái niệm như địa phương tạm thời).

Tuy nhiên, một câu trả lời thực tế là maybe. Hiệu suất vẫn chủ yếu là một thước đo năng suất trong bất kỳ cơ sở mã không tầm thường nào. Chúng tôi thường không tìm thấy các cơ sở mã hóa khủng khiếp để duy trì vấp ngã trong các điều kiện chủng tộc là hiệu quả nhất, ngay cả khi chúng tôi bỏ qua các lỗi. Hiệu quả thường là một chức năng của sự thanh lịch và đơn giản. Đỉnh cao của tối ưu hóa vi mô có thể phần nào xung đột, nhưng chúng thường được dành cho các phần nhỏ nhất và quan trọng nhất của mã.

Chuyển đổi bit và byte bất biến

Xuất phát từ quan điểm cấp thấp, nếu chúng ta có các khái niệm x-quang như objectsstringsv.v., trung tâm của nó chỉ là các bit và byte trong các dạng bộ nhớ khác nhau với các đặc điểm tốc độ / kích thước khác nhau (tốc độ và kích thước của phần cứng bộ nhớ thường là loại trừ lẫn nhau).

nhập mô tả hình ảnh ở đây

Hệ thống phân cấp bộ nhớ của máy tính thích nó khi chúng ta liên tục truy cập vào cùng một đoạn bộ nhớ, như trong sơ đồ trên, vì nó sẽ giữ cho đoạn bộ nhớ được truy cập thường xuyên ở dạng bộ nhớ nhanh nhất (bộ nhớ cache L1, ví dụ: là gần như nhanh như một đăng ký). Chúng tôi có thể liên tục truy cập vào cùng một bộ nhớ (sử dụng lại nhiều lần) hoặc truy cập nhiều lần vào các phần khác nhau của đoạn (ví dụ: lặp qua các phần tử trong một đoạn liền kề, liên tục truy cập vào các phần khác nhau của đoạn bộ nhớ đó).

Chúng tôi cuối cùng sẽ ném một cờ lê trong quá trình đó nếu sửa đổi bộ nhớ này cuối cùng muốn tạo ra một khối bộ nhớ hoàn toàn mới ở bên cạnh, như vậy:

nhập mô tả hình ảnh ở đây

... trong trường hợp này, việc truy cập khối bộ nhớ mới có thể yêu cầu lỗi trang bắt buộc và lỗi bộ nhớ cache để chuyển nó trở lại dạng bộ nhớ nhanh nhất (tất cả các cách vào một thanh ghi). Đó có thể là một kẻ giết người hiệu suất thực sự.

Tuy nhiên, có nhiều cách để giảm thiểu điều này, tuy nhiên, bằng cách sử dụng một nhóm bộ nhớ dự trữ, đã được chạm vào.

Tập hợp lớn

Một vấn đề khái niệm khác phát sinh từ quan điểm cấp cao hơn một chút chỉ đơn giản là thực hiện các bản sao không cần thiết của các tập hợp thực sự lớn với số lượng lớn.

Để tránh một sơ đồ quá phức tạp, hãy tưởng tượng khối bộ nhớ đơn giản này bằng cách nào đó đắt tiền (có thể là các ký tự UTF-32 trên phần cứng bị giới hạn không thể tin được).

nhập mô tả hình ảnh ở đây

Trong trường hợp này, nếu chúng tôi muốn thay thế "GIÚP" bằng "KILL" và khối bộ nhớ này là bất biến, chúng tôi sẽ phải tạo toàn bộ một khối mới để tạo một đối tượng mới duy nhất, mặc dù chỉ một phần của nó đã thay đổi :

nhập mô tả hình ảnh ở đây

Kéo dài trí tưởng tượng của chúng tôi khá nhiều, loại bản sao sâu sắc này của mọi thứ khác chỉ để làm cho một phần nhỏ độc đáo có thể khá tốn kém (trong trường hợp thực tế, khối bộ nhớ này sẽ lớn hơn nhiều, gây ra vấn đề).

Tuy nhiên, mặc dù chi phí như vậy, loại thiết kế này sẽ có xu hướng ít bị lỗi của con người hơn. Bất cứ ai đã làm việc trong một ngôn ngữ chức năng với các hàm thuần túy có thể đánh giá cao điều này, và đặc biệt trong các trường hợp đa luồng mà chúng ta có thể đa luồng mã như vậy mà không cần quan tâm trên thế giới. Nói chung, các lập trình viên của con người có xu hướng vượt qua các thay đổi trạng thái, đặc biệt là các tác nhân gây ra tác dụng phụ bên ngoài cho các trạng thái bên ngoài phạm vi của chức năng hiện tại. Ngay cả việc khôi phục từ một lỗi bên ngoài (ngoại lệ) trong trường hợp như vậy có thể cực kỳ khó khăn với những thay đổi trạng thái bên ngoài có thể thay đổi trong hỗn hợp.

Một cách để giảm thiểu công việc sao chép dự phòng này là biến các khối bộ nhớ này thành một tập hợp các con trỏ (hoặc tham chiếu) cho các ký tự, như vậy:

Xin lỗi, tôi không nhận ra chúng ta không cần phải tạo ra sự Lđộc đáo trong khi tạo sơ đồ.

Màu xanh biểu thị dữ liệu sao chép nông.

nhập mô tả hình ảnh ở đây

... Thật không may, điều này sẽ cực kỳ tốn kém khi trả một con trỏ / chi phí tham chiếu cho mỗi ký tự. Hơn nữa, chúng tôi có thể phân tán nội dung của các ký tự trên khắp không gian địa chỉ và cuối cùng trả tiền cho nó dưới dạng một lỗi thuyền trang và lỗi bộ nhớ cache, dễ dàng khiến giải pháp này thậm chí còn tệ hơn là sao chép toàn bộ nội dung.

Ngay cả khi chúng tôi cẩn thận phân bổ các ký tự này một cách liên tục, hãy nói rằng máy có thể tải 8 ký tự và 8 con trỏ cho một ký tự vào một dòng bộ đệm. Chúng tôi kết thúc việc tải bộ nhớ như thế này để duyệt qua chuỗi mới:

nhập mô tả hình ảnh ở đây

Trong trường hợp này, cuối cùng chúng tôi yêu cầu 7 dòng bộ nhớ cache khác nhau có giá trị bộ nhớ liền kề được tải để đi qua chuỗi này, khi lý tưởng chúng tôi chỉ cần 3.

Chunk Up dữ liệu

Để giảm thiểu vấn đề ở trên, chúng ta có thể áp dụng chiến lược cơ bản tương tự nhưng ở mức độ thô hơn 8 ký tự, ví dụ:

nhập mô tả hình ảnh ở đây

Kết quả yêu cầu 4 dòng dữ liệu có giá trị (1 cho 3 con trỏ và 3 cho các ký tự) để được tải để đi qua chuỗi này, chỉ thiếu 1 tối ưu về mặt lý thuyết.

Vì vậy, điều đó không tệ chút nào. Có một số lãng phí bộ nhớ nhưng bộ nhớ rất dồi dào và sử dụng nhiều hơn sẽ không làm mọi thứ chậm lại nếu bộ nhớ thêm sẽ chỉ là dữ liệu lạnh không được truy cập thường xuyên. Nó chỉ dành cho dữ liệu nóng, liền kề, nơi giảm tốc độ sử dụng bộ nhớ và tốc độ thường đi đôi với nhau khi chúng tôi muốn lắp thêm bộ nhớ vào một trang hoặc một dòng bộ đệm và truy cập tất cả trước khi bị trục xuất. Đại diện này là khá thân thiện với bộ nhớ cache.

Tốc độ

Vì vậy, sử dụng một đại diện như trên có thể mang lại sự cân bằng hiệu suất khá. Có lẽ việc sử dụng hiệu quả nhất đối với các cấu trúc dữ liệu bất biến sẽ mang tính chất này là sửa đổi các mẩu dữ liệu chunky và làm cho chúng trở nên độc nhất trong quy trình, trong khi sao chép nông các phần không được sửa đổi. Nó cũng ngụ ý một số chi phí hoạt động nguyên tử để tham chiếu các phần sao chép nông một cách an toàn trong bối cảnh đa luồng (có thể với một số tham chiếu nguyên tử đang diễn ra).

Tuy nhiên, miễn là những mẩu dữ liệu này được thể hiện ở mức đủ thô, rất nhiều chi phí này giảm đi và thậm chí có thể tầm thường hóa, trong khi vẫn mang lại cho chúng ta sự an toàn và dễ dàng mã hóa và đa chức năng nhiều chức năng hơn ở dạng thuần túy mà không cần bên ngoài Các hiệu ứng.

Giữ dữ liệu mới và cũ

Nơi tôi thấy sự bất biến có khả năng hữu ích nhất từ ​​quan điểm hiệu suất (theo nghĩa thực tế) là khi chúng ta có thể tạo ra các bản sao dữ liệu lớn để làm cho nó trở nên độc đáo trong bối cảnh có thể thay đổi trong đó mục tiêu là tạo ra thứ gì đó mới từ một cái gì đó đã tồn tại theo cách mà chúng ta muốn giữ cả mới và cũ, khi chúng ta có thể tạo ra những mảnh nhỏ và những mảnh nhỏ độc đáo với một thiết kế bất biến cẩn thận.

Ví dụ: Hoàn tác hệ thống

Một ví dụ về điều này là một hệ thống hoàn tác. Chúng tôi có thể thay đổi một phần nhỏ của cấu trúc dữ liệu và muốn giữ cả hình thức ban đầu mà chúng tôi có thể hoàn tác và hình thức mới. Với kiểu thiết kế bất biến này chỉ làm cho các phần nhỏ, được sửa đổi của cấu trúc dữ liệu trở nên độc đáo, chúng ta có thể chỉ cần lưu một bản sao của dữ liệu cũ trong một mục nhập hoàn tác trong khi chỉ phải trả chi phí bộ nhớ cho dữ liệu các phần duy nhất được thêm vào. Điều này cung cấp một sự cân bằng năng suất rất hiệu quả (làm cho việc thực hiện một hệ thống hoàn tác trở thành một miếng bánh) và hiệu suất.

Giao diện cấp cao

Tuy nhiên, một cái gì đó vụng về phát sinh với trường hợp trên. Trong một loại bối cảnh chức năng cục bộ, dữ liệu có thể thay đổi thường là dễ dàng nhất và đơn giản nhất để sửa đổi. Rốt cuộc, cách dễ nhất để sửa đổi một mảng thường là chỉ lặp qua nó và sửa đổi một phần tử tại một thời điểm. Cuối cùng, chúng ta có thể tăng chi phí trí tuệ nếu chúng ta có một số lượng lớn thuật toán cấp cao để chọn để chuyển đổi một mảng và phải chọn một thuật toán thích hợp để đảm bảo rằng tất cả các bản sao nông mờ này được tạo ra trong khi các phần được sửa đổi là làm độc đáo.

Có lẽ cách dễ nhất trong các trường hợp đó là sử dụng bộ đệm có thể thay đổi cục bộ bên trong ngữ cảnh của hàm (trong đó chúng thường không làm chúng tôi thay đổi), điều này cam kết thay đổi nguyên tử cấu trúc dữ liệu để có được một bản sao bất biến mới (tôi tin rằng một số ngôn ngữ gọi những "quá độ") ...

... Hoặc chúng ta có thể đơn giản mô hình hóa các hàm biến đổi cấp cao hơn và cao hơn trên dữ liệu để chúng ta có thể ẩn quá trình sửa đổi bộ đệm có thể thay đổi và cam kết nó với cấu trúc mà không cần logic có thể thay đổi. Trong mọi trường hợp, đây chưa phải là một lãnh thổ được khám phá rộng rãi và chúng tôi sẽ loại bỏ công việc nếu chúng tôi nắm giữ các thiết kế bất biến hơn để đưa ra các giao diện có ý nghĩa về cách chuyển đổi các cấu trúc dữ liệu này.

Cấu trúc dữ liệu

Một điều nữa phát sinh ở đây là tính bất biến được sử dụng trong bối cảnh quan trọng về hiệu năng có thể sẽ muốn cấu trúc dữ liệu bị phá vỡ thành dữ liệu chunky trong đó các khối không quá nhỏ nhưng cũng không quá lớn.

Danh sách được liên kết có thể muốn thay đổi khá nhiều để phù hợp với điều này và biến thành danh sách không được kiểm soát. Các mảng lớn, liền kề có thể biến thành một mảng các con trỏ thành các khối liền kề với chỉ mục modulo để truy cập ngẫu nhiên.

Nó có khả năng thay đổi cách chúng ta nhìn vào các cấu trúc dữ liệu theo một cách thú vị, trong khi đẩy các chức năng sửa đổi của các cấu trúc dữ liệu này giống với bản chất cồng kềnh hơn để che giấu sự phức tạp thêm trong việc sao chép một số bit ở đây và làm cho các bit khác trở nên độc đáo ở đó.

Hiệu suất

Dù sao, đây là quan điểm cấp thấp hơn của tôi về chủ đề này. Về mặt lý thuyết, tính bất biến có thể có chi phí từ rất lớn đến nhỏ hơn. Nhưng một cách tiếp cận rất lý thuyết không phải lúc nào cũng làm cho các ứng dụng đi nhanh. Nó có thể làm cho chúng có thể mở rộng, nhưng tốc độ trong thế giới thực thường đòi hỏi phải có tư duy thực tế hơn.

Từ góc độ thực tế, các phẩm chất như hiệu suất, khả năng bảo trì và an toàn có xu hướng biến thành một vết mờ lớn, đặc biệt đối với một cơ sở mã rất lớn. Mặc dù hiệu suất trong một số ý nghĩa tuyệt đối bị suy giảm với tính bất biến, thật khó để tranh luận về lợi ích của nó đối với năng suất và an toàn (bao gồm cả an toàn luồng). Với sự gia tăng này thường có thể làm tăng hiệu suất thực tế, nếu chỉ vì các nhà phát triển có nhiều thời gian hơn để điều chỉnh và tối ưu hóa mã của họ mà không bị lỗi.

Vì vậy, tôi nghĩ từ ý nghĩa thực tế này, các cấu trúc dữ liệu bất biến thực sự có thể hỗ trợ hiệu suất trong rất nhiều trường hợp, kỳ lạ như nó có vẻ. Một thế giới lý tưởng có thể tìm kiếm sự kết hợp của cả hai: cấu trúc dữ liệu bất biến và cấu trúc dữ liệu có thể thay đổi, với cấu trúc có thể thay đổi thường rất an toàn để sử dụng trong phạm vi rất cục bộ (ví dụ: cục bộ cho một chức năng), trong khi các cấu trúc bất biến có thể tránh bên ngoài tác động hoàn toàn và biến tất cả các thay đổi đối với cấu trúc dữ liệu thành hoạt động nguyên tử tạo ra một phiên bản mới không có rủi ro về điều kiện chủng tộc.


11

ImmutableJS thực sự khá hiệu quả. Nếu chúng ta lấy một ví dụ:

var x = {
    Foo: 1,
    Bar: { Baz: 2 }
    Qux: { AnotherVal: 3 }
}

Nếu đối tượng trên được tạo thành bất biến thì bạn sửa đổi giá trị của thuộc tính 'Baz' mà bạn sẽ nhận được là:

var y = x.setIn('/Bar/Baz', 3);
y !== x; // Different object instance
y.Bar !== x.Bar // As the Baz property was changed, the Bar object is a diff instance
y.Qux === y.Qux // Qux is the same object instance

Điều này tạo ra một số cải tiến hiệu suất thực sự tuyệt vời cho các mô hình đối tượng sâu, trong đó bạn chỉ cần sao chép các loại giá trị trên các đối tượng trên đường dẫn đến thư mục gốc. Mô hình đối tượng càng lớn và các thay đổi bạn thực hiện càng nhỏ, hiệu năng bộ nhớ và CPU của cấu trúc dữ liệu bất biến càng tốt khi chúng kết thúc việc chia sẻ nhiều đối tượng.

Như các câu trả lời khác đã nói, nếu bạn đối chiếu điều này với việc cố gắng cung cấp các đảm bảo tương tự bằng cách sao chép phòng thủ xtrước khi chuyển nó vào một chức năng có thể thao túng nó thì hiệu suất sẽ tốt hơn đáng kể.


4

Trong một đường thẳng, mã bất biến có chi phí tạo ra đối tượng, chậm hơn. Tuy nhiên, có rất nhiều tình huống mà mã đột biến trở nên rất khó quản lý hiệu quả (dẫn đến việc sao chép phòng thủ rất tốn kém) và có rất nhiều chiến lược thông minh để giảm thiểu chi phí 'sao chép' một đối tượng , như đã đề cập bởi những người khác.

Nếu bạn có một đối tượng như bộ đếm và nó sẽ tăng lên nhiều lần trong một giây, thì bộ đếm đó là bất biến có thể không đáng bị phạt hiệu suất. Nếu bạn có một đối tượng được đọc bởi nhiều phần khác nhau trong ứng dụng của bạn và mỗi đối tượng muốn có bản sao đối tượng hơi khác nhau của mình, bạn sẽ dễ dàng sắp xếp thời gian đó theo cách thức biểu diễn bằng cách sử dụng tốt đối tượng bất biến thực hiện.


4

Để thêm vào câu hỏi này (đã được trả lời xuất sắc):

Câu trả lời ngắn gọn là ; nó sẽ ảnh hưởng đến hiệu suất vì bạn chỉ tạo các đối tượng thay vì làm biến đổi các đối tượng hiện có, dẫn đến chi phí tạo đối tượng nhiều hơn.


Tuy nhiên, câu trả lời dài là không thực sự .

Từ quan điểm thời gian chạy thực tế, trong JavaScript, bạn đã tạo ra khá nhiều đối tượng thời gian chạy - các hàm và nghĩa đen của đối tượng có ở khắp mọi nơi trong JavaScript và dường như không ai nghĩ đến việc sử dụng chúng. Tôi sẽ lập luận rằng việc tạo đối tượng thực sự khá rẻ, mặc dù tôi không có trích dẫn nào cho việc này nên tôi sẽ không sử dụng nó như một cuộc tranh luận độc lập.

Đối với tôi, sự gia tăng 'hiệu suất' lớn nhất không phải ở hiệu năng thời gian chạy mà là hiệu suất của nhà phát triển . Một trong những điều đầu tiên tôi học được khi làm việc trên các ứng dụng Real World (tm) là khả năng biến đổi thực sự nguy hiểm và khó hiểu. Tôi đã mất nhiều giờ để theo đuổi một luồng (không phải kiểu đồng thời) khi cố gắng tìm ra nguyên nhân gây ra lỗi tối nghĩa khi nó biến thành một đột biến từ phía bên kia của ứng dụng chết tiệt!

Sử dụng bất biến làm cho mọi thứ dễ dàng hơn nhiều để lý do về. Bạn có thể biết ngay rằng đối tượng X sẽ không thay đổi trong suốt vòng đời của nó và cách duy nhất để nó thay đổi là sao chép nó. Tôi đánh giá điều này nhiều hơn (đặc biệt là trong môi trường nhóm) hơn bất kỳ tối ưu hóa vi mô nào mà tính đột biến có thể mang lại.

Có các trường hợp ngoại lệ, đáng chú ý nhất là cấu trúc dữ liệu như đã lưu ý ở trên. Tôi hiếm khi bắt gặp một kịch bản mà tôi muốn thay đổi bản đồ sau khi tạo (mặc dù phải thừa nhận rằng tôi đang nói về bản đồ giả đối tượng theo nghĩa đen thay vì bản đồ ES6), tương tự cho các mảng. Khi bạn đang xử lý các cấu trúc dữ liệu lớn hơn, khả năng biến đổi có thể được đền đáp. Hãy nhớ rằng mọi đối tượng trong JavaScript được truyền dưới dạng tham chiếu thay vì giá trị.


Điều đó nói rằng, một điểm được đưa lên ở trên là GC và không có khả năng phát hiện các bản sao. Đây là một mối quan tâm chính đáng, nhưng theo tôi nó chỉ là mối quan tâm khi bộ nhớ là mối quan tâm, và có nhiều cách dễ dàng hơn để tự mã hóa vào một góc - ví dụ, tham chiếu vòng tròn trong các bao đóng.


Cuối cùng, tôi muốn có một cơ sở mã hóa bất biến với rất ít (nếu có) các phần có thể thay đổi và có hiệu suất thấp hơn một chút so với khả năng biến đổi ở mọi nơi. Bạn luôn có thể tối ưu hóa sau này nếu bất biến, vì một số lý do, trở thành mối quan tâm cho hiệu suất.

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.