Cơ chế tối ưu hóa chuỗi ngắn trong libc ++ là gì?


102

Câu trả lời này cung cấp một cái nhìn tổng quan cấp cao về tối ưu hóa chuỗi ngắn (SSO). Tuy nhiên, tôi muốn biết chi tiết hơn cách nó hoạt động trong thực tế, cụ thể là trong việc triển khai libc ++:

  • Chuỗi phải ngắn đến mức nào để đủ điều kiện nhận SSO? Điều này có phụ thuộc vào kiến ​​trúc mục tiêu không?

  • Làm thế nào để triển khai phân biệt giữa chuỗi ngắn và chuỗi dài khi truy cập dữ liệu chuỗi? Nó có đơn giản như m_size <= 16hay nó là một cờ là một phần của một số biến thành viên khác? (Tôi tưởng tượng rằng m_sizehoặc một phần của nó cũng có thể được sử dụng để lưu trữ dữ liệu chuỗi).

Tôi đã hỏi câu hỏi này đặc biệt cho libc ++ vì tôi biết rằng nó sử dụng SSO, điều này thậm chí còn được đề cập trên trang chủ libc ++ .

Dưới đây là một số nhận xét sau khi xem nguồn :

libc ++ có thể được biên dịch với hai cách bố trí bộ nhớ hơi khác nhau cho lớp chuỗi, điều này được điều chỉnh bởi _LIBCPP_ALTERNATE_STRING_LAYOUTcờ. Cả hai cách bố trí cũng phân biệt giữa máy đời nhỏ và máy cỡ lớn, khiến chúng ta có tổng cộng 4 biến thể khác nhau. Tôi sẽ giả định bố cục "bình thường" và ít endian trong những gì sau đây.

Giả sử xa hơn đó size_typelà 4 byte và đó value_typelà 1 byte, đây là 4 byte đầu tiên của chuỗi sẽ trông như thế nào trong bộ nhớ:

// short string: (s)ize and 3 bytes of char (d)ata
sssssss0;dddddddd;dddddddd;dddddddd
       ^- is_long = 0

// long string: (c)apacity
ccccccc1;cccccccc;cccccccc;cccccccc
       ^- is_long = 1

Vì kích thước của chuỗi ngắn nằm trong 7 bit trên, nên nó cần được dịch chuyển khi truy cập:

size_type __get_short_size() const {
    return __r_.first().__s.__size_ >> 1;
}

Tương tự, getter và setter cho khả năng của một chuỗi dài sử dụng __long_maskđể làm việc xung quanh is_longbit.

Tôi vẫn đang tìm câu trả lời cho câu hỏi đầu tiên của mình, tức là giá trị nào __min_cap, dung lượng của các chuỗi ngắn, sẽ lấy cho các kiến ​​trúc khác nhau?

Các triển khai thư viện tiêu chuẩn khác

Câu trả lời này cung cấp một cái nhìn tổng quan tốt đẹp về std::stringbố cục bộ nhớ trong các triển khai thư viện tiêu chuẩn khác.


libc ++ là mã nguồn mở, bạn có thể tìm thấy stringtiêu đề của nó ở đây , tôi đang kiểm tra nó vào lúc này :)
Matthieu M.


@Matthieu M.: Tôi đã từng thấy nó trước đây, tiếc là nó là một tệp rất lớn, cảm ơn vì đã giúp kiểm tra nó.
ValarDohaeris

@Ali: Tôi đã vấp phải điều này trong googling xung quanh. Tuy nhiên, bài đăng trên blog này nói rõ ràng rằng nó chỉ là hình minh họa của SSO và không phải là một biến thể được tối ưu hóa cao sẽ được sử dụng trong thực tế.
ValarDohaeris

Câu trả lời:


120

Libc ++ basic_stringđược thiết kế để có sizeof3 từ trên tất cả các kiến ​​trúc, ở đâu sizeof(word) == sizeof(void*). Bạn đã phân tích chính xác cờ dài / ngắn và trường kích thước trong biểu mẫu ngắn.

__min_cap, dung lượng của các chuỗi ngắn, sẽ nhận giá trị nào cho các kiến ​​trúc khác nhau?

Ở dạng ngắn, có 3 từ để làm việc với:

  • 1 bit đi đến cờ dài / ngắn.
  • 7 bit là kích thước.
  • Giả sử char, 1 byte chuyển đến giá trị null ở cuối (libc ++ sẽ luôn lưu trữ giá trị null ở phía sau dữ liệu).

Điều này để lại 3 từ trừ đi 2 byte để lưu trữ một chuỗi ngắn (tức là lớn nhất capacity()mà không có phân bổ).

Trên máy 32 bit, 10 ký tự sẽ nằm gọn trong chuỗi ngắn. sizeof (string) là 12.

Trên máy 64 bit, 22 ký tự sẽ nằm gọn trong chuỗi ngắn. sizeof (string) là 24.

Mục tiêu thiết kế chính là giảm thiểu sizeof(string), đồng thời làm cho bộ đệm bên trong càng lớn càng tốt. Cơ sở lý luận là để tăng tốc độ xây dựng và di chuyển phân công. Càng lớn sizeof, bạn càng phải di chuyển nhiều từ hơn trong quá trình xây dựng di chuyển hoặc chuyển nhiệm vụ.

Dạng dài cần tối thiểu 3 từ để lưu trữ con trỏ dữ liệu, kích thước và dung lượng. Vì vậy, tôi đã hạn chế hình thức rút gọn trong cùng 3 từ đó. Có ý kiến ​​cho rằng kích thước 4 từ có thể có hiệu suất tốt hơn. Tôi đã không thử nghiệm lựa chọn thiết kế đó.

_LIBCPP_ABI_ALTERNATE_STRING_LAYOUT

Có một cờ cấu hình được gọi là cờ _LIBCPP_ABI_ALTERNATE_STRING_LAYOUTsắp xếp lại các thành viên dữ liệu để "bố cục dài" thay đổi từ:

struct __long
{
    size_type __cap_;
    size_type __size_;
    pointer   __data_;
};

đến:

struct __long
{
    pointer   __data_;
    size_type __size_;
    size_type __cap_;
};

Động lực cho sự thay đổi này là niềm tin rằng đặt lên hàng __data_đầu sẽ có một số lợi thế về hiệu suất do sự liên kết tốt hơn. Một nỗ lực đã được thực hiện để đo lường các lợi thế về hiệu suất, và rất khó để đo lường. Nó sẽ không làm cho hiệu suất kém đi và nó có thể làm cho nó tốt hơn một chút.

Cờ nên được sử dụng cẩn thận. Nó là một ABI khác và nếu vô tình trộn lẫn với libc ++ std::stringđược biên dịch với một cài đặt khác _LIBCPP_ABI_ALTERNATE_STRING_LAYOUTsẽ tạo ra lỗi thời gian chạy.

Tôi khuyên bạn chỉ nên thay đổi cờ này bởi nhà cung cấp libc ++.


17
Không chắc liệu có khả năng tương thích giấy phép giữa libc ++ và Facebook Folly hay không, nhưng FBstring quản lý để lưu trữ thêm một ký tự (tức là 23) bằng cách thay đổi kích thước thành dung lượng còn lại , để nó có thể thực hiện nhiệm vụ kép như là null terminator cho một chuỗi ngắn gồm 23 ký tự .
TemplateRex

20
@TemplateRex: Thật là thông minh. Tuy nhiên, nếu libc ++ thông qua nó sẽ yêu cầu libc ++ từ bỏ một đặc điểm khác mà tôi thích về std :: string của nó: Một cấu trúc mặc định stringlà tất cả các bit 0. Điều đó làm cho việc xây dựng mặc định trở nên siêu hiệu quả. Và nếu bạn sẵn sàng bẻ cong các quy tắc, thậm chí đôi khi là miễn phí. Ví dụ: bạn có thể callocnhớ và chỉ cần khai báo nó chứa đầy các chuỗi được xây dựng mặc định.
Howard Hinnant

6
Ah, 0-init thực sự rất hay! BTW, FBstring có 2 bit cờ, biểu thị chuỗi ngắn, trung gian và chuỗi lớn. Nó sử dụng SSO cho các chuỗi có tối đa 23 ký tự và sau đó sử dụng vùng bộ nhớ malloc-ed cho các chuỗi lên đến 254 ký tự và xa hơn nữa chúng sử dụng COW (tôi biết không còn hợp pháp trong C ++ 11).
TemplateRex

Tại sao kích thước và dung lượng không thể được lưu trữ bằng ints để lớp có thể được đóng gói chỉ còn 16 byte trên kiến ​​trúc 64 bit?
phuclv

@ LưuVĩnhPhúc: Tôi muốn cho phép chuỗi lớn hơn 2Gb trên 64-bit. Chi phí được thừa nhận là lớn hơn sizeof. Nhưng đồng thời bộ đệm bên trong tăng chartừ 14 đến 22, đây là một lợi ích khá tốt.
Howard Hinnant

21

Việc triển khai libc ++ hơi phức tạp, tôi sẽ bỏ qua thiết kế thay thế của nó và giả sử một máy tính endian nhỏ:

template <...>
class basic_string {
/* many many things */

    struct __long
    {
        size_type __cap_;
        size_type __size_;
        pointer   __data_;
    };

    enum {__short_mask = 0x01};
    enum {__long_mask  = 0x1ul};

    enum {__min_cap = (sizeof(__long) - 1)/sizeof(value_type) > 2 ?
                      (sizeof(__long) - 1)/sizeof(value_type) : 2};

    struct __short
    {
        union
        {
            unsigned char __size_;
            value_type __lx;
        };
        value_type __data_[__min_cap];
    };

    union __ulx{__long __lx; __short __lxx;};

    enum {__n_words = sizeof(__ulx) / sizeof(size_type)};

    struct __raw
    {
        size_type __words[__n_words];
    };

    struct __rep
    {
        union
        {
            __long  __l;
            __short __s;
            __raw   __r;
        };
    };

    __compressed_pair<__rep, allocator_type> __r_;
}; // basic_string

Lưu ý: __compressed_pairvề cơ bản là một cặp được tối ưu hóa cho Tối ưu hóa cơ sở trống , hay còn gọi là template <T1, T2> struct __compressed_pair: T1, T2 {};; đối với tất cả các ý định và mục đích, bạn có thể coi nó là một cặp thông thường. Tầm quan trọng của nó chỉ xuất hiện vì std::allocatorkhông có trạng thái và do đó trống rỗng.

Được rồi, cái này khá thô, vì vậy hãy kiểm tra cơ học! Bên trong, nhiều hàm sẽ gọi hàm __get_pointer()mà chính nó gọi __is_longđể xác định xem chuỗi có đang sử dụng __longhoặc __shortbiểu diễn:

bool __is_long() const _NOEXCEPT
    { return bool(__r_.first().__s.__size_ & __short_mask); }

// __r_.first() -> __rep const&
//     .__s     -> __short const&
//     .__size_ -> unsigned char

Thành thật mà nói, tôi không quá chắc chắn đây là Chuẩn C ++ (Tôi biết điều khoản phụ ban đầu trong unionnhưng không biết cách nó liên kết với một liên minh ẩn danh và bí danh được ném cùng nhau), nhưng Thư viện Chuẩn được phép tận dụng việc triển khai đã định nghĩa hành vi dù sao.


Cảm ơn bạn vì câu trả lời chi tiết này! Phần duy nhất tôi còn thiếu là những gì __min_capsẽ đánh giá cho các kiến ​​trúc khác nhau, tôi không chắc điều gì sizeof()sẽ quay trở lại và nó bị ảnh hưởng như thế nào bởi răng cưa.
ValarDohaeris

1
@ValarDohaeris nó được triển khai. thông thường, bạn sẽ mong đợi 3 * the size of one pointertrong trường hợp này, sẽ là 12 octet trên vòm 32 bit và 24 trên vòm 64 bit.
justin
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.