Tôi xoay quanh một số phần trung tâm nhất của cơ sở mã của tôi (một công cụ ECS) xung quanh loại cấu trúc dữ liệu mà bạn đã mô tả, mặc dù nó sử dụng các khối liền kề nhỏ hơn (giống như 4 kilobyte thay vì 4 megabyte).
Nó sử dụng danh sách miễn phí gấp đôi để đạt được các lần chèn và xóa liên tục với một danh sách miễn phí cho các khối miễn phí đã sẵn sàng để được chèn vào (các khối không đầy đủ) và danh sách không có phụ trong khối cho các chỉ mục trong khối đó sẵn sàng để được khai hoang khi chèn.
Tôi sẽ đề cập đến những ưu và nhược điểm của cấu trúc này. Hãy bắt đầu với một số khuyết điểm vì có một số trong số họ:
Nhược điểm
- Mất khoảng 4 lần để chèn một vài trăm triệu phần tử vào cấu trúc này hơn
std::vector
(một cấu trúc hoàn toàn liền kề). Và tôi khá giỏi trong việc tối ưu hóa vi mô nhưng về mặt khái niệm còn nhiều việc phải làm vì trường hợp phổ biến trước tiên phải kiểm tra khối miễn phí ở đầu danh sách miễn phí khối, sau đó truy cập vào khối và bật chỉ mục miễn phí từ khối danh sách miễn phí, viết phần tử ở vị trí miễn phí, sau đó kiểm tra xem khối đã đầy chưa và bật khối từ danh sách miễn phí khối nếu có. Đây vẫn là một hoạt động liên tục nhưng với hằng số lớn hơn nhiều so với đẩy lùi std::vector
.
- Mất khoảng gấp đôi thời gian khi truy cập các phần tử bằng cách sử dụng mẫu truy cập ngẫu nhiên được cung cấp thêm số học để lập chỉ mục và lớp bổ sung thêm.
- Truy cập tuần tự không ánh xạ hiệu quả đến thiết kế của trình vòng lặp vì trình vòng lặp phải thực hiện phân nhánh bổ sung mỗi khi nó tăng lên.
- Nó có một chút chi phí bộ nhớ, thường là khoảng 1 bit cho mỗi phần tử. 1 bit cho mỗi phần tử nghe có vẻ không nhiều, nhưng nếu bạn đang sử dụng phần tử này để lưu trữ một triệu số nguyên 16 bit, thì đó là sử dụng bộ nhớ nhiều hơn 6,25% so với một mảng nhỏ gọn hoàn hảo. Tuy nhiên, trong thực tế, điều này có xu hướng sử dụng ít bộ nhớ hơn
std::vector
trừ khi bạn đang nén vector
để loại bỏ dung lượng dư thừa mà nó dự trữ. Ngoài ra tôi thường không sử dụng nó để lưu trữ các yếu tố tuổi teen như vậy.
Ưu
- Truy cập tuần tự bằng cách sử dụng một
for_each
hàm có phạm vi xử lý gọi lại của các phần tử trong một khối gần như cạnh tranh với tốc độ truy cập tuần tự với std::vector
(chỉ giống như chênh lệch 10%), do đó, nó không hiệu quả hơn trong các trường hợp sử dụng hiệu suất quan trọng nhất đối với tôi ( hầu hết thời gian dành cho động cơ ECS là truy cập tuần tự).
- Nó cho phép loại bỏ thời gian liên tục từ giữa với các khối sắp xếp cấu trúc khi chúng trở nên hoàn toàn trống rỗng. Kết quả là nó thường khá tốt trong việc đảm bảo cấu trúc dữ liệu không bao giờ sử dụng nhiều bộ nhớ hơn mức cần thiết.
- Nó không làm mất hiệu lực các chỉ số đối với các phần tử không được loại bỏ trực tiếp khỏi vùng chứa vì nó chỉ để lại các lỗ hổng bằng cách sử dụng cách tiếp cận danh sách miễn phí để lấy lại các lỗ đó sau khi chèn tiếp theo.
- Bạn không phải lo lắng quá nhiều về việc hết bộ nhớ ngay cả khi cấu trúc này chứa một số lượng lớn các yếu tố, vì nó chỉ yêu cầu các khối liền kề nhỏ không gây ra thách thức cho HĐH để tìm ra một số lượng lớn không sử dụng liền kề trang.
- Nó cho vay chính nó để đồng thời và an toàn luồng mà không khóa toàn bộ cấu trúc, vì các hoạt động thường được tập trung vào các khối riêng lẻ.
Bây giờ một trong những ưu điểm lớn nhất đối với tôi là nó trở nên tầm thường khi tạo ra một phiên bản bất biến của cấu trúc dữ liệu này, như thế này:
Kể từ đó, điều đó đã mở ra tất cả các loại cửa để viết thêm các chức năng mà không có tác dụng phụ giúp dễ dàng đạt được sự an toàn ngoại lệ, an toàn luồng, v.v. cấu trúc dữ liệu này trong nhận thức muộn và tình cờ, nhưng có thể nói là một trong những lợi ích tốt nhất mà nó đã có được vì nó giúp duy trì codebase dễ dàng hơn nhiều.
Các mảng không liên tục không có bộ nhớ cache dẫn đến hiệu năng kém. Tuy nhiên, ở kích thước khối 4M, có vẻ như sẽ có đủ địa phương để lưu trữ tốt.
Địa phương của tài liệu tham khảo không phải là điều mà bạn quan tâm ở các khối có kích thước đó, chứ đừng nói đến các khối 4 kilobyte. Một dòng bộ đệm chỉ là 64 byte thông thường. Nếu bạn muốn giảm các lỗi bộ nhớ cache, thì chỉ cần tập trung vào việc sắp xếp các khối đó đúng cách và ưu tiên các mẫu truy cập tuần tự hơn khi có thể.
Một cách rất nhanh để biến một mẫu bộ nhớ truy cập ngẫu nhiên thành một mẫu tuần tự là sử dụng một bitet. Giả sử bạn có một số lượng lớn các chỉ số và chúng theo thứ tự ngẫu nhiên. Bạn chỉ có thể cày qua chúng và đánh dấu các bit trong bitet. Sau đó, bạn có thể lặp qua bitet của mình và kiểm tra byte nào khác không, kiểm tra, giả sử, 64 bit mỗi lần. Khi bạn gặp một tập hợp 64 bit trong đó ít nhất một bit được đặt, bạn có thể sử dụng các hướng dẫn FFS để nhanh chóng xác định các bit nào được đặt. Các bit cho bạn biết những chỉ số nào bạn nên truy cập, ngoại trừ bây giờ bạn nhận được các chỉ mục được sắp xếp theo thứ tự.
Điều này có một số chi phí nhưng có thể là một trao đổi đáng giá trong một số trường hợp, đặc biệt là nếu bạn sẽ lặp đi lặp lại các chỉ số này nhiều lần.
Truy cập vào một mục không hoàn toàn đơn giản, có thêm một mức độ gián tiếp. Điều này sẽ được tối ưu hóa đi? Nó sẽ gây ra vấn đề bộ nhớ cache?
Không, nó không thể được tối ưu hóa đi. Truy cập ngẫu nhiên, ít nhất, sẽ luôn có giá cao hơn với cấu trúc này. Nó thường không làm tăng bộ nhớ cache của bạn nhiều như vậy mặc dù bạn sẽ có xu hướng có được tính cục bộ cao theo thời gian với các mảng con trỏ tới các khối, đặc biệt nếu các đường dẫn thực thi trường hợp thông thường của bạn sử dụng các mẫu truy cập tuần tự.
Vì có sự tăng trưởng tuyến tính sau khi đạt đến giới hạn 4M, bạn có thể có nhiều phân bổ hơn so với thông thường (giả sử, tối đa 250 phân bổ cho 1GB bộ nhớ). Không có bộ nhớ bổ sung được sao chép sau 4M, tuy nhiên tôi không chắc việc phân bổ thêm có đắt hơn so với sao chép bộ nhớ lớn hay không.
Trong thực tế, việc sao chép thường nhanh hơn vì đây là trường hợp hiếm gặp, chỉ xảy ra một log(N)/log(2)
số lần như tổng số lần trong khi đồng thời đơn giản hóa trường hợp phổ biến bẩn mà bạn có thể viết một phần tử vào mảng nhiều lần trước khi nó trở nên đầy đủ và cần được phân bổ lại. Vì vậy, thông thường, bạn sẽ không nhận được các phần chèn nhanh hơn với loại cấu trúc này bởi vì công việc thông thường đắt hơn ngay cả khi nó không phải xử lý trường hợp hiếm hoi đắt đỏ đó của việc phân bổ lại các mảng lớn.
Sức hấp dẫn chính của cấu trúc này đối với tôi bất chấp tất cả các nhược điểm là giảm sử dụng bộ nhớ, không phải lo lắng về OOM, có thể lưu trữ các chỉ số và con trỏ không bị vô hiệu, đồng thời và không thay đổi. Thật tuyệt khi có một cấu trúc dữ liệu nơi bạn có thể chèn và xóa mọi thứ trong thời gian liên tục trong khi nó tự làm sạch cho bạn và không làm mất hiệu lực các con trỏ và chỉ mục vào cấu trúc.