Tôi dựa rất nhiều vào các chuỗi được thực hiện như Basile gợi ý, trong đó một tra cứu chuỗi chuyển thành chỉ mục 32 bit để lưu trữ và so sánh. Điều đó hữu ích trong trường hợp của tôi vì đôi khi tôi có hàng trăm nghìn đến hàng triệu thành phần có thuộc tính có tên là "x", ví dụ, vẫn cần phải là một tên chuỗi thân thiện với người dùng vì nó thường được các nhà viết kịch bản truy cập theo tên.
Tôi sử dụng bộ ba để tra cứu (cũng đã thử nghiệm unordered_map
nhưng bộ ba được điều chỉnh của tôi được hỗ trợ bởi nhóm bộ nhớ ít nhất bắt đầu hoạt động tốt hơn và cũng dễ dàng hơn để tạo luồng an toàn mà không bị khóa mỗi khi cấu trúc được truy cập) nhưng không phải như vậy nhanh chóng để xây dựng như tạo ra std::string
. Vấn đề quan trọng hơn là tăng tốc các hoạt động tiếp theo như kiểm tra sự bằng nhau của chuỗi, trong trường hợp của tôi, chỉ cần kiểm tra hai số nguyên để tìm sự bằng nhau và giảm đáng kể việc sử dụng bộ nhớ.
Tôi đoán một tùy chọn sẽ là duy trì một loại sổ đăng ký các giá trị đã được phân bổ nhưng thậm chí có thể làm cho việc tra cứu sổ đăng ký nhanh hơn phân bổ bộ nhớ dự phòng không?
Điều đó sẽ rất khó để thực hiện tìm kiếm thông qua cấu trúc dữ liệu nhanh hơn nhiều so với một malloc
, ví dụ: Nếu bạn gặp trường hợp bạn đang đọc một chuỗi các chuỗi từ đầu vào bên ngoài như tệp, chẳng hạn, thì cám dỗ của tôi sẽ là sử dụng bộ cấp phát tuần tự nếu có thể. Điều đó đi kèm với nhược điểm là bạn không thể giải phóng bộ nhớ của một chuỗi riêng lẻ. Tất cả bộ nhớ được phân bổ bởi bộ cấp phát phải được giải phóng cùng một lúc hoặc không. Nhưng một bộ cấp phát tuần tự có thể có ích trong trường hợp bạn chỉ cần phân bổ một khối lượng bộ nhớ nhỏ có kích thước thay đổi theo kiểu tuần tự thẳng, chỉ sau đó mới ném nó đi. Tôi không biết điều đó có áp dụng trong trường hợp của bạn hay không, nhưng khi áp dụng, có thể là một cách dễ dàng để khắc phục một điểm nóng liên quan đến việc phân bổ bộ nhớ tuổi teen thường xuyên (có thể liên quan đến lỗi bộ nhớ cache và lỗi trang hơn so với bên dưới thuật toán được sử dụng bởi, nói, malloc
).
Phân bổ có kích thước cố định sẽ dễ dàng tăng tốc hơn mà không bị ràng buộc phân bổ tuần tự ngăn bạn giải phóng các khối bộ nhớ cụ thể để được sử dụng lại sau này. Nhưng thực hiện phân bổ kích thước thay đổi nhanh hơn phân bổ mặc định là khá khó khăn. Về cơ bản làm cho bất kỳ loại cấp phát bộ nhớ nào nhanh hơn malloc
thường rất khó khăn nếu bạn không áp dụng các ràng buộc làm thu hẹp khả năng ứng dụng của nó. Một giải pháp là sử dụng bộ cấp phát có kích thước cố định cho tất cả các chuỗi có 8 byte trở xuống nếu bạn có tải trọng của chúng và các chuỗi dài hơn là một trường hợp hiếm gặp (mà bạn chỉ có thể sử dụng bộ cấp phát mặc định). Điều đó không có nghĩa là 7 byte bị lãng phí cho các chuỗi 1 byte, nhưng nó sẽ loại bỏ các điểm nóng liên quan đến phân bổ, nếu, giả sử, 95% thời gian, chuỗi của bạn rất ngắn.
Một giải pháp khác xảy ra với tôi là sử dụng các danh sách được liên kết không được kiểm soát, nghe có vẻ điên rồ nhưng hãy nghe tôi nói.
Ý tưởng ở đây là làm cho mỗi nút không được kiểm soát có kích thước cố định thay vì kích thước thay đổi. Khi bạn làm điều đó, bạn có thể sử dụng bộ cấp phát khối có kích thước cố định cực nhanh, chứa bộ nhớ, phân bổ các khối có kích thước cố định cho các chuỗi có kích thước thay đổi được liên kết với nhau. Điều đó sẽ không làm giảm việc sử dụng bộ nhớ, nó sẽ có xu hướng thêm vào vì chi phí của các liên kết, nhưng bạn có thể chơi với kích thước không được kiểm soát để tìm sự cân bằng phù hợp với nhu cầu của mình. Đó là một ý tưởng kỳ quặc nhưng nên loại bỏ các điểm nóng liên quan đến bộ nhớ vì giờ đây bạn có thể tập hợp hiệu quả bộ nhớ đã được phân bổ trong các khối liền kề cồng kềnh và vẫn có lợi ích của việc giải phóng các chuỗi riêng lẻ. Đây là một công cụ phân bổ cố định đơn giản mà tôi đã viết (một minh họa tôi đã làm cho người khác, không có lông tơ liên quan đến sản xuất) mà bạn có thể tự do sử dụng:
#ifndef FIXED_ALLOCATOR_HPP
#define FIXED_ALLOCATOR_HPP
class FixedAllocator
{
public:
/// Creates a fixed allocator with the specified type and block size.
explicit FixedAllocator(int type_size, int block_size = 2048);
/// Destroys the allocator.
~FixedAllocator();
/// @return A pointer to a newly allocated chunk.
void* allocate();
/// Frees the specified chunk.
void deallocate(void* mem);
private:
struct Block;
struct FreeElement;
FreeElement* free_element;
Block* head;
int type_size;
int num_block_elements;
};
#endif
#include "FixedAllocator.hpp"
#include <cstdlib>
struct FixedAllocator::FreeElement
{
FreeElement* next_element;
};
struct FixedAllocator::Block
{
Block* next;
char* mem;
};
FixedAllocator::FixedAllocator(int type_size, int block_size): free_element(0), head(0)
{
type_size = type_size > sizeof(FreeElement) ? type_size: sizeof(FreeElement);
num_block_elements = block_size / type_size;
if (num_block_elements == 0)
num_block_elements = 1;
}
FixedAllocator::~FixedAllocator()
{
// Free each block in the list, popping a block until the stack is empty.
while (head)
{
Block* block = head;
head = head->next;
free(block->mem);
free(block);
}
free_element = 0;
}
void* FixedAllocator::allocate()
{
// Common case: just pop free element and return.
if (free_element)
{
void* mem = free_element;
free_element = free_element->next_element;
return mem;
}
// Rare case when we're out of free elements.
// Create new block.
Block* new_block = static_cast<Block*>(malloc(sizeof(Block)));
new_block->mem = malloc(type_size * num_block_elements);
new_block->next = head;
head = new_block;
// Push all but one of the new block's elements to the free stack.
char* mem = new_block->mem;
for (int j=1; j < num_block_elements; ++j)
{
void* ptr = mem + j*type_size;
FreeElement* element = static_cast<FreeElement*>(ptr);
element->next_element = free_element;
free_element = element;
}
return mem;
}
void FixedAllocator::deallocate(void* mem)
{
// Just push a free element to the stack.
FreeElement* element = static_cast<FreeElement*>(mem);
element->next_element = free_element;
free_element = element;
}