Kiểu gợi ý của Kylotan lặp lại nhưng tôi khuyên bạn nên giải quyết vấn đề này ở cấp cấu trúc dữ liệu khi có thể, chứ không phải ở cấp phân bổ thấp hơn nếu bạn có thể giúp đỡ.
Đây là một ví dụ đơn giản về cách bạn có thể tránh phân bổ và giải phóng Foos
nhiều lần bằng cách sử dụng một mảng có lỗ với các phần tử được liên kết với nhau (giải quyết vấn đề này ở cấp độ "container" thay vì mức "cấp phát"):
struct FooNode
{
explicit FooNode(const Foo& ielement): element(ielement), next(-1) {}
// Stores a 'Foo'.
Foo element;
// Points to the next foo available; either the
// next used foo or the next deleted foo. Can
// use SoA and hoist this out if Foo doesn't
// have 32-bit alignment.
int next;
};
struct Foos
{
// Stores all the Foo nodes.
vector<FooNode> nodes;
// Points to the first used node.
int first_node;
// Points to the first free node.
int free_node;
Foos(): first_node(-1), free_node(-1)
{
}
const FooNode& operator[](int n) const
{
return data[n];
}
void insert(const Foo& element)
{
int index = free_node;
if (index != -1)
{
// If there's a free node available,
// pop it from the free list, overwrite it,
// and push it to the used list.
free_node = data[index].next;
data[index].next = first_node;
data[index].element = element;
first_node = index;
}
else
{
// If there's no free node available, add a
// new node and push it to the used list.
FooNode new_node(element);
new_node.next = first_node;
first_node = data.size() - 1;
data.push_back(new_node);
}
}
void erase(int n)
{
// If the node being removed is the first used
// node, pop it from the used list.
if (first_node == n)
first_node = data[n].next;
// Push the node to the free list.
data[n].next = free_node;
free_node = n;
}
};
Một cái gì đó cho hiệu ứng này: một danh sách chỉ mục liên kết đơn với một danh sách miễn phí. Các liên kết chỉ mục cho phép bạn bỏ qua các phần tử bị loại bỏ, loại bỏ các phần tử trong thời gian không đổi và cũng có thể lấy lại / tái sử dụng / ghi đè các phần tử miễn phí bằng cách chèn thời gian không đổi. Để lặp qua cấu trúc, bạn làm một cái gì đó như thế này:
for (int index = foos.first_node; index != -1; index = foos[index].next)
// do something with foos[index]
Và bạn có thể khái quát cấu trúc dữ liệu "mảng lỗ được liên kết" ở trên bằng cách sử dụng các mẫu, gọi vị trí dtor mới và thủ công để tránh yêu cầu gán sao chép, làm cho nó gọi hàm hủy khi các phần tử được loại bỏ, cung cấp một trình lặp chuyển tiếp, v.v. đã chọn giữ ví dụ rất giống C để minh họa rõ hơn cho khái niệm này và cũng bởi vì tôi rất lười biếng.
Điều đó nói rằng, cấu trúc này không có xu hướng xuống cấp tại địa phương sau khi bạn loại bỏ và chèn nhiều thứ vào / từ giữa. Tại thời điểm đó, các next
liên kết có thể khiến bạn đi đi lại lại dọc theo vectơ, tải lại dữ liệu trước đây bị xóa khỏi một dòng bộ đệm trong cùng một giao dịch tuần tự (điều này là không thể tránh khỏi với bất kỳ cấu trúc dữ liệu hoặc bộ cấp phát nào cho phép loại bỏ thời gian liên tục mà không bị xáo trộn khoảng trắng từ giữa với chèn thời gian liên tục và không sử dụng cái gì đó như bitet song song hoặc removed
cờ). Để khôi phục tính thân thiện với bộ đệm, bạn có thể thực hiện phương thức sao chép và trao đổi như thế này:
Foos(const Foos& other)
{
for (int index = other.first_node; index != -1; index = other[index].next)
insert(foos[index].element);
}
void Foos::swap(Foos& other)
{
nodes.swap(other.nodes):
std::swap(first_node, other.first_node);
std::swap(free_node, other.free_node);
}
// ... then just copy and swap:
Foos(foos).swap(foos);
Bây giờ phiên bản mới lại thân thiện với bộ đệm. Một phương pháp khác là lưu trữ một danh sách các chỉ mục riêng biệt vào cấu trúc và sắp xếp chúng theo định kỳ. Một cách khác là sử dụng một bitet để chỉ ra những chỉ số nào được sử dụng. Điều đó sẽ luôn có bạn đi qua các bit theo thứ tự tuần tự (để thực hiện điều này một cách hiệu quả, hãy kiểm tra 64 bit tại một thời điểm, ví dụ như sử dụng FFS / FFZ). Bitet là hiệu quả nhất và không xâm phạm, chỉ cần một bit song song cho mỗi phần tử để chỉ ra phần tử nào được sử dụng và phần nào bị xóa thay vì yêu cầu chỉ mục 32 bit next
, nhưng tốn nhiều thời gian nhất để viết tốt (nó sẽ không nhanh chóng truyền tải nếu bạn đang kiểm tra từng bit một - bạn cần FFS / FFZ để tìm một tập hợp hoặc bỏ đặt bit ngay lập tức trong số hơn 32 bit tại một thời điểm để nhanh chóng xác định phạm vi các chỉ số bị chiếm dụng).
Giải pháp được liên kết này nói chung là dễ thực hiện nhất và không xâm phạm (không yêu cầu sửa đổi Foo
để lưu trữ một số removed
cờ) rất hữu ích nếu bạn muốn tổng quát hóa vùng chứa này để hoạt động với bất kỳ loại dữ liệu nào nếu bạn không nhớ rằng 32 bit phí trên mỗi phần tử.
Tôi có nên tạo bất kỳ nhóm bộ nhớ nào để phân bổ động hay không cần phải bận tâm đến vấn đề này? Nếu nền tảng đích là thiết bị di động thì sao?
cần là một từ mạnh mẽ và tôi thiên vị khi làm việc trong các lĩnh vực rất quan trọng về hiệu suất như raytracing, xử lý hình ảnh, mô phỏng hạt và xử lý lưới, nhưng việc phân bổ và giải phóng các vật thể tuổi teen được sử dụng để xử lý rất nhẹ như đạn là tương đối tốn kém và các hạt riêng lẻ dựa trên bộ cấp phát bộ nhớ có kích thước chung, đa mục đích. Cho rằng bạn sẽ có thể khái quát cấu trúc dữ liệu trên trong một hoặc hai ngày để lưu trữ bất cứ thứ gì bạn muốn, tôi nghĩ rằng đó là một trao đổi đáng giá để loại bỏ chi phí phân bổ / phân bổ heap như vậy hoàn toàn khỏi việc trả cho mỗi điều thiếu niên. Ngoài việc giảm chi phí phân bổ / phân bổ, bạn có được địa phương tốt hơn của việc tham chiếu khi đi qua kết quả (tức là ít lỗi bộ nhớ cache và lỗi trang hơn).
Đối với những gì Josh đã đề cập về GC, tôi đã không nghiên cứu triển khai GC của C # gần giống như Java, nhưng các bộ cấp phát GC thường có phân bổ ban đầuđiều đó rất nhanh bởi vì đó là sử dụng bộ cấp phát tuần tự không thể giải phóng bộ nhớ từ giữa (gần giống như một ngăn xếp, bạn không thể xóa những thứ ở giữa). Sau đó, nó trả cho các chi phí đắt đỏ để thực sự cho phép loại bỏ các đối tượng riêng lẻ trong một luồng riêng biệt bằng cách sao chép bộ nhớ và xóa toàn bộ bộ nhớ được phân bổ trước đó (như phá hủy toàn bộ ngăn xếp cùng một lúc trong khi sao chép dữ liệu vào một thứ giống như cấu trúc được liên kết), nhưng vì nó được thực hiện trong một luồng riêng biệt, nên nó không nhất thiết làm trì hoãn các luồng của ứng dụng của bạn rất nhiều. Tuy nhiên, điều đó mang một chi phí ẩn rất đáng kể về một mức độ gián tiếp bổ sung và mất LOR chung sau một chu kỳ GC ban đầu. Đó là một chiến lược khác để tăng tốc độ phân bổ mặc dù - làm cho nó rẻ hơn trong chuỗi cuộc gọi và sau đó thực hiện công việc đắt tiền ở nơi khác. Vì vậy, bạn cần hai cấp độ gián tiếp để tham chiếu các đối tượng của mình thay vì một cấp vì cuối cùng chúng sẽ bị xáo trộn trong bộ nhớ giữa thời gian bạn phân bổ ban đầu và sau một chu kỳ đầu tiên.
Một chiến lược khác theo cách tương tự dễ áp dụng hơn trong C ++ là đừng bận tâm giải phóng các đối tượng của bạn trong các luồng chính. Chỉ cần tiếp tục thêm và thêm và thêm vào cuối cấu trúc dữ liệu không cho phép xóa những thứ ở giữa. Tuy nhiên, đánh dấu những thứ cần phải loại bỏ. Sau đó, một luồng riêng biệt có thể đảm nhiệm công việc tốn kém trong việc tạo cấu trúc dữ liệu mới mà không cần các phần tử bị loại bỏ và sau đó trao đổi nguyên tử cái mới với cái cũ, ví dụ: Phần lớn chi phí của cả hai phần tử phân bổ và giải phóng có thể được chuyển cho tách luồng nếu bạn có thể đưa ra giả định rằng yêu cầu xóa phần tử không phải được thỏa mãn ngay lập tức. Điều đó không chỉ làm cho việc giải phóng rẻ hơn khi có liên quan đến chủ đề của bạn mà còn giúp phân bổ rẻ hơn, vì bạn có thể sử dụng cấu trúc dữ liệu đơn giản và gọn gàng hơn nhiều mà không bao giờ phải xử lý các trường hợp loại bỏ từ giữa. Nó giống như một container chỉ cần mộtpush_back
chức năng chèn, một clear
chức năng để loại bỏ tất cả các yếu tố và swap
trao đổi nội dung với một thùng chứa mới, nhỏ gọn, ngoại trừ các yếu tố bị loại bỏ; đó là xa như biến đổi đi.