C ++ में निरर्थक स्ट्रिंग आवंटन का अनुकूलन


10

मेरे पास एक काफी जटिल सी ++ घटक है जिसका प्रदर्शन एक समस्या बन गया है। प्रोफाइलिंग से पता चलता है कि निष्पादन का अधिकांश समय केवल स्मृति के लिए आवंटित करने में खर्च होता है std::string

मुझे पता है कि उन तार के बीच अतिरेक है। मुट्ठी भर मूल्य बहुत बार दोहराते हैं लेकिन बहुत सारे अनूठे मूल्य भी होते हैं। तार आमतौर पर काफी कम होते हैं।

मैं अब सिर्फ यह सोच रहा हूं कि क्या यह किसी तरह से उन आवंटन को फिर से उपयोग करने के लिए समझ में आएगा। 1000 पॉइंटर्स के बजाय 1000 अलग-अलग "फ़ॉबर" मानों के लिए, मेरे पास "फ़ॉबर" मान के लिए 1000 पॉइंटर्स हो सकते हैं। तथ्य यह है कि यह अधिक स्मृति कुशल होगा एक अच्छा बोनस है, लेकिन मैं ज्यादातर यहाँ विलंबता के बारे में चिंतित हूं।

मुझे लगता है कि एक विकल्प यह होगा कि पहले से आवंटित मूल्यों की किसी प्रकार की रजिस्ट्री को बनाए रखा जाए, लेकिन क्या यह संभव है कि रजिस्ट्री को अनावश्यक स्मृति आवंटन से अधिक तेज़ बनाया जाए? क्या यह एक व्यवहार्य दृष्टिकोण है?


6
संभव? हां निश्चित रूप से - अन्य भाषाएँ यह नियमित रूप से करती हैं (उदाहरण के लिए जावा - स्ट्रिंग इंटर्न की खोज)। हालांकि, एक महत्वपूर्ण बात यह है कि कैश्ड वस्तुओं को अपरिवर्तनीय होना चाहिए, जो कि std :: string नहीं है।
हल्क

2
यह प्रश्न अधिक प्रासंगिक है: stackoverflow.com/q/26130941
9:00

8
क्या आपने विश्लेषण किया है कि आपके आवेदन में किस प्रकार के स्ट्रिंग जोड़तोड़ हावी हैं? क्या यह नकल, उप-स्ट्रिंग निष्कर्षण, संघनन, चरित्र-दर-चरित्र हेरफेर है? प्रत्येक प्रकार के ऑपरेशन के लिए अलग-अलग अनुकूलन तकनीकों की आवश्यकता होती है। इसके अलावा, कृपया जांच लें कि आपका कंपाइलर और मानक पुस्तकालय कार्यान्वयन "छोटे स्ट्रिंग अनुकूलन" का समर्थन करता है या नहीं। अंत में, यदि आप स्ट्रिंग इंटर्निंग का उपयोग करते हैं, तो हैश फ़ंक्शन का प्रदर्शन भी महत्वपूर्ण है।
रॉन्ग

2
आप उन तारों के साथ क्या कर रहे हैं? क्या वे सिर्फ किसी प्रकार के पहचानकर्ता या कुंजी के रूप में उपयोग किए जाते हैं? या वे कुछ आउटपुट बनाने के लिए संयुक्त हैं? यदि हां, तो आप स्ट्रिंग कॉन्सेप्टन कैसे करते हैं? साथ +ऑपरेटर या स्ट्रिंग धाराओं के साथ? तार कहां से आए? आपके कोड या बाहरी इनपुट में साहित्य?
अमोन

जवाबों:


3

बेसिल के सुझाव के अनुसार, मैं बहुत सी चीजों पर जोर देता हूं, जहां एक स्ट्रिंग लुकअप स्टोर करने और तुलना करने के लिए 32-बिट इंडेक्स में अनुवाद करता है। मेरे मामले में यह उपयोगी है क्योंकि मेरे पास कभी-कभी "x" नाम की संपत्ति के साथ सैकड़ों-लाखों घटक होते हैं, उदाहरण के लिए, जिसे अभी भी उपयोगकर्ता के अनुकूल स्ट्रिंग नाम की आवश्यकता है क्योंकि इसे अक्सर नाम से स्क्रिप्टर्स द्वारा एक्सेस किया जाता है।

मैं लुकअप के लिए एक ट्राइ का उपयोग करता हूं (प्रयोग किया जाता है, unordered_mapलेकिन मेमोरी ट्यून द्वारा समर्थित मेरा ट्यून किया गया ट्राइब कम से कम बेहतर प्रदर्शन करना शुरू कर देता है और हर बार स्ट्रक्चर को एक्सेस किए बिना केवल थ्रेड-सेफ बनाना आसान होता है) लेकिन ऐसा नहीं है निर्माण के लिए उपवास के रूप में std::string। यह बिंदु स्ट्रिंग समानता के लिए जाँच जैसे बाद के संचालन को गति देने के लिए अधिक है, जो मेरे मामले में, समानता के लिए दो पूर्णांकों की जाँच करने और स्मृति उपयोग को कम करने के लिए बस उबलता है।

मुझे लगता है कि एक विकल्प यह होगा कि पहले से आवंटित मूल्यों की किसी प्रकार की रजिस्ट्री को बनाए रखा जाए, लेकिन क्या यह संभव है कि रजिस्ट्री को अनावश्यक स्मृति आवंटन से अधिक तेज़ बनाया जाए?

यह एक एकल की तुलना में बहुत तेजी से डेटा संरचना के माध्यम से एक खोज करने के लिए कठिन होने जा रहा है malloc, उदाहरण के लिए, यदि आपके पास एक मामला है जहां आप एक फ़ाइल की तरह बाहरी इनपुट से स्ट्रिंग्स का एक नाव लोड पढ़ रहे हैं, उदाहरण के लिए, तो मेरा प्रलोभन एक अनुक्रमिक आवंटन का उपयोग करना होगा यदि संभव हो तो। यह नकारात्मक पक्ष के साथ आता है कि आप एक व्यक्तिगत स्ट्रिंग की स्मृति को मुक्त नहीं कर सकते हैं। आवंटनकर्ता द्वारा जमा की गई सभी मेमोरी को एक बार में मुक्त किया जाना चाहिए या नहीं। लेकिन एक अनुक्रमिक आवंटनकर्ता उन मामलों में काम कर सकता है जहां आपको बस एक सीधे अनुक्रमिक फैशन में स्मृति के छोटे चर-आकार के टुकड़े को आवंटित करने की आवश्यकता होती है, केवल बाद में इसे दूर फेंकने के लिए। मुझे नहीं पता कि यह आपके मामले में लागू होता है या नहीं, लेकिन जब लागू होता है, तो यह लगातार किशोरी स्मृति आवंटन से संबंधित हॉटस्पॉट को ठीक करने का एक आसान तरीका हो सकता है (जो कि कैश मिस और पेज दोष के साथ अंतर्निहित से अधिक हो सकता है) द्वारा इस्तेमाल किया एल्गोरिथ्म, कहते हैं, malloc)।

फिक्स्ड-आकार के आवंटन को अनुक्रमिक आवंटन अवरोधों के बिना तेजी से बढ़ाना आसान है जो आपको बाद में पुन: उपयोग किए जाने वाले मेमोरी के विशिष्ट विखंडों को मुक्त करने से रोकते हैं। लेकिन डिफ़ॉल्ट आवंटनकर्ता की तुलना में चर-आकार का आवंटन अधिक कठिन है। मूल रूप से किसी भी प्रकार का मेमोरी एलोकेटर बनाना जो कि तेजी से mallocहोता है, आम तौर पर बेहद कठिन होता है यदि आप बाधाओं को लागू नहीं करते हैं जो इसकी प्रयोज्यता को कम करते हैं। एक समाधान के लिए एक निश्चित आकार के आवंटनकर्ता का उपयोग करना है, कहते हैं, सभी तार जो 8 बाइट्स या उससे कम हैं यदि आपके पास उनमें से एक नाव लोड है और लंबे तार एक दुर्लभ मामला है (जिसके लिए आप बस डिफ़ॉल्ट आवंटनकर्ता का उपयोग कर सकते हैं)। इसका मतलब है कि 1-बाइट स्ट्रिंग्स के लिए 7 बाइट बर्बाद हो जाते हैं, लेकिन यह आवंटन से संबंधित हॉटस्पॉट्स को समाप्त कर देना चाहिए, अगर, कहते हैं, 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

एक बार संकलक निर्माण में हमने डेटा-चेयर (डेटा-बैंक के बजाय, डीबी के लिए बोलचाल की जर्मन अनुवाद) नामक कुछ का उपयोग किया। यह बस एक स्ट्रिंग के लिए एक हैश बनाया और आवंटन के लिए इस्तेमाल किया। इसलिए किसी भी तार को ढेर / स्टैक पर मेमोरी का कुछ टुकड़ा नहीं था, लेकिन इस डेटा-कुर्सी में एक हैश कोड। आप Stringइस तरह के वर्ग से बदल सकते हैं । हालांकि कुछ कोड कोड की आवश्यकता है। और निश्चित रूप से यह केवल आर / ओ तार के लिए उपयोग करने योग्य है।


कॉपी-ऑन-राइट के बारे में क्या। यदि आप स्ट्रिंग बदलते हैं तो आप हैश को फिर से जोड़ देंगे और इसे पुनर्स्थापित करेंगे। या कि काम नहीं करेगा?
जेरी यिर्मयाह

@JerryJeremiah जो आपके आवेदन पर निर्भर करता है। आप हैश द्वारा दर्शाए गए स्ट्रिंग को बदल सकते हैं, और जब आप हैश प्रतिनिधित्व को पुनः प्राप्त करते हैं तो आपको नया मान मिलता है। संकलक संदर्भ में आप एक नए स्ट्रिंग के लिए एक नया हैश बनाएंगे।
qwerty_so

0

ध्यान दें कि मेमोरी आवंटन और वास्तविक मेमोरी दोनों का उपयोग खराब प्रदर्शन से कैसे संबंधित है:

स्मृति को वास्तव में आवंटित करने की लागत, ज़ाहिर है, बहुत अधिक है। इसलिए std :: string छोटे स्ट्रिंग्स के लिए पहले से ही इन-प्लेस आवंटन का उपयोग कर सकती है, और वास्तविक आवंटन की मात्रा इसलिए हो सकती है कि आप पहले मान लें। यदि इस बफ़र का आकार काफी बड़ा नहीं है, तो आप उदाहरण के लिए Facebook के स्ट्रिंग वर्ग ( https://github.com/facebook/folly/blob/master/folly/FBString.h ) से प्रेरित हो सकते हैं, जो 23 वर्णों का उपयोग करता है आंतरिक रूप से आवंटित करने से पहले।

बहुत सारी मेमोरी का उपयोग करने की लागत भी ध्यान देने योग्य है। यह शायद सबसे बड़ा अपराधी है: आपके पास आपके मशीन में बहुत सारे रैम हो सकते हैं, हालांकि, कैश आकार अभी भी काफी छोटा है कि यह मेमोरी को एक्सेस करने पर चोट पहुंचाएगा जो पहले से ही कैश नहीं है। आप इसके बारे में यहां पढ़ सकते हैं: https://en.wikipedia.org/wiki/Locality_of_reference


0

स्ट्रिंग ऑपरेशन को तेज करने के बजाय, एक और तरीका स्ट्रिंग ऑपरेशन की संख्या को कम करना है। उदाहरण के लिए, एनम के साथ तारों को बदलना संभव होगा?

एक और दृष्टिकोण जो उपयोगी हो सकता है वह कोको में उपयोग किया जाता है: ऐसे मामले हैं जहां आपके पास सैकड़ों या हजारों शब्दकोश हैं, जिनमें से ज्यादातर एक ही कुंजी के साथ हैं। तो वे आपको एक ऑब्जेक्ट बनाने देते हैं जो शब्दकोश कुंजियों का एक समूह है, और एक शब्दकोश निर्माता है जो इस तरह के ऑब्जेक्ट को एक तर्क के रूप में लेता है। शब्दकोश किसी भी अन्य शब्दकोश के समान व्यवहार करता है, लेकिन जब आप उस कुंजी सेट में एक कुंजी के साथ एक कुंजी / मूल्य जोड़ी जोड़ते हैं, तो कुंजी को डुप्लिकेट नहीं किया जाता है, लेकिन कुंजी सेट में कुंजी के लिए बस एक पॉइंटर संग्रहीत होता है। तो इन हजारों शब्दकोशों को उस सेट में प्रत्येक कुंजी स्ट्रिंग की केवल एक प्रति की आवश्यकता होती है।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.