Phát triển cửa hàng khóa / giá trị chuyển sang C ++ hiện đại


9

Tôi đang phát triển một máy chủ cơ sở dữ liệu tương tự như Cassandra.

Sự phát triển đã được bắt đầu ở C, nhưng mọi thứ trở nên rất phức tạp nếu không có lớp học.

Hiện tại tôi đã chuyển mọi thứ trong C ++ 11, nhưng tôi vẫn đang học C ++ "hiện đại" và nghi ngờ về nhiều thứ.

Cơ sở dữ liệu sẽ hoạt động với các cặp Khóa / Giá trị. Mỗi cặp có thêm một số thông tin - khi nào cũng được tạo khi hết hạn (0 nếu không hết hạn). Mỗi cặp là bất biến.

Khóa là chuỗi C, Giá trị là void *, nhưng ít nhất là tại thời điểm tôi đang hoạt động với giá trị là chuỗi C.

IListlớp trừu tượng . Nó được kế thừa từ ba lớp

  • VectorList - Mảng động C - tương tự std :: vector, nhưng sử dụng realloc
  • LinkList - được thực hiện để kiểm tra và so sánh hiệu suất
  • SkipList - lớp cuối cùng sẽ được sử dụng.

Trong tương lai tôi cũng có thể làm Red Blackcây.

Mỗi cái IListchứa 0 hoặc nhiều con trỏ tới các cặp, được sắp xếp theo khóa.

Nếu IListtrở nên quá dài, nó có thể được lưu trên đĩa trong một tệp đặc biệt. Tập tin đặc biệt này là loại read only list.

Nếu bạn cần tìm kiếm một khóa,

  • đầu tiên trong bộ nhớ IListđược tìm kiếm ( SkipList, SkipListhoặc LinkList).
  • Sau đó, tìm kiếm được gửi đến các tệp được sắp xếp theo ngày
    (tệp mới nhất trước, tệp cũ nhất - cuối cùng).
    Tất cả các tệp này là mmap-ed trong bộ nhớ.
  • Nếu không tìm thấy gì, thì không tìm thấy khóa.

Tôi không có nghi ngờ về việc thực hiện những IListđiều.


Những gì hiện đang làm tôi bối rối là sau:

Các cặp có kích thước khác nhau , chúng được phân bổ new()và chúng đã std::shared_ptrchỉ vào chúng.

class Pair{
public:
    // several methods...
private:
    struct Blob;

    std::shared_ptr<const Blob> _blob;
};

struct Pair::Blob{
    uint64_t    created;
    uint32_t    expires;
    uint32_t    vallen;
    uint16_t    keylen;
    uint8_t     checksum;
    char        buffer[2];
};

Biến thành viên "đệm" là biến có kích thước khác nhau. Nó lưu trữ khóa + giá trị.
Ví dụ: nếu khóa có 10 ký tự và giá trị là 10 byte khác, toàn bộ đối tượng sẽ là sizeof(Pair::Blob) + 20(bộ đệm có kích thước ban đầu là 2, do hai byte kết thúc null)

Bố cục tương tự này cũng được sử dụng trên đĩa, vì vậy tôi có thể làm một cái gì đó như thế này:

// get the blob
Pair::Blob *blob = (Pair::Blob *) & mmaped_array[pos];

// create the pair, true makes std::shared_ptr not to delete the memory,
// since it does not own it.
Pair p = Pair(blob, true);

// however if I want the Pair to own the memory,
// I can copy it, but this is slower operation.
Pair p2 = Pair(blob);

Tuy nhiên kích thước khác nhau này là một vấn đề ở nhiều nơi với mã C ++.

Ví dụ tôi không thể sử dụng std::make_shared(). Điều này rất quan trọng đối với tôi, vì nếu tôi có 1M Cặp, tôi sẽ có phân bổ 2M.

Từ phía bên kia, nếu tôi thực hiện "bộ đệm" cho mảng động (ví dụ: char mới [123]), tôi sẽ mất "lừa" mmap, tôi sẽ thực hiện hai lần hủy bỏ nếu tôi muốn kiểm tra khóa và tôi sẽ thêm con trỏ - 8 byte cho lớp.

Tôi cũng đã cố gắng "kéo" tất cả các thành viên Pair::Blobvào Pair, vì vậy Pair::Blobchỉ là bộ đệm, nhưng khi tôi kiểm tra nó, nó khá chậm, có lẽ là do sao chép dữ liệu đối tượng xung quanh.

Một thay đổi khác tôi cũng nghĩ là loại bỏ Pairlớp và thay thế nó bằng std::shared_ptrvà "đẩy" tất cả các phương thức trở lại Pair::Blob, nhưng điều này sẽ không giúp tôi với Pair::Bloblớp kích thước thay đổi .

Tôi tự hỏi làm thế nào tôi có thể cải thiện thiết kế đối tượng để thân thiện hơn với C ++.


Mã nguồn đầy đủ có tại đây:
https://github.com/nmmmnu/HM3


2
Tại sao bạn không sử dụng std::maphay std::unordered_map? Tại sao các giá trị (liên quan đến khóa) một số void*? Bạn có thể sẽ cần phải tiêu diệt chúng tại một số điểm; làm thế nào khi? Tại sao bạn không sử dụng mẫu?
Basile Starynkevitch

Tôi không sử dụng std :: map, vì tôi tin rằng (hoặc ít nhất là thử) để làm điều gì đó tốt hơn std :: map cho trường hợp hiện tại. Nhưng vâng, tôi đang suy nghĩ tại một số điểm để bọc std :: map và kiểm tra hiệu suất của nó như là một IList.
Nick

Giao dịch và gọi d-tors được thực hiện khi phần tử nằm IList::removehoặc khi IList bị hủy. Mất nhiều thời gian, nhưng tôi sẽ làm theo chủ đề riêng biệt. Nó sẽ dễ dàng bởi vì IList sẽ được std::unique_ptr<IList>. vì vậy tôi sẽ có thể "chuyển đổi" nó với danh sách mới và giữ đối tượng cũ ở nơi tôi có thể gọi d-tor.
Nick

Tôi đã thử mẫu. Chúng không phải là giải pháp tốt nhất ở đây, bởi vì đây không phải là thư viện người dùng, khóa luôn luôn C stringvà dữ liệu luôn là một số bộ đệm void *hoặc char *, vì vậy bạn có thể truyền mảng char. Bạn có thể tìm thấy tương tự trong redishoặc memcached. Tại một số điểm tôi có thể quyết định sử dụng std::stringhoặc cố định mảng char cho khóa, nhưng gạch chân nó sẽ vẫn là chuỗi C.
Nick

6
Thay vì thêm 4 bình luận, bạn nên chỉnh sửa câu hỏi của mình
Basile Starynkevitch

Câu trả lời:


3

Cách tiếp cận tôi muốn giới thiệu là tập trung vào giao diện của kho lưu trữ khóa-giá trị của bạn, để làm cho nó sạch nhất có thể và không hạn chế nhất có thể, nghĩa là nó sẽ cho phép tự do tối đa cho người gọi, nhưng cũng có thể tự do tối đa cho việc lựa chọn Làm thế nào để thực hiện nó.

Sau đó, tôi sẽ khuyên bạn nên cung cấp càng nhiều càng tốt, và càng sạch càng tốt, mà không có bất kỳ lo ngại nào về hiệu suất. Đối với tôi có vẻ như unordered_mapđó là lựa chọn đầu tiên của bạn, hoặc có lẽ mapnếu một loại sắp xếp các phím phải được hiển thị bởi giao diện.

Vì vậy, trước tiên hãy làm cho nó hoạt động sạch sẽ và tối thiểu; sau đó, đưa nó vào sử dụng trong một ứng dụng thực tế; khi làm như vậy, bạn sẽ tìm thấy những vấn đề bạn cần giải quyết trên giao diện; Sau đó, đi trước và giải quyết chúng. Hầu hết các khả năng là do thay đổi giao diện, bạn sẽ cần phải viết lại các phần lớn của việc triển khai, vì vậy bất cứ khi nào bạn đã đầu tư vào lần lặp đầu tiên của việc triển khai vượt quá thời gian tối thiểu cần thiết để đưa nó vào hầu như không làm việc là lãng phí thời gian.

Sau đó, hồ sơ nó, và xem những gì cần được cải thiện trong việc thực hiện, mà không thay đổi giao diện. Hoặc bạn có thể có ý tưởng của riêng mình về cách cải thiện việc thực hiện, trước cả khi bạn lập hồ sơ. Điều đó tốt, nhưng vẫn không có lý do để thực hiện những ý tưởng này tại bất kỳ thời điểm nào trước đó.

Bạn nói rằng bạn hy vọng sẽ làm tốt hơn map; Có hai điều có thể nói về điều đó:

a) bạn có thể sẽ không;

b) tránh tối ưu hóa sớm bằng mọi giá.

Đối với việc triển khai, vấn đề chính của bạn dường như là phân bổ bộ nhớ, vì dường như bạn quan tâm đến cách cấu trúc thiết kế của mình để giải quyết các vấn đề mà bạn thấy trước về việc phân bổ bộ nhớ. Cách tốt nhất để giải quyết các mối quan tâm phân bổ bộ nhớ trong C ++ là bằng cách thực hiện quản lý cấp phát bộ nhớ phù hợp, không phải bằng cách xoắn và uốn cong thiết kế xung quanh chúng. Bạn nên tự cho mình may mắn khi bạn đang sử dụng C ++, cho phép bạn thực hiện quản lý phân bổ bộ nhớ của riêng mình, trái ngược với các ngôn ngữ như Java và C #, nơi bạn gặp khá nhiều khó khăn với thời gian chạy ngôn ngữ.

Có nhiều cách khác nhau để quản lý bộ nhớ trong C ++ và khả năng quá tải newtoán tử có thể có ích. Một bộ cấp phát bộ nhớ đơn giản cho dự án của bạn sẽ phân bổ một mảng byte lớn và sử dụng nó như một đống. ( byte* heap.) Bạn sẽ có một firstFreeBytechỉ mục, được khởi tạo về 0, cho biết byte miễn phí đầu tiên trong heap. Khi một yêu cầu cho Nbyte đến, bạn trả lại địa chỉ heap + firstFreeBytevà bạn thêm Nvào firstFreeByte. Vì vậy, việc cấp phát bộ nhớ trở nên nhanh và hiệu quả đến mức hầu như không có vấn đề gì.

Tất nhiên, việc sắp xếp lại tất cả bộ nhớ của bạn có thể không phải là một ý tưởng hay, vì vậy bạn có thể phải chia hàng đống của mình vào các ngân hàng được phân bổ theo yêu cầu và tiếp tục phục vụ các yêu cầu phân bổ từ ngân hàng mới nhất.

Vì dữ liệu của bạn là bất biến, đây là một giải pháp tốt. Nó cho phép bạn từ bỏ ý tưởng về các đối tượng có độ dài thay đổi và để mỗi đối tượng Pairchứa một con trỏ tới dữ liệu của nó, vì việc cấp phát bộ nhớ thêm cho dữ liệu hầu như không có gì.

Nếu bạn muốn có thể loại bỏ các đối tượng khỏi heap, để có thể lấy lại bộ nhớ của chúng, thì mọi thứ trở nên phức tạp hơn: bạn sẽ cần sử dụng không phải con trỏ, mà là con trỏ tới con trỏ, để bạn luôn có thể di chuyển các đối tượng xung quanh trong đống để lấy lại không gian của các đối tượng bị xóa. Mọi thứ trở nên chậm hơn một chút do có thêm sự gián tiếp, nhưng mọi thứ vẫn nhanh như chớp so với việc sử dụng các thói quen phân bổ bộ nhớ thư viện thời gian chạy tiêu chuẩn.

Nhưng tất cả điều này tất nhiên thực sự vô ích khi bạn quan tâm nếu trước tiên bạn không xây dựng một phiên bản cơ sở dữ liệu đơn giản, tối giản, đơn giản và đưa nó vào sử dụng trong một ứng dụng thực.

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.