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ư objects
và strings
v.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).
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:
... 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).
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 :
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.
... 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:
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ụ:
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.