กลไกของการเพิ่มประสิทธิภาพสตริงสั้นใน libc ++ คืออะไร?


106

คำตอบนี้ให้ภาพรวมระดับสูงที่ดีของการเพิ่มประสิทธิภาพสตริงสั้น (SSO) อย่างไรก็ตามฉันต้องการทราบรายละเอียดเพิ่มเติมว่ามันทำงานอย่างไรในทางปฏิบัติโดยเฉพาะในการใช้งาน libc ++:

  • สตริงต้องสั้นแค่ไหนจึงจะมีคุณสมบัติเป็น SSO สิ่งนี้ขึ้นอยู่กับสถาปัตยกรรมเป้าหมายหรือไม่?

  • การใช้งานแยกความแตกต่างระหว่างสตริงแบบสั้นและแบบยาวเมื่อเข้าถึงข้อมูลสตริงอย่างไร มันง่ายเหมือนm_size <= 16หรือเป็นแฟล็กที่เป็นส่วนหนึ่งของตัวแปรสมาชิกอื่น ๆ หรือไม่? (ฉันคิดว่าm_sizeหรือบางส่วนอาจใช้ในการจัดเก็บข้อมูลสตริง)

ผมถามคำถามนี้มาโดยเฉพาะสำหรับ libc ++ เพราะฉันรู้ว่ามันใช้ SSO นี้ถูกกล่าวถึงแม้ในlibc ++ หน้าแรก

นี่คือข้อสังเกตบางประการหลังจากดูที่มา :

libc ++ สามารถคอมไพล์โดยมีเลย์เอาต์หน่วยความจำที่แตกต่างกันเล็กน้อยสำหรับคลาสสตริงซึ่งถูกควบคุมโดย_LIBCPP_ALTERNATE_STRING_LAYOUTแฟล็ก เค้าโครงทั้งสองยังแยกความแตกต่างระหว่างเครื่องจักรเล็ก ๆ น้อย ๆ และเครื่องใหญ่เอนด์เซียนซึ่งทำให้เรามีรูปแบบต่างๆทั้งหมด 4 แบบ ฉันจะถือว่าเค้าโครง "ปกติ" และ endian น้อยในสิ่งต่อไปนี้

สมมติว่าต่อไปsize_typeคือ 4 ไบต์และนั่นvalue_typeคือ 1 ไบต์นี่คือลักษณะของสตริง 4 ไบต์แรกในหน่วยความจำ:

// 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

เนื่องจากขนาดของสตริงสั้นอยู่ใน 7 บิตด้านบนจึงจำเป็นต้องเลื่อนเมื่อเข้าถึง:

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

ในทำนองเดียวกัน getter และ setter สำหรับความจุของสตริงแบบยาวใช้__long_maskเพื่อแก้ไขis_longบิต

ฉันยังคงมองหาคำตอบสำหรับคำถามแรกของฉันคือค่าอะไร__min_capความสามารถของสตริงสั้นใช้สำหรับสถาปัตยกรรมที่แตกต่างกัน?

การใช้งานไลบรารีมาตรฐานอื่น ๆ

คำตอบนี้ให้ภาพรวมที่ดีของstd::stringเค้าโครงหน่วยความจำในการใช้งานไลบรารีมาตรฐานอื่น ๆ


libc ++ เป็นโอเพ่นซอร์สคุณสามารถค้นหาstringส่วนหัวได้ที่นี่ฉันกำลังตรวจสอบอยู่ในขณะนี้ :)
Matthieu M.

คุณอาจสนใจSmall String Optimization and Move Operations
Ali

@ Matthieu M: ฉันเคยเห็นมาก่อนน่าเสียดายที่มันเป็นไฟล์ขนาดใหญ่มากขอบคุณสำหรับความช่วยเหลือในการตรวจสอบ
ValarDohaeris

@ อาลี: ฉันสะดุดกับเรื่องนี้ใน googling ไปรอบ ๆ อย่างไรก็ตามบล็อกโพสต์นี้กล่าวอย่างชัดเจนว่าเป็นเพียงภาพประกอบของ SSO และไม่ใช่ตัวแปรที่ปรับให้เหมาะสมอย่างมากที่จะใช้ในทางปฏิบัติ
ValarDohaeris

คำตอบ:


121

libc ++ basic_stringถูกออกแบบมาให้มีsizeof3 sizeof(word) == sizeof(void*)คำบนสถาปัตยกรรมทุกที่ คุณผ่าธงยาว / สั้นอย่างถูกต้องและช่องขนาดในรูปแบบสั้น

ค่า __min_cap ความจุของสตริงสั้นจะใช้สำหรับสถาปัตยกรรมที่แตกต่างกันอย่างไร

ในรูปแบบสั้นมี 3 คำที่ต้องใช้:

  • 1 บิตไปที่แฟล็กยาว / สั้น
  • ขนาด 7 บิต
  • สมมติว่าchar1 ไบต์ไปที่ null ต่อท้าย (libc ++ จะเก็บค่าว่างหลังข้อมูลเสมอ)

ซึ่งจะทำให้ 3 คำลบ 2 ไบต์เพื่อเก็บสตริงสั้น ๆ (เช่นใหญ่ที่สุดcapacity()โดยไม่มีการจัดสรร)

ในเครื่อง 32 บิต 10 ตัวอักษรจะพอดีกับสตริงสั้น ๆ sizeof (สตริง) คือ 12

ในเครื่อง 64 บิต 22 ตัวอักษรจะพอดีกับสตริงสั้น ๆ sizeof (สตริง) คือ 24

เป้าหมายในการออกแบบที่สำคัญคือการลดขนาดให้เล็กที่สุดsizeof(string)ในขณะที่ทำให้บัฟเฟอร์ภายในมีขนาดใหญ่ที่สุด เหตุผลคือเพื่อเร่งการก่อสร้างและย้ายงาน ขนาดใหญ่sizeofคำอื่น ๆ ที่คุณต้องย้ายในระหว่างการก่อสร้างย้ายหรือย้ายที่ได้รับมอบหมาย

แบบยาวต้องมีอย่างน้อย 3 คำเพื่อจัดเก็บตัวชี้ข้อมูลขนาดและความจุ ดังนั้นฉันจึง จำกัด รูปแบบสั้น ๆ ไว้ที่ 3 คำเดียวกันนั้น มีการแนะนำว่าขนาด 4 คำอาจมีประสิทธิภาพที่ดีกว่า ฉันยังไม่ได้ทดสอบตัวเลือกการออกแบบนั้น

_LIBCPP_ABI_ALTERNATE_STRING_LAYOUT

มีการเรียกแฟล็กคอนฟิกูเรชัน_LIBCPP_ABI_ALTERNATE_STRING_LAYOUTซึ่งจัดเรียงสมาชิกข้อมูลใหม่เพื่อให้ "โครงร่างแบบยาว" เปลี่ยนจาก:

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

ถึง:

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

แรงจูงใจในการเปลี่ยนแปลงนี้คือความเชื่อที่ว่าการใส่__data_ก่อนจะมีข้อดีในด้านประสิทธิภาพเนื่องจากการจัดตำแหน่งที่ดีขึ้น มีความพยายามในการวัดความได้เปรียบด้านประสิทธิภาพและเป็นการยากที่จะวัด จะไม่ทำให้ประสิทธิภาพแย่ลงและอาจทำให้ดีขึ้นเล็กน้อย

ธงควรใช้ด้วยความระมัดระวัง เป็น ABI ที่แตกต่างกันและหากผสมโดยบังเอิญกับ libc ++ ที่std::stringคอมไพล์ด้วยการตั้งค่าอื่น_LIBCPP_ABI_ALTERNATE_STRING_LAYOUTจะสร้างข้อผิดพลาดรันไทม์

ฉันแนะนำให้เปลี่ยนแฟล็กนี้โดยผู้ขาย libc ++ เท่านั้น


17
ไม่แน่ใจว่ามีความเข้ากันได้ของใบอนุญาตระหว่าง libc ++ และ Facebook Folly หรือไม่ แต่ FBstring สามารถจัดเก็บถ่านพิเศษ (เช่น 23) ได้โดยเปลี่ยนขนาดเป็นความจุที่เหลือเพื่อให้สามารถทำหน้าที่สองเท่าในฐานะตัวยุติแบบว่างสำหรับสตริงสั้น ๆ ที่ 23 ตัวอักษร .
TemplateRex

20
@TemplateRex: นั่นคือความฉลาด อย่างไรก็ตามหากใช้ libc ++ มันจะต้องใช้ libc ++ เพื่อละทิ้งคุณสมบัติอื่น ๆ ที่ฉันชอบเกี่ยวกับ std :: string: ค่าเริ่มต้นที่สร้างขึ้นstringคือ 0 บิตทั้งหมด ทำให้การก่อสร้างเริ่มต้นมีประสิทธิภาพสูง และหากคุณเต็มใจที่จะทำผิดกฎบางครั้งก็ฟรีด้วย เช่นคุณสามารถcallocจำได้และเพียงแค่ประกาศว่าเต็มไปด้วยสตริงที่สร้างขึ้นเริ่มต้น
Howard Hinnant

6
อ่า 0-init ดีจริงๆ! BTW, FBstring มีแฟล็ก 2 บิตบ่งบอกถึงสตริงสั้นกลางและใหญ่ มันใช้ SSO สำหรับสตริงที่สูงถึง 23 ตัวอักษรจากนั้นใช้ขอบเขตหน่วยความจำ malloc-ed สำหรับสตริงที่สูงถึง 254 ตัวอักษรและนอกเหนือจากที่พวกเขาทำ COW (ฉันรู้ว่าไม่ถูกกฎหมายใน C ++ 11 อีกต่อไป)
TemplateRex

เหตุใดจึงไม่สามารถจัดเก็บขนาดและความจุในints เพื่อให้คลาสสามารถบรรจุได้เพียง 16 ไบต์บนสถาปัตยกรรม 64 บิต
phuclv

@ LưuVĩnhPhúc: ฉันต้องการอนุญาตสตริงที่มากกว่า 2Gb บน 64 บิต sizeofค่าใช้จ่ายเป็นที่ยอมรับมากขึ้น แต่ในขณะเดียวกันบัฟเฟอร์ภายในสำหรับcharไปจาก 14 ถึง 22 ซึ่งเป็นประโยชน์ที่ดีทีเดียว
Howard Hinnant

21

การใช้งาน libc ++ค่อนข้างซับซ้อนฉันจะเพิกเฉยต่อการออกแบบทางเลือกและสมมติว่ามีคอมพิวเตอร์ endian ตัวเล็ก ๆ :

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

หมายเหตุ: __compressed_pairโดยพื้นฐานแล้วเป็นคู่ที่ปรับให้เหมาะสมสำหรับEmpty Base Optimizationหรือที่เรียกว่าtemplate <T1, T2> struct __compressed_pair: T1, T2 {};; สำหรับความตั้งใจและวัตถุประสงค์ทั้งหมดคุณสามารถพิจารณาคู่ปกติได้ ความสำคัญเกิดขึ้นเพราะstd::allocatorไร้สัญชาติและว่างเปล่า

โอเคนี่ค่อนข้างดิบดังนั้นมาตรวจสอบกลไกกันเถอะ! ภายในฟังก์ชันจำนวนมากจะเรียก__get_pointer()สิ่งที่ตัวเองเรียก__is_longเพื่อตรวจสอบว่าสตริงกำลังใช้__longหรือการ__shortแสดง:

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

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

พูดตามตรงฉันไม่แน่ใจว่านี่คือ Standard C ++ มากเกินไป (ฉันรู้ข้อกำหนดเบื้องต้นในภายหลังunionแต่ไม่รู้ว่ามันเชื่อมโยงกับสหภาพที่ไม่ระบุชื่อและนามแฝงที่โยนเข้าด้วยกันอย่างไร) แต่ไลบรารีมาตรฐานได้รับอนุญาตให้ใช้ประโยชน์จากการใช้งานที่กำหนดไว้ พฤติกรรมต่อไป.


ขอบคุณสำหรับคำตอบโดยละเอียดนี้! สิ่งเดียวที่ฉันขาดหายไปคือสิ่งที่__min_capจะประเมินสำหรับสถาปัตยกรรมที่แตกต่างกันฉันไม่แน่ใจว่าอะไรsizeof()จะกลับมาและได้รับอิทธิพลจากนามแฝงอย่างไร
ValarDohaeris

1
@ValarDohaer เป็นการนำไปใช้งานที่กำหนดไว้ โดยทั่วไปคุณจะคาดหวัง3 * the size of one pointerในกรณีนี้ซึ่งจะเป็น 12 อ็อกเต็ตในส่วนโค้ง 32 บิตและ 24 บนส่วนโค้ง 64 บิต
justin
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.