การเพิ่มประสิทธิภาพการจัดสรรสตริงที่ซ้ำซ้อนใน C ++


10

ฉันมีองค์ประกอบ C ++ ที่ค่อนข้างซับซ้อนซึ่งประสิทธิภาพได้กลายเป็นปัญหา การทำโปรไฟล์แสดงให้เห็นว่าเวลาดำเนินการส่วนใหญ่นั้นใช้เพียงการจัดสรรหน่วยความจำสำหรับstd::strings เท่านั้น

ฉันรู้ว่ามีจำนวนมากซ้ำซ้อนในสายเหล่านั้น ค่าจำนวนเล็กน้อยทำซ้ำบ่อยมาก แต่ก็มีค่าที่ไม่ซ้ำกันจำนวนมาก โดยทั่วไปแล้วสตริงค่อนข้างสั้น

ตอนนี้ฉันแค่คิดว่ามันจะทำให้รู้สึกถึงการใช้ซ้ำการจัดสรรบ่อยเหล่านั้นอย่างใด แทนที่จะเป็น 1,000 พอยน์เตอร์ถึง 1,000 ค่า "foobar" ที่แตกต่างกันฉันสามารถมี 1,000 พอยน์เตอร์ต่อหนึ่งค่า "foobar" ความจริงที่ว่านี้จะมีประสิทธิภาพมากขึ้นของหน่วยความจำเป็นโบนัสที่ดี แต่ฉันส่วนใหญ่กังวลเกี่ยวกับความล่าช้าที่นี่

ฉันเดาว่าตัวเลือกหนึ่งจะรักษารีจิสทรีของค่าที่จัดสรรไว้บางส่วน แต่เป็นไปได้หรือไม่ที่จะทำให้การค้นหารีจิสทรีเร็วขึ้นกว่าการจัดสรรหน่วยความจำซ้ำซ้อน นี่เป็นวิธีที่ปฏิบัติได้หรือไม่?


6
เป็นไปได้? ใช่แน่นอน - ภาษาอื่นจะทำสิ่งนี้เป็นประจำ (เช่น Java - ค้นหาการฝึกงานสตริง) สิ่งสำคัญที่ต้องพิจารณาอย่างไรก็ตามคือวัตถุแคชต้องไม่เปลี่ยนรูปซึ่งstd :: stringไม่
Hulk

2
คำถามนี้มีความเกี่ยวข้องมากกว่านี้: stackoverflow.com/q/26130941
rwong

8
คุณได้วิเคราะห์ประเภทของการจัดการสตริงที่มีอิทธิพลต่อแอปพลิเคชันของคุณหรือไม่? มันคือการคัดลอกการแยกสตริงย่อยการเรียงต่อกันการจัดการแบบตัวอักษรต่ออักขระ? การทำงานแต่ละประเภทต้องใช้เทคนิคการปรับให้เหมาะสมต่างกัน นอกจากนี้โปรดตรวจสอบว่าการใช้งานคอมไพเลอร์และไลบรารีมาตรฐานของคุณรองรับ "การเพิ่มประสิทธิภาพสตริงขนาดเล็ก" หรือไม่ สุดท้ายถ้าคุณใช้การฝึกงานแบบสตริงประสิทธิภาพของฟังก์ชันแฮชก็สำคัญเช่นกัน

2
คุณกำลังทำอะไรกับสายเหล่านั้น พวกมันใช้เป็นตัวระบุหรือคีย์บางอย่างหรือไม่ หรือพวกเขารวมกันเพื่อสร้างผลลัพธ์บางอย่าง? ถ้าเป็นเช่นนั้นคุณจะต่อกันสตริงได้อย่างไร? ด้วย+โอเปอเรเตอร์หรือกับสตริงสตรีม สตริงมาจากไหน ตัวอักษรในรหัสหรืออินพุตภายนอกของคุณ?
amon

คำตอบ:


3

ฉันเรียนรู้อย่างหนักกับสตริงที่อยู่ภายในซึ่ง Basile แนะนำซึ่งการค้นหาสตริงแปลเป็นดัชนีแบบ 32 บิตเพื่อจัดเก็บและเปรียบเทียบ สิ่งนี้มีประโยชน์ในกรณีของฉันเนื่องจากบางครั้งฉันมีชิ้นส่วนนับแสนถึงล้านชิ้นที่มีคุณสมบัติชื่อ "x" เช่นซึ่งยังคงต้องเป็นชื่อสตริงที่ใช้งานง่ายเนื่องจากผู้ใช้สามารถเข้าถึงได้โดย scripters ตามชื่อ

ฉันใช้ trie สำหรับการค้นหา (ทดลองด้วยunordered_mapแต่ trie ที่ปรับแล้วของฉันสำรองไว้โดยพูลหน่วยความจำอย่างน้อยก็เริ่มทำงานได้ดีขึ้นและยังง่ายต่อการทำเธรดที่ปลอดภัยโดยไม่ต้องล็อคทุกครั้งที่เข้าถึงโครงสร้าง) std::stringได้อย่างรวดเร็วสำหรับการก่อสร้างการสร้าง ประเด็นคือเพื่อเพิ่มความเร็วในการดำเนินการที่ตามมาเช่นการตรวจสอบความเท่าเทียมกันของสตริงซึ่งในกรณีของฉันเพียงแค่เดือดลงไปที่การตรวจสอบจำนวนเต็มสองจำนวนเพื่อความเท่าเทียมกันและเพื่อลดการใช้หน่วยความจำอย่างมาก

ฉันเดาว่าตัวเลือกหนึ่งจะรักษารีจิสทรีของค่าที่จัดสรรไว้บางส่วน แต่เป็นไปได้หรือไม่ที่จะทำให้การค้นหารีจิสทรีเร็วขึ้นกว่าการจัดสรรหน่วยความจำซ้ำซ้อน

นั่นจะเป็นเรื่องยากที่จะทำการค้นหาผ่านโครงสร้างข้อมูลได้เร็วกว่าการค้นหาเพียงครั้งเดียว mallocเช่นหากคุณมีกรณีที่คุณกำลังอ่านจำนวนเรือของสตริงจากอินพุตภายนอกเช่นไฟล์จากนั้นสิ่งล่อใจของฉันจะใช้ตัวจัดสรรแบบลำดับหากเป็นไปได้ ที่มาพร้อมกับข้อเสียที่คุณไม่สามารถเพิ่มหน่วยความจำของแต่ละสายได้ หน่วยความจำทั้งหมดที่รวบรวมโดยตัวจัดสรรจะต้องเป็นอิสระในครั้งเดียวหรือไม่เลย แต่ตัวจัดสรรแบบต่อเนื่องอาจมีประโยชน์ในกรณีที่คุณเพียงแค่ต้องจัดสรรจำนวนหน่วยความจำขนาดเล็กในรูปแบบเรียงลำดับแบบต่อเนื่องเพียงเพื่อที่จะโยนมันออกไปในภายหลัง ฉันไม่ทราบว่ามีการใช้งานในกรณีของคุณหรือไม่ แต่ถ้าเป็นไปได้อาจเป็นวิธีที่ง่ายในการแก้ไขฮอตสปอตที่เกี่ยวข้องกับการจัดสรรหน่วยความจำเล็ก ๆ บ่อย ๆ (ซึ่งอาจมีส่วนเกี่ยวข้องกับการคิดถึงแคช อัลกอริทึมที่ใช้โดย, พูด, malloc)

การจัดสรรที่มีขนาดคงที่นั้นทำได้ง่ายขึ้นโดยไม่มีข้อ จำกัด ของตัวจัดสรรแบบลำดับที่ป้องกันไม่ให้คุณเพิ่มหน่วยความจำที่เฉพาะเจาะจงเพื่อนำมาใช้ซ้ำในภายหลัง แต่การจัดสรรขนาดแบบผันแปรเร็วกว่าตัวจัดสรรเริ่มต้นค่อนข้างยาก โดยพื้นฐานแล้วการจัดสรรหน่วยความจำชนิดใดที่เร็วกว่าmallocโดยทั่วไปจะยากมากหากคุณไม่ใช้ข้อ จำกัด ที่ จำกัด การบังคับใช้ให้แคบลง ทางออกหนึ่งคือการใช้ตัวจัดสรรขนาดคงที่สำหรับพูดสตริงทั้งหมดที่มีขนาด 8 ไบต์หรือน้อยกว่าหากคุณมี boatload ของพวกเขาและสตริงที่ยาวกว่านั้นเป็นกรณีที่หายาก (ซึ่งคุณสามารถใช้ตัวจัดสรรเริ่มต้น) นั่นหมายความว่า 7 ไบต์จะสูญเปล่าสำหรับสตริง 1 ไบต์ แต่ควรกำจัดฮอตสปอตที่เกี่ยวข้องกับการจัดสรรถ้าพูด 95% ของเวลาสตริงของคุณสั้นมาก

อีกวิธีหนึ่งที่เพิ่งเกิดขึ้นกับฉันคือการใช้รายการลิงก์ที่ไม่ได้ควบคุมซึ่งอาจฟังดูบ้า แต่ได้ยินฉัน

ป้อนคำอธิบายรูปภาพที่นี่

แนวคิดในที่นี้คือการทำให้แต่ละโหนดที่ไม่ถูกควบคุมเป็นขนาดคงที่แทนที่จะเป็นขนาดตัวแปร เมื่อคุณทำเช่นนั้นคุณสามารถใช้ตัวจัดสรรก้อนขนาดคงที่ที่รวดเร็วซึ่งหน่วยความจำพูลการจัดสรรชิ้นขนาดคงที่สำหรับสตริงขนาดผันแปรที่เชื่อมโยงเข้าด้วยกัน ที่จะไม่ลดการใช้หน่วยความจำก็จะมีแนวโน้มที่จะเพิ่มเพราะค่าใช้จ่ายของการเชื่อมโยง แต่คุณสามารถเล่นกับขนาดที่ไม่ได้ควบคุมเพื่อหาสมดุลที่เหมาะสมกับความต้องการของคุณ เป็นแนวคิดที่แปลกประหลาด แต่ควรกำจัดฮอตสปอตที่เกี่ยวข้องกับหน่วยความจำเนื่องจากตอนนี้คุณสามารถจัดสรรหน่วยความจำได้อย่างมีประสิทธิภาพแล้วในบล็อกที่อยู่ติดกันขนาดใหญ่และยังคงมีประโยชน์ในการเพิ่มสตริงทีละรายการ นี่คือตัวจัดสรรค่าคงที่ ol ที่เรียบง่ายที่ฉันเขียน (ตัวอย่างที่ฉันทำเพื่อคนอื่นไม่มีการผลิตปุยที่เกี่ยวข้องกับการผลิต) ซึ่งคุณสามารถใช้ได้อย่างอิสระ:

#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;
}

2

คุณอาจต้องการมีเครื่องจักรสายอักขระภายใน (แต่สตริงควรไม่เปลี่ยนรูปดังนั้นใช้const std::string-s) คุณอาจจะต้องการบางสัญลักษณ์ คุณอาจดูตัวชี้อัจฉริยะ (เช่นstd :: shared_ptr ) หรือแม้แต่std :: string_viewใน C ++ 17


0

กาลครั้งหนึ่งในการสร้างคอมไพเลอร์เราใช้สิ่งที่เรียกว่า data-chair (แทนที่จะเป็น data-bank ซึ่งเป็นการแปลภาษาเยอรมันสำหรับ DB) นี่เป็นการสร้างแฮชสำหรับสตริงและใช้สำหรับการจัดสรร ดังนั้นสตริงใด ๆ จึงไม่ใช่หน่วยความจำบน heap / stack แต่เป็นรหัสแฮชใน data-chair นี้ คุณสามารถแทนที่Stringด้วยชั้นเรียนดังกล่าว ต้องการการทำใหม่รหัสบางอย่าง และแน่นอนว่าสิ่งนี้ใช้ได้สำหรับสตริง r / o เท่านั้น


สิ่งที่เกี่ยวกับการคัดลอกเมื่อเขียน หากคุณเปลี่ยนสตริงคุณจะคำนวณแฮชใหม่และกู้คืน หรือว่าจะไม่ทำงาน?
Jerry Jeremiah

@JerryJeremiah ขึ้นอยู่กับใบสมัครของคุณ คุณสามารถเปลี่ยนสตริงที่แสดงโดยแฮชและเมื่อคุณดึงข้อมูลการแทนแฮชคุณจะได้รับค่าใหม่ ในบริบทคอมไพเลอร์คุณจะสร้างแฮชใหม่สำหรับสตริงใหม่
qwerty_so

0

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

ค่าใช้จ่ายในการจัดสรรหน่วยความจำจริง ๆ แล้วสูงมาก ดังนั้นสตริง std :: อาจใช้การจัดสรรแบบแทนที่สำหรับสตริงขนาดเล็กอยู่แล้วและจำนวนการจัดสรรจริงอาจต่ำกว่าที่คุณคิดเอาไว้ ในกรณีที่ขนาดของบัฟเฟอร์นี้ไม่ใหญ่พอคุณอาจได้รับแรงบันดาลใจจากเช่นคลาสสตริงของ Facebook ( https://github.com/facebook/folly/blob/master/folly/FBString.h ) ซึ่งใช้ 23 ตัวอักษร ภายในก่อนจัดสรร

ค่าใช้จ่ายในการใช้หน่วยความจำจำนวนมากก็คุ้มค่าเช่นกัน นี่อาจเป็นผู้กระทำความผิดที่ใหญ่ที่สุด: คุณอาจมี RAM จำนวนมากในเครื่องของคุณอย่างไรก็ตามขนาดแคชยังเล็กพอที่จะทำให้ประสิทธิภาพในการเข้าถึงหน่วยความจำที่ยังไม่ได้แคช คุณสามารถอ่านเกี่ยวกับเรื่องนี้ได้ที่นี่: https://en.wikipedia.org/wiki/Locality_of_reference


0

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

อีกวิธีหนึ่งที่อาจเป็นประโยชน์นั้นถูกนำมาใช้ในโกโก้: มีหลายกรณีที่คุณมีพจนานุกรมหลายร้อยหรือหลายพันพจนานุกรมซึ่งส่วนใหญ่เป็นคีย์เดียวกันทั้งหมด ดังนั้นพวกเขาจึงให้คุณสร้างวัตถุซึ่งเป็นชุดของคีย์พจนานุกรมและมีตัวสร้างพจนานุกรมที่ใช้วัตถุเช่นอาร์กิวเมนต์ พจนานุกรมจะทำงานเหมือนกับพจนานุกรมอื่น ๆ แต่เมื่อคุณเพิ่มคู่คีย์ / ค่าพร้อมกับคีย์ในชุดคีย์นั้นคีย์จะไม่ซ้ำกัน แต่จะเก็บเพียงตัวชี้ไปยังคีย์ในชุดคีย์ ดังนั้นพจนานุกรมหลายพันเล่มเหล่านี้ต้องการเพียงหนึ่งสำเนาของแต่ละสายอักขระในชุดนั้น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.