95% मामलों में मूल्य 0 या 1 होने पर बहुत बड़े सरणी पर यादृच्छिक अभिगम के लिए कोई भी अनुकूलन?


133

क्या बहुत बड़ी सरणी पर यादृच्छिक अभिगम के लिए कोई संभावित अनुकूलन है (मैं वर्तमान में उपयोग करता uint8_tहूं, और मैं इसके बारे में पूछ रहा हूं कि बेहतर क्या है)

uint8_t MyArray[10000000];

जब सरणी में किसी भी स्थिति में मान है

  • सभी मामलों के 95% के लिए 0 या 1 ,
  • 2 से 4% मामलों में,
  • 3 और 255 के बीच अन्य 1% मामलों में?

तो, क्या इसके uint8_tलिए उपयोग करने के लिए एक सरणी से बेहतर कुछ है ? यह एक यादृच्छिक क्रम में पूरे सरणी पर लूप करने के लिए जितना संभव हो उतना जल्दी होना चाहिए, और यह रैम बैंडविड्थ पर बहुत भारी है, इसलिए जब कुछ थ्रेड से अधिक एक ही समय में विभिन्न सरणियों के लिए कर रहे हैं, तो वर्तमान में पूरे रैम बैंडविड्थ जल्दी से संतृप्त है।

मैं पूछ रहा हूं क्योंकि यह बहुत बड़ा अकुशल लगता है (10 एमबी) जब यह वास्तव में जाना जाता है कि 5% के अलावा लगभग सभी मान 0 या 1. होंगे, तो जब सरणी में सभी मूल्यों का 95% होगा वास्तव में 8 बिट के बजाय केवल 1 बिट की आवश्यकता होगी, यह लगभग एक परिमाण के स्मृति उपयोग को कम करेगा। ऐसा लगता है कि एक अधिक मेमोरी कुशल समाधान होना चाहिए जो इसके लिए आवश्यक रैम बैंडविड्थ को बहुत कम कर देगा, और परिणामस्वरूप यादृच्छिक पहुंच के लिए भी काफी तेज होगा।


36
दो बिट्स (0/1 / हैशटेबल देखें) और 1 से बड़े मूल्यों के लिए हैशटेबल?
user253751

6
@ user202729 यह किस पर निर्भर करता है? मुझे लगता है कि यह एक ऐसी चीज़ है जो किसी के लिए भी एक दिलचस्प सवाल है, जिसे मेरे जैसा कुछ करना है, इसलिए मैं इसके लिए एक सार्वभौमिक समाधान देखना चाहता हूं, न कि एक उत्तर जो मेरे कोड के लिए विशिष्ट है। यदि यह किसी चीज़ पर निर्भर करता है, तो यह अच्छा होगा कि यह समझाने के लिए एक उत्तर दिया जाए कि यह किस पर निर्भर करता है ताकि पढ़ने वाला हर व्यक्ति यह समझ सके कि क्या उसके अपने मामले के लिए बेहतर समाधान है।
जॉनअल

7
अनिवार्य रूप से, आप जो पूछ रहे हैं उसे विरलता कहा जाता है ।
मतीन उल्हाक

5
अधिक जानकारी की आवश्यकता है ... एक्सेस यादृच्छिक क्यों है, और गैर-शून्य मान एक पैटर्न का पालन करते हैं?
Ext3h

4
@IwillnotexistIdonotexist एक प्री-कॉम्प्लेक्शन चरण ठीक होगा, लेकिन सरणी को समय-समय पर संशोधित किया जाना चाहिए, इसलिए प्री-कॉम्प्लेक्शन चरण बहुत महंगा नहीं होना चाहिए।
जॉनअल

जवाबों:


155

एक सरल संभावना जो मन में आती है वह है सामान्य मामलों के लिए प्रति मूल्य 2 बिट्स का एक संपीड़ित सरणी रखना, और एक अलग 4 बाइट प्रति मान (मूल तत्व सूचकांक के लिए 24 बिट, वास्तविक मूल्य के लिए 8 बिट, इसलिए (idx << 8) | value)) के लिए क्रमबद्ध सरणी। अन्य।

जब आप एक मूल्य देखते हैं, तो आप पहले 2bpp सरणी (O (1)) में एक लुकअप करते हैं; यदि आप 0, 1 या 2 पाते हैं तो यह वह मूल्य है जो आप चाहते हैं; यदि आप 3 पाते हैं तो इसका मतलब है कि आपको इसे द्वितीयक सरणी में देखना होगा। यहां आप अपनी रुचि के सूचकांक को 8 (O (लॉग) (n) एक छोटे से n के साथ छोड़ते हैं, जैसा कि यह 1% होना चाहिए) के सूचकांक को देखने के लिए एक द्विआधारी खोज करेंगे, और 4- से मान निकालें। बाइट वाली चीज़

std::vector<uint8_t> main_arr;
std::vector<uint32_t> sec_arr;

uint8_t lookup(unsigned idx) {
    // extract the 2 bits of our interest from the main array
    uint8_t v = (main_arr[idx>>2]>>(2*(idx&3)))&3;
    // usual (likely) case: value between 0 and 2
    if(v != 3) return v;
    // bad case: lookup the index<<8 in the secondary array
    // lower_bound finds the first >=, so we don't need to mask out the value
    auto ptr = std::lower_bound(sec_arr.begin(), sec_arr.end(), idx<<8);
#ifdef _DEBUG
    // some coherency checks
    if(ptr == sec_arr.end()) std::abort();
    if((*ptr >> 8) != idx) std::abort();
#endif
    // extract our 8-bit value from the 32 bit (index, value) thingie
    return (*ptr) & 0xff;
}

void populate(uint8_t *source, size_t size) {
    main_arr.clear(); sec_arr.clear();
    // size the main storage (round up)
    main_arr.resize((size+3)/4);
    for(size_t idx = 0; idx < size; ++idx) {
        uint8_t in = source[idx];
        uint8_t &target = main_arr[idx>>2];
        // if the input doesn't fit, cap to 3 and put in secondary storage
        if(in >= 3) {
            // top 24 bits: index; low 8 bit: value
            sec_arr.push_back((idx << 8) | in);
            in = 3;
        }
        // store in the target according to the position
        target |= in << ((idx & 3)*2);
    }
}

एक सरणी के लिए जैसे कि आपने जो प्रस्तावित किया था, उसके लिए पहले सरणी के लिए 10000000/4 = 2500000 बाइट्स लेने चाहिए, साथ ही दूसरे सरणी के लिए 10000000 * 1% * 4 B = 400000 बाइट्स; इसलिए 2900000 बाइट्स, यानी मूल सरणी का एक तिहाई से भी कम, और सबसे अधिक इस्तेमाल किया जाने वाला हिस्सा सभी को एक साथ मेमोरी में रखा जाता है, जो कैशिंग के लिए अच्छा होना चाहिए (यह एल 3 भी फिट हो सकता है)।

यदि आपको 24-बिट एड्रेसिंग की आवश्यकता है, तो आपको "सेकेंडरी स्टोरेज" को ट्वीक करना होगा; इसे विस्तारित करने का एक तुच्छ तरीका यह है कि सूचकांक के शीर्ष 8 बिट्स पर स्विच करने के लिए 256 एलिमेंट पॉइंटर सरणी हो और ऊपर की तरह 24-बिट इंडेक्स किए गए सॉर्ट किए गए सरणी के लिए आगे।


त्वरित बेंचमार्क

#include <algorithm>
#include <vector>
#include <stdint.h>
#include <chrono>
#include <stdio.h>
#include <math.h>

using namespace std::chrono;

/// XorShift32 generator; extremely fast, 2^32-1 period, way better quality
/// than LCG but fail some test suites
struct XorShift32 {
    /// This stuff allows to use this class wherever a library function
    /// requires a UniformRandomBitGenerator (e.g. std::shuffle)
    typedef uint32_t result_type;
    static uint32_t min() { return 1; }
    static uint32_t max() { return uint32_t(-1); }

    /// PRNG state
    uint32_t y;

    /// Initializes with seed
    XorShift32(uint32_t seed = 0) : y(seed) {
        if(y == 0) y = 2463534242UL;
    }

    /// Returns a value in the range [1, 1<<32)
    uint32_t operator()() {
        y ^= (y<<13);
        y ^= (y>>17);
        y ^= (y<<15);
        return y;
    }

    /// Returns a value in the range [0, limit); this conforms to the RandomFunc
    /// requirements for std::random_shuffle
    uint32_t operator()(uint32_t limit) {
        return (*this)()%limit;
    }
};

struct mean_variance {
    double rmean = 0.;
    double rvariance = 0.;
    int count = 0;

    void operator()(double x) {
        ++count;
        double ormean = rmean;
        rmean     += (x-rmean)/count;
        rvariance += (x-ormean)*(x-rmean);
    }

    double mean()     const { return rmean; }
    double variance() const { return rvariance/(count-1); }
    double stddev()   const { return std::sqrt(variance()); }
};

std::vector<uint8_t> main_arr;
std::vector<uint32_t> sec_arr;

uint8_t lookup(unsigned idx) {
    // extract the 2 bits of our interest from the main array
    uint8_t v = (main_arr[idx>>2]>>(2*(idx&3)))&3;
    // usual (likely) case: value between 0 and 2
    if(v != 3) return v;
    // bad case: lookup the index<<8 in the secondary array
    // lower_bound finds the first >=, so we don't need to mask out the value
    auto ptr = std::lower_bound(sec_arr.begin(), sec_arr.end(), idx<<8);
#ifdef _DEBUG
    // some coherency checks
    if(ptr == sec_arr.end()) std::abort();
    if((*ptr >> 8) != idx) std::abort();
#endif
    // extract our 8-bit value from the 32 bit (index, value) thingie
    return (*ptr) & 0xff;
}

void populate(uint8_t *source, size_t size) {
    main_arr.clear(); sec_arr.clear();
    // size the main storage (round up)
    main_arr.resize((size+3)/4);
    for(size_t idx = 0; idx < size; ++idx) {
        uint8_t in = source[idx];
        uint8_t &target = main_arr[idx>>2];
        // if the input doesn't fit, cap to 3 and put in secondary storage
        if(in >= 3) {
            // top 24 bits: index; low 8 bit: value
            sec_arr.push_back((idx << 8) | in);
            in = 3;
        }
        // store in the target according to the position
        target |= in << ((idx & 3)*2);
    }
}

volatile unsigned out;

int main() {
    XorShift32 xs;
    std::vector<uint8_t> vec;
    int size = 10000000;
    for(int i = 0; i<size; ++i) {
        uint32_t v = xs();
        if(v < 1825361101)      v = 0; // 42.5%
        else if(v < 4080218931) v = 1; // 95.0%
        else if(v < 4252017623) v = 2; // 99.0%
        else {
            while((v & 0xff) < 3) v = xs();
        }
        vec.push_back(v);
    }
    populate(vec.data(), vec.size());
    mean_variance lk_t, arr_t;
    for(int i = 0; i<50; ++i) {
        {
            unsigned o = 0;
            auto beg = high_resolution_clock::now();
            for(int i = 0; i < size; ++i) {
                o += lookup(xs() % size);
            }
            out += o;
            int dur = (high_resolution_clock::now()-beg)/microseconds(1);
            fprintf(stderr, "lookup: %10d µs\n", dur);
            lk_t(dur);
        }
        {
            unsigned o = 0;
            auto beg = high_resolution_clock::now();
            for(int i = 0; i < size; ++i) {
                o += vec[xs() % size];
            }
            out += o;
            int dur = (high_resolution_clock::now()-beg)/microseconds(1);
            fprintf(stderr, "array:  %10d µs\n", dur);
            arr_t(dur);
        }
    }

    fprintf(stderr, " lookup |   ±  |  array  |   ±  | speedup\n");
    printf("%7.0f | %4.0f | %7.0f | %4.0f | %0.2f\n",
            lk_t.mean(), lk_t.stddev(),
            arr_t.mean(), arr_t.stddev(),
            arr_t.mean()/lk_t.mean());
    return 0;
}

(कोड और डेटा हमेशा मेरे Bitbucket में अपडेट किया गया)

ऊपर दिया गया कोड एक 10M एलिमेंट एरे को पॉप्युलेट करता है जिसमें रैंडम डेटा को उनके पोस्ट में निर्दिष्ट ओपी के रूप में वितरित किया जाता है, मेरे डेटा स्ट्रक्चर को इनिशियलाइज़ करता है और फिर:

  • मेरी डेटा संरचना के साथ 10M तत्वों की एक यादृच्छिक खोज करता है
  • मूल सरणी के माध्यम से ही करता है।

(ध्यान दें कि क्रमिक लुकअप के मामले में सरणी हमेशा भारी माप से जीतती है, क्योंकि यह सबसे अधिक कैश-फ्रेंडली लुकअप है जो आप कर सकते हैं)

ये अंतिम दो ब्लॉक 50 बार दोहराए जाते हैं और समयबद्ध होते हैं; अंत में, प्रत्येक प्रकार के लुकअप के लिए माध्य और मानक विचलन की गणना और मुद्रित की जाती है, साथ ही स्पीडअप (लुकअप_मेन / array_mean)।

मैंने -O3 -staticउबंटू 16.04 पर जी ++ 5.4.0 (और , कुछ चेतावनियों) के साथ ऊपर कोड संकलित किया और इसे कुछ मशीनों पर चलाया; उनमें से ज्यादातर Ubuntu 16.04 चला रहे हैं, कुछ पुराने लिनक्स, कुछ नए लिनक्स। मुझे नहीं लगता कि इस मामले में ओएस बिल्कुल प्रासंगिक होना चाहिए।

            CPU           |  cache   |  lookup s)   |     array s)  | speedup (x)
Xeon E5-1650 v3 @ 3.50GHz | 15360 KB |  60011 ±  3667 |   29313 ±  2137 | 0.49
Xeon E5-2697 v3 @ 2.60GHz | 35840 KB |  66571 ±  7477 |   33197 ±  3619 | 0.50
Celeron G1610T  @ 2.30GHz |  2048 KB | 172090 ±   629 |  162328 ±   326 | 0.94
Core i3-3220T   @ 2.80GHz |  3072 KB | 111025 ±  5507 |  114415 ±  2528 | 1.03
Core i5-7200U   @ 2.50GHz |  3072 KB |  92447 ±  1494 |   95249 ±  1134 | 1.03
Xeon X3430      @ 2.40GHz |  8192 KB | 111303 ±   936 |  127647 ±  1503 | 1.15
Core i7 920     @ 2.67GHz |  8192 KB | 123161 ± 35113 |  156068 ± 45355 | 1.27
Xeon X5650      @ 2.67GHz | 12288 KB | 106015 ±  5364 |  140335 ±  6739 | 1.32
Core i7 870     @ 2.93GHz |  8192 KB |  77986 ±   429 |  106040 ±  1043 | 1.36
Core i7-6700    @ 3.40GHz |  8192 KB |  47854 ±   573 |   66893 ±  1367 | 1.40
Core i3-4150    @ 3.50GHz |  3072 KB |  76162 ±   983 |  113265 ±   239 | 1.49
Xeon X5650      @ 2.67GHz | 12288 KB | 101384 ±   796 |  152720 ±  2440 | 1.51
Core i7-3770T   @ 2.50GHz |  8192 KB |  69551 ±  1961 |  128929 ±  2631 | 1.85

परिणाम हैं ... मिश्रित!

  1. सामान्य तौर पर, इन मशीनों में से अधिकांश पर किसी तरह का स्पीडअप होता है, या कम से कम वे बराबर होते हैं।
  2. दो मामलों में जहां सरणी वास्तव में "स्मार्ट संरचना" को ट्रम्प करती है, बहुत सारे कैश के साथ एक मशीन पर लुक होता है और विशेष रूप से व्यस्त नहीं होता है: ऊपर Xeon E5-1650 (15 एमबी कैश) एक रात का निर्माण मशीन है, इस समय काफी निष्क्रिय है; Xeon E5-2697 (35 एमबी कैश) एक बेकार क्षण में भी उच्च प्रदर्शन गणना के लिए एक मशीन है। यह समझ में आता है, मूल सरणी पूरी तरह से उनके विशाल कैश में फिट बैठता है, इसलिए कॉम्पैक्ट डेटा संरचना केवल जटिलता जोड़ती है।
  3. "प्रदर्शन स्पेक्ट्रम" के विपरीत पक्ष पर - लेकिन जहां फिर से सरणी थोड़ा तेज है, वहां विनम्र सेलेरॉन है जो मेरे एनएएस को शक्ति देता है; इसमें इतना कम कैश है कि न तो सरणी और न ही "स्मार्ट संरचना" इसमें बिल्कुल फिट बैठता है। कैश के साथ अन्य मशीनें काफी छोटी हैं।
  4. Xeon X5650 को थोड़ी सावधानी के साथ लिया जाना चाहिए - वे काफी व्यस्त दोहरे सॉकेट वर्चुअल मशीन सर्वर पर वर्चुअल मशीन हैं; यह अच्छी तरह से हो सकता है, हालांकि नाममात्र में यह कैश की एक सभ्य राशि है, परीक्षण के समय के दौरान यह कई बार पूरी तरह से असंबंधित आभासी मशीनों द्वारा पूर्वनिर्धारित हो जाता है।

7
@ जॉन आपको एक संरचना की आवश्यकता नहीं है। A uint32_tठीक रहेगा। माध्यमिक बफ़र से एक तत्व को मिटाकर स्पष्ट रूप से इसे छंटनी होगी। किसी तत्व को सम्मिलित किया जा सकता है std::lower_boundऔर फिर insert(पूरी चीज़ को जोड़ने और फिर से छांटने के बजाय) किया जा सकता है । अपडेट पूर्ण आकार के माध्यमिक सरणी को और अधिक आकर्षक बनाते हैं - मैं निश्चित रूप से इसके साथ शुरू करूँगा।
मार्टिन बोनर

6
@JohnAl क्योंकि मूल्य (idx << 8) + valआप मूल्य भाग के बारे में चिंता करने की ज़रूरत नहीं है - बस एक सीधी तुलना का उपयोग करें। यह हमेशा की तुलना में कम ((idx+1) << 8) + valऔर कम से तुलना करेगा((idx-1) << 8) + val
मार्टिन बोनर

3
@ जॉन: यदि यह उपयोगी हो सकता है, तो मैंने एक populateफ़ंक्शन जोड़ा जो पॉप्युलेट होना चाहिए main_arrऔर sec_arrउस प्रारूप के अनुसार जो lookupउम्मीद करता है। मैं वास्तव में यह कोशिश नहीं की थी, इसलिए यह वास्तव में सही ढंग से काम करने की उम्मीद नहीं है :-); किसी भी तरह, यह आपको सामान्य विचार देना चाहिए।
माटेओ इटालिया

6
मैं इसे केवल बेंचमार्किंग के लिए +1 दे रहा हूं। दक्षता के बारे में एक सवाल पर और कई प्रोसेसर प्रकारों के लिए परिणामों के साथ देखकर अच्छा लगा! अच्छा!
जैक एडले

2
@ जॉनी आपको अपने वास्तविक उपयोग के मामले के लिए इसे प्रोफाइल करना चाहिए और कुछ नहीं। सफेद कमरे की गति कोई फर्क नहीं पड़ता।
जैक एडले

33

एक और विकल्प हो सकता है

  • जाँच करें कि परिणाम 0, 1 या 2 है
  • अगर एक नियमित रूप से देखने नहीं है

दूसरे शब्दों में कुछ इस तरह:

unsigned char lookup(int index) {
    int code = (bmap[index>>2]>>(2*(index&3)))&3;
    if (code != 3) return code;
    return full_array[index];
}

जहाँ bmapमान 3 के साथ प्रति तत्व 2 बिट का उपयोग किया जाता है जिसका अर्थ है "अन्य"।

यह संरचना अद्यतन करने के लिए तुच्छ है, 25% अधिक मेमोरी का उपयोग करता है लेकिन बड़े हिस्से को केवल 5% मामलों में देखा जाता है। बेशक, हमेशा की तरह, अगर यह एक अच्छा विचार है या नहीं, तो यह बहुत सारी अन्य स्थितियों पर निर्भर करता है इसलिए एकमात्र उत्तर वास्तविक उपयोग के साथ प्रयोग कर रहा है।


4
मैं कहूंगा कि रैंडम एक्सेस टाइम पर ज्यादा नुकसान किए बिना, जितना संभव हो उतने कैश हिट्स को प्राप्त करने के लिए एक अच्छा समझौता है (क्योंकि कम संरचना कैश में अधिक आसानी से फिट हो सकती है)।
मेनडेलल

मुझे लगता है कि इसमें और सुधार किया जा सकता है। मुझे अतीत में एक समान लेकिन अलग समस्या के साथ सफलता मिली है, जहां शाखा की भविष्यवाणी का फायदा उठाने में बहुत मदद मिली। यह में विभाजित करने if(code != 3) return code;में मदद कर सकता हैif(code == 0) return 0; if(code==1) return 1; if(code == 2) return 2;
kutschkem

@kutschkem: उस स्थिति में, __builtin_expectऔर सह या PGO भी मदद कर सकता है।
माटेओ इटालिया

23

यह एक ठोस उत्तर की तुलना में "लंबी टिप्पणी" है

जब तक आपका डेटा कुछ ऐसा है जो कुछ अच्छी तरह से जाना जाता है, मुझे संदेह है कि कोई भी आपके प्रश्न का उत्तर दे सकता है (और मुझे ऐसी किसी भी चीज़ के बारे में पता नहीं है जो आपके विवरण से मेल खाती है, लेकिन तब मैं सभी के लिए सभी प्रकार के डेटा पैटर्न के बारे में कभी नहीं जानता। उपयोग-मामलों के प्रकार)। उच्च प्रदर्शन कंप्यूटिंग में विरल डेटा एक आम समस्या है, लेकिन यह आमतौर पर "हमारे पास एक बहुत बड़ी सरणी है, लेकिन केवल कुछ मूल्य गैर-शून्य हैं"।

अच्छी तरह से ज्ञात पैटर्न के लिए जैसे मुझे नहीं लगता कि आपका क्या है, कोई भी सीधे पता नहीं चलेगा जो बेहतर है, और यह विवरण पर निर्भर करता है: यादृच्छिक एक्सेस कितना यादृच्छिक है - क्या सिस्टम डेटा आइटम के क्लस्टर तक पहुंच रहा है, या यह पूरी तरह से यादृच्छिक है जैसे एक समान यादृच्छिक संख्या जनरेटर। क्या टेबल डेटा पूरी तरह से यादृच्छिक है, या 0 के अनुक्रम हैं तो अन्य मूल्यों के बिखरने के साथ 1 का अनुक्रम? यदि आपके पास 0 और 1 के लंबे अनुक्रम हैं, तो रन लेंथ एन्कोडिंग अच्छी तरह से काम करेगी, लेकिन यदि आपके पास "0/1 का चेकबोर्ड" नहीं है, तो यह काम नहीं करेगा। इसके अलावा, आपको "शुरुआती बिंदुओं" की एक तालिका रखनी होगी, ताकि आप अपने स्थान को यथोचित रूप से जल्दी से काम कर सकें।

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

बहुत सारे मामलों में, "गति और छोटे आकार" के बीच समझौता करना उन चीजों में से एक है जिन्हें आपको सॉफ्टवेयर इंजीनियरिंग में [अन्य इंजीनियरिंग में जरूरी नहीं है कि यह इतना समझौता हो]। तो, "सरल कोड के लिए मेमोरी बर्बाद करना" अक्सर पसंदीदा विकल्प होता है। इस अर्थ में, "सरल" समाधान गति के लिए काफी बेहतर है, लेकिन अगर आपके पास रैम के लिए "बेहतर" उपयोग है, तो तालिका के आकार के लिए अनुकूलन आपको पर्याप्त प्रदर्शन और आकार में एक अच्छा सुधार देगा। आपके द्वारा इसे प्राप्त करने के कई अलग-अलग तरीके हो सकते हैं - जैसा कि एक टिप्पणी में सुझाया गया है, एक 2 बिट क्षेत्र जहां दो या तीन सबसे सामान्य मूल्य संग्रहीत हैं, और फिर अन्य मूल्यों के लिए कुछ वैकल्पिक डेटा प्रारूप - एक हैश-टेबल मेरा होगा पहला दृष्टिकोण, लेकिन एक सूची या बाइनरी ट्री भी काम कर सकता है - फिर से, यह आपके "नहीं 0, 1 या 2" के पैटर्न पर निर्भर करता है। फिर, यह इस बात पर निर्भर करता है कि तालिका में मान "बिखरे हुए" कैसे हैं - क्या वे समूहों में हैं या वे समान रूप से वितरित पैटर्न के अधिक हैं?

लेकिन इसके साथ एक समस्या यह है कि आप अभी भी रैम से डेटा पढ़ रहे हैं। फिर आप डेटा को संसाधित करने में अधिक कोड खर्च कर रहे हैं, जिसमें "यह एक सामान्य मूल्य नहीं है" का सामना करने के लिए कुछ कोड भी शामिल हैं।

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

अंत में, आपको संभवतः परीक्षण करने के लिए टिप्पणियों / उत्तरों में से एक या कुछ विचारों को लागू करना होगा, देखें कि क्या यह आपकी समस्या को हल करने में मदद करता है, या यदि स्मृति बस अभी भी मुख्य सीमित कारक है।


धन्यवाद! अंत में, मुझे बस व्हाट्सएप में दिलचस्पी है जब 100% सीपीयू इस तरह के सरणियों (अलग-अलग सरणियों पर अलग-अलग धागे) पर लूपिंग के साथ व्यस्त है। वर्तमान में, एक uint8_tसरणी के साथ , रैम बैंडविड्थ को संतृप्त किया जाता है ~ 5 धागे उसी समय (एक क्वाड चैनल सिस्टम पर) काम कर रहे हैं, इसलिए 5 से अधिक थ्रेड्स का उपयोग करने से अब कोई लाभ नहीं मिलता है। मैं चाहूंगा कि रैम बैंडविड्थ के मुद्दों में भाग के बिना> 10 थ्रेड्स का उपयोग किया जाए, लेकिन अगर पहुंच का सीपीयू पक्ष इतना धीमा हो जाता है कि 10 धागे पहले 5 थ्रेड्स से कम हो जाते हैं, तो यह स्पष्ट रूप से प्रगति नहीं होगी।
जॉनएएल

@ जॉन, आपके पास कितने कोर हैं? यदि आप CPU बाउंड हैं, तो कोर की तुलना में अधिक थ्रेड होने का कोई मतलब नहीं है। इसके अलावा, शायद GPU प्रोग्रामिंग को देखने का समय?
मार्टिन बोनर

@MartinBonner मेरे पास वर्तमान में 12 धागे हैं। और मैं मानता हूं, यह शायद एक GPU पर बहुत अच्छी तरह से चलेगा।
जॉनअल

2
@ जॉनी: यदि आप एक ही अक्षम प्रक्रिया के कई संस्करणों को कई थ्रेड्स पर चला रहे हैं, तो आप हमेशा सीमित प्रगति देखेंगे। भंडारण संरचना को ट्विक करने की तुलना में समानांतर प्रसंस्करण के लिए अपने एल्गोरिथ्म को डिजाइन करने में बड़ी जीत होगी।
जैक एडले

13

मैंने अतीत में जो किया है वह बिटसेट के सामने हैशमैप का उपयोग करता है ।

यह मैट्टे के उत्तर की तुलना में अंतरिक्ष को आधा कर देता है, लेकिन अगर "अपवाद" लुकअप धीमा है (यानी कई अपवाद हैं) धीमा हो सकता है।

अक्सर, हालांकि, "कैश राजा है"।


2
माटेओ के उत्तर की तुलना में एक हैशमैप वास्तव में अंतरिक्ष को आधा कैसे करेगा ? उस हैशमैप में क्या होना चाहिए?
जॉनअल

1
@ जॉन्हल 2-बिट बिटवेक के बजाय 1-बिट बिटसेट = बिटवेक का उपयोग करना।
o11c

2
@ o11c मुझे यकीन नहीं है कि अगर मैं इसे सही ढंग से समझता हूं। आप जहां 1 बिट मूल्यों की एक सरणी के लिए मतलब 0साधन को देखोmain_arr और 1साधन को देखोsec_arr (Matteos कोड के मामले में)? हालांकि मैटटेओस के उत्तर की तुलना में समग्र स्थान की आवश्यकता होगी, क्योंकि इसके एक अतिरिक्त सरणी के बाद से। मुझे यह बिल्कुल समझ में नहीं आता है कि मैटेओस के उत्तर की तुलना में आप केवल आधे स्थान का उपयोग कैसे करेंगे।
जॉनअल

1
क्या आप इसे स्पष्ट कर सकते हैं? आप पहले अपेक्षित मामलों को देखते हैं , और फिर बिटमैप में देखते हैं? यदि हां, तो मुझे शक है कि हैश में धीमी गति से देखने से बिटमैप के आकार को कम करने में बचत बढ़ जाएगी।
मार्टिन बोनर ने मोनिका का

मैंने सोचा था कि इसे हैशलिंग कहा जाता है - लेकिन Google कोई प्रासंगिक हिट नहीं देता है, इसलिए इसे कुछ और होना चाहिए। जिस तरह से आम तौर पर काम किया जाता था, उसे एक बाइट सरणी कहा जाता था, जो कि 0..254 के बीच के विशाल बहुमत को मान देता था। फिर आप 255 को एक ध्वज के रूप में उपयोग करेंगे, और यदि आपके पास एक 255 तत्व था तो आप संबंधित हैश तालिका में सही मूल्य देखेंगे। क्या कोई याद कर सकता है कि इसे क्या कहा गया था? (मुझे लगता है कि मैं एक पुराने आईबीएम टीआर में इसके बारे में पढ़ता हूं।) वैसे भी, आप इसे उस तरह से व्यवस्थित कर सकते हैं जैसे @ o11c का सुझाव है - हमेशा हैश में पहले देखना, अगर यह नहीं है, तो अपने बिट सरणी में देखें।
davidbak

11

जब तक आपके डेटा का पैटर्न नहीं होता है, यह संभावना नहीं है कि कोई भी समझदार गति या आकार अनुकूलन है, और - यह मानकर कि आप एक सामान्य कंप्यूटर को लक्षित कर रहे हैं - 10 एमबी वैसे भी उतना बड़ा सौदा नहीं है।

आपके प्रश्नों में दो मान्यताएँ हैं:

  1. डेटा को खराब तरीके से संग्रहीत किया जा रहा है क्योंकि आप सभी बिट्स का उपयोग नहीं कर रहे हैं
  2. इसे बेहतर तरीके से संग्रहीत करने से चीजें तेजी से बढ़ेंगी।

मुझे लगता है कि ये दोनों धारणाएँ झूठी हैं। ज्यादातर मामलों में डेटा को स्टोर करने का उपयुक्त तरीका सबसे प्राकृतिक प्रतिनिधित्व को संग्रहीत करना है। आपके मामले में, यह वह है जिसके लिए आप गए हैं: 0 और 255 के बीच की संख्या के लिए एक बाइट। कोई भी अन्य प्रतिनिधित्व अधिक जटिल होगा और इसलिए - अन्य सभी चीजें समान - धीमी और अधिक त्रुटि प्रवण होती हैं। इस सामान्य सिद्धांत से अलग होने के लिए आपको अपने डेटा के 95% पर संभावित छह "बर्बाद" बिट्स की तुलना में एक मजबूत कारण की आवश्यकता होती है।

आपकी दूसरी धारणा के लिए, यह सच होगा यदि, और केवल अगर, सरणी के आकार को बदलने से काफी कम कैश मिस हो जाता है। क्या ऐसा होगा यह केवल निश्चित रूप से कामकाजी कोड को निर्धारित करके निर्धारित किया जा सकता है, लेकिन मुझे लगता है कि यह पर्याप्त अंतर बनाने की संभावना नहीं है। क्योंकि आप किसी भी स्थिति में बेतरतीब ढंग से एक्सेस कर रहे होंगे, प्रोसेसर यह जानने के लिए संघर्ष करेगा कि किन बिट डेटा को कैश करना है और दोनों ही स्थिति में रखना है।


8

यदि डेटा और एक्सेस समान रूप से बेतरतीब ढंग से वितरित किए जाते हैं, तो प्रदर्शन संभवतः इस बात पर निर्भर करने वाला है कि एक्सेस का कौन सा अंश बाहरी-स्तर कैश मिस से बचें। ऑप्टिमाइज़ करने से यह जानना होगा कि कैश में किस आकार की सरणी को मज़बूती से समायोजित किया जा सकता है। यदि आपका कैश प्रत्येक पांच कोशिकाओं के लिए एक बाइट को समायोजित करने के लिए काफी बड़ा है, तो सबसे आसान तरीका यह हो सकता है कि एक बाइट पांच बेस-थ्री एन्कोडेड मानों को 0-2 की सीमा में रखे (5 मानों के 243 संयोजन हैं, इसलिए यह होगा एक बाइट में फिट), एक 10,000,000 बाइट सरणी के साथ जो कि आधार -3 मान "2" को इंगित करता है जब भी क्वियर किया जाएगा।

यदि कैश उतना बड़ा नहीं है, लेकिन प्रति 8 कोशिकाओं में एक बाइट को समायोजित कर सकता है, तो आठ बेस -3 मूल्यों के सभी 6,561 संभावित संयोजनों में से चयन करने के लिए एक बाइट मूल्य का उपयोग करना संभव नहीं होगा, लेकिन एकमात्र प्रभाव से 0 या 1 को 2 में बदलना एक अन्यथा अनावश्यक लुकअप का कारण होगा, शुद्धता के लिए सभी 6,561 को सपोर्ट करने की आवश्यकता नहीं होगी। इसके बजाय, कोई 256 सबसे "उपयोगी" मूल्यों पर ध्यान केंद्रित कर सकता है।

खासकर यदि 0 1 से अधिक आम है, या इसके विपरीत, 0 और 1 के संयोजन को एन्कोड करने के लिए 217 मानों का उपयोग करने के लिए एक अच्छा तरीका हो सकता है जिसमें 5 या उससे कम 1 का संयोजन है, xxxx1111 के माध्यम से xxxx0000 को एनकोड करने के लिए 16 मान, 16 को 0000xxxx को एनकोड करने के लिए 1111xxxx, और xxxxxxxx के लिए एक। चार मूल्यों के लिए जो भी अन्य उपयोग एक मिल सकता है के लिए रहेगा। यदि डेटा को यादृच्छिक रूप से वर्णित के रूप में वितरित किया जाता है, तो सभी प्रश्नों का एक छोटा सा हिस्सा बाइट्स को मार देगा, जिसमें सिर्फ जीरो और वे शामिल थे (आठ के सभी समूहों के लगभग 2/3 में, सभी बिट्स शून्य और वाले होंगे, और लगभग 7/8 उन छह या कम 1 बिट) होगा; उनमें से अधिकांश जो कि एक बाइट में नहीं उतरते थे जिसमें चार x होते थे, और एक शून्य या एक पर उतरने का 50% मौका होता। इस प्रकार, केवल चार प्रश्नों में से एक को एक बड़े-सरणी लुकअप की आवश्यकता होगी।

यदि डेटा को यादृच्छिक रूप से वितरित किया जाता है, लेकिन कैश आठ बाइट्स को प्रति आठ तत्वों को संभालने के लिए पर्याप्त नहीं है, तो प्रत्येक बाइट को आठ से अधिक वस्तुओं को संभालने के साथ इस दृष्टिकोण का उपयोग करने की कोशिश कर सकता है, लेकिन जब तक 0 या 1 की ओर एक मजबूत पूर्वाग्रह नहीं है। , मानों के अंश जिन्हें बड़ी सरणी में लुकअप करने के बिना संभाला जा सकता है, क्योंकि प्रत्येक बाइट बढ़ने पर संभाला संख्या घट जाएगी।


7

मैं @ o11c के उत्तर में जोड़ दूंगा, क्योंकि उनका शब्दांकन थोड़ा भ्रमित करने वाला हो सकता है। अगर मुझे पिछले बिट और सीपीयू चक्र को निचोड़ने की आवश्यकता है, तो मैं निम्नलिखित कार्य करूंगा।

हम एक संतुलित द्विआधारी खोज पेड़ का निर्माण शुरू करेंगे जो 5% "कुछ और" मामलों को रखता है। प्रत्येक लुकअप के लिए, आप ट्री को जल्दी से चलते हैं: आपके पास 10000000 तत्व हैं: जिनमें से 5% ट्री में है: इसलिए ट्री डेटा संरचना में 500000 तत्व हैं। इसे O (लॉग (n)) समय में चलना, आपको 19 पुनरावृत्तियों देता है। मैं इस पर कोई विशेषज्ञ नहीं हूं, लेकिन मुझे लगता है कि वहाँ कुछ स्मृति-कुशल कार्यान्वयन हैं। आइए निर्देशित करें:

  • संतुलित पेड़, इसलिए सबट्री स्थिति की गणना की जा सकती है (सूचक को पेड़ के नोड्स में संग्रहीत करने की आवश्यकता नहीं है)। उसी तरह एक ढेर (डेटा संरचना) रैखिक स्मृति में संग्रहीत किया जाता है।
  • 1 बाइट मान (2 से 255)
  • इंडेक्स के लिए 3 बाइट्स (10000000 में 23 बिट्स लगते हैं, जो 3 बाइट्स के लिए उपयुक्त है)

कुल, 4 बाइट्स: 500000 * 4 = 1953 केबी। कैश फिट करता है!

अन्य सभी मामलों (0 या 1) के लिए, आप एक बिटवेक्टर का उपयोग कर सकते हैं। ध्यान दें कि आप यादृच्छिक अभिगम के लिए 5% अन्य मामलों को नहीं छोड़ सकते हैं: 1.19 एमबी।

इन दोनों का संयोजन लगभग 3,099 एमबी का उपयोग करता है। इस तकनीक का उपयोग करके, आप स्मृति के एक कारक 3.08 को बचाएंगे।

हालाँकि, यह @Matteo इटालिया (जो 2.76 एमबी का उपयोग करता है), एक अफ़सोस के जवाब को हरा नहीं करता है । क्या ऐसा कुछ है जो हम अतिरिक्त कर सकते हैं? सबसे अधिक स्मृति लेने वाला हिस्सा पेड़ में सूचकांक के 3 बाइट्स है। अगर हम इसे 2 पर ले सकते हैं, तो हम 488 kB बचाएंगे और कुल मेमोरी का उपयोग होगा: 2.622 MB, जो छोटा है!

हम इसे कैसे करते हैं? हमें अनुक्रमण को 2 बाइट्स तक कम करना होगा। फिर, 10000000 में 23 बिट्स लगते हैं। हमें 7 बिट्स को छोड़ने में सक्षम होना चाहिए। हम बस elements, simply०० तत्वों के २ ^ = (= १२ regions) क्षेत्रों में १००००००० तत्वों की सीमा को विभाजित करके ऐसा कर सकते हैं। अब हम इन क्षेत्रों में से प्रत्येक के लिए औसतन 3906 तत्वों के साथ एक संतुलित वृक्ष का निर्माण कर सकते हैं। सही पेड़ को उठाकर 2 ^ 7 (या एक बिटशिफ्ट >> 7) द्वारा लक्ष्य सूचकांक के एक साधारण विभाजन द्वारा किया जाता है । अब स्टोर करने के लिए आवश्यक इंडेक्स को शेष 16 बिट्स द्वारा दर्शाया जा सकता है। ध्यान दें कि पेड़ की लंबाई के लिए कुछ ओवरहेड है जिसे संग्रहीत करने की आवश्यकता है, लेकिन यह नगण्य है। यह भी ध्यान दें कि यह विभाजन तंत्र पेड़ को चलने के लिए पुनरावृत्तियों की आवश्यक संख्या को कम करता है, यह अब 7 पुनरावृत्तियों को कम कर देता है, क्योंकि हमने 7 बिट्स को गिरा दिया: केवल 12 पुनरावृत्तियों को छोड़ दिया गया है।

ध्यान दें कि आप सैद्धांतिक रूप से अगले 8 बिट्स को काटने की प्रक्रिया को दोहरा सकते हैं, लेकिन इसके लिए आपको औसतन ~ 305 तत्वों के साथ 2 ^ 15 संतुलित पेड़ बनाने की आवश्यकता होगी। इसका परिणाम 2.143 एमबी होगा, केवल 4 पुनरावृत्तियों के साथ पेड़ पर चलना होगा, जो कि काफी गति है, 19 पुनरावृत्तियों की तुलना में हमने शुरू किया था।

अंतिम निष्कर्ष के रूप में: यह 2-बिट वेक्टर रणनीति को स्मृति के एक छोटे से उपयोग से हरा देता है, लेकिन इसे लागू करने के लिए एक संपूर्ण संघर्ष है। लेकिन अगर यह कैश की फिटिंग के बीच अंतर कर सकता है या नहीं, यह कोशिश के काबिल हो सकता है।


1
वीरतापूर्ण प्रयास!
दाविदक

1
इसे आज़माएं: चूंकि 4% मामलों में मूल्य 2 हैं ... असाधारण मामलों का एक सेट बनाएं (> 1)। एक पेड़ बनाएं जैसा कि वास्तव में असाधारण मामलों (> 2) के लिए वर्णित है। यदि सेट और पेड़ में मौजूद है, तो पेड़ में मूल्य का उपयोग करें; यदि सेट में मौजूद है और पेड़ नहीं है, तो मान 2 का उपयोग करें, अन्यथा (बिट में मौजूद नहीं) अपने बिटवेक्टर में देखें। ट्री में केवल 100000 तत्व (बाइट्स) होंगे। सेट में 500000 तत्व शामिल हैं (लेकिन कोई मान नहीं)। क्या इसकी बढ़ी हुई लागत को सही ठहराते हुए इसका आकार कम किया जाता है? (100% लुकअप सेट में दिखते हैं; 5% लुकअप को पेड़ में भी देखने की जरूरत है।)
davidbak

जब आप एक अपरिवर्तनीय पेड़ होते हैं, तो आप हमेशा सीएफबीएस-सॉर्ट किए गए सरणी का उपयोग करना चाहते हैं, इसलिए केवल डेटा के लिए नोड्स के लिए कोई आवंटन नहीं है।
o11c

5

यदि आप केवल रीड ऑपरेशंस करते हैं, तो बेहतर होगा कि किसी एक इंडेक्स को वैल्यू न दें, बल्कि इंडेक्स के अंतराल पर।

उदाहरण के लिए:

[0, 15000] = 0
[15001, 15002] = 153
[15003, 26876] = 2
[25677, 31578] = 0
...

यह एक संरचना के साथ किया जा सकता है। यदि आप एक OO दृष्टिकोण पसंद करते हैं, तो आप इसके समान एक वर्ग को परिभाषित करना चाह सकते हैं।

class Interval{
  private:
    uint32_t start; // First element of interval
    uint32_t end; // Last element of interval
    uint8_t value; // Assigned value

  public:
    Interval(uint32_t start, uint32_t end, uint8_t value);
    bool isInInterval(uint32_t item); // Checks if item lies within interval
    uint8_t getValue(); // Returns the assigned value
}

अब आपको बस अंतराल की एक सूची को गढ़ना होगा और जांचना होगा कि क्या आपका सूचकांक उनमें से एक के भीतर है जो औसत से कम स्मृति गहन हो सकता है, लेकिन अधिक सीपीयू संसाधनों की लागत होती है।

Interval intervals[INTERVAL_COUNT];
intervals[0] = Interval(0, 15000, 0);
intervals[1] = Interval(15001, 15002, 153);
intervals[2] = Interval(15003, 26876, 2);
intervals[3] = Interval(25677, 31578, 0);
...

uint8_t checkIntervals(uint32_t item)

    for(int i=0; i<INTERVAL_COUNT-1; i++)
    {
        if(intervals[i].isInInterval(item) == true)
        {
            return intervals[i].getValue();
        }
    }
    return DEFAULT_VALUE;
}

यदि आप आकार के अवरोही अंतराल का आदेश देते हैं तो आप इस संभावना को बढ़ाते हैं कि आप जिस वस्तु की तलाश कर रहे हैं वह जल्दी मिल जाती है जो आपकी औसत मेमोरी और सीपीयू संसाधन उपयोग को कम कर देती है।

आप 1 के आकार के साथ सभी अंतराल भी निकाल सकते हैं। संबंधित मानों को मानचित्र में रखें और उन्हें केवल तभी जांचें जब आप जिस आइटम की तलाश कर रहे हैं वह अंतराल में नहीं मिला था। इससे औसत प्रदर्शन भी थोड़ा बढ़ जाना चाहिए।


4
दिलचस्प विचार (+1) लेकिन मुझे कुछ संदेह है कि यह ओवरहेड को उचित ठहराएगा जब तक कि 0 और / या 1 के लंबे रन के बहुत सारे रन न हों। वास्तव में आप डेटा की रन-लंबाई एन्कोडिंग का उपयोग करने का सुझाव दे रहे हैं। यह कुछ स्थितियों में अच्छा हो सकता है लेकिन शायद इस समस्या के लिए एक अच्छा सामान्य दृष्टिकोण नहीं है।
जॉन कोलमैन

सही। यादृच्छिक अभिगम के लिए विशेष रूप से, यह निश्चित रूप से एक साधारण सरणी की तुलना में धीमा है या unt8_t, भले ही यह बहुत कम स्मृति लेता है।
१४:०४

4

बहुत समय पहले, मैं सिर्फ याद कर सकता हूं ...

विश्वविद्यालय में हमें एक किरण अनुरेखक कार्यक्रम में तेजी लाने के लिए एक कार्य मिला, जिसे बफर एरे से बार-बार एल्गोरिदम द्वारा पढ़ना पड़ता है। एक मित्र ने मुझसे कहा कि हमेशा रैम-रीड का उपयोग करें जो 4Bytes के गुणक हैं। इसलिए मैंने [X1, y1, z1,0, x2, y2, z2] के पैटर्न के लिए [X1, y1, z1, x2, y2, z2, ..., xn, yn, zn] के पैटर्न से ऐरे को बदल दिया। , 0, ..., xn, yn, Zn, 0]। इसका मतलब है कि मैं प्रत्येक 3 डी समन्वय के बाद एक खाली क्षेत्र जोड़ता हूं। कुछ प्रदर्शन परीक्षण के बाद: यह तेज था। इतनी लंबी कहानी छोटी: रैम से अपने एरे से कई बाइट्स पढ़ें, और शायद सही शुरुआती स्थिति से भी, इसलिए आप एक छोटा क्लस्टर पढ़ते हैं जहाँ खोजा गया इंडेक्स उसमें होता है और खोजे गए इंडेक्स को सीपीयू में इस छोटे क्लस्टर से पढ़ें। (आपके मामले में आपको फिल-फ़ील्ड सम्मिलित करने की आवश्यकता नहीं होगी, लेकिन अवधारणा स्पष्ट होनी चाहिए)

हो सकता है कि नए सिस्टम में अन्य गुणकों की भी कुंजी हो।

मुझे नहीं पता कि यह आपके मामले में काम करेगा, इसलिए यदि यह काम नहीं करता है: क्षमा करें। अगर यह काम करता है तो मुझे कुछ परीक्षा परिणामों के बारे में सुनकर खुशी होगी।

PS: ओह और यदि कोई एक्सेस पैटर्न या पास के एक्सेस किए गए इंडेक्स हैं, तो आप कैश्ड क्लस्टर का फिर से उपयोग कर सकते हैं।

PPS: यह हो सकता है, कि कई कारक 16Bytes की तरह अधिक थे या ऐसा कुछ, यह बहुत पहले की बात है, जिसे मैं बिल्कुल याद रख सकता हूं।


आप शायद कैशेलिंस के बारे में सोच रहे हैं, जो आमतौर पर 32 या 64 बाइट्स होते हैं, लेकिन अभ्यस्त यादृच्छिकता के रूप में यहां बहुत मदद करते हैं।
सुर्त

3

इसे देखते हुए, आप अपने डेटा को विभाजित कर सकते हैं, उदाहरण के लिए:

  • एक बिटसेट जो अनुक्रमित हो जाता है और मान 0 का प्रतिनिधित्व करता है (std :: वेक्टर यहां उपयोगी होगा)
  • एक बिटसेट जो अनुक्रमित हो जाता है और मान 1 का प्रतिनिधित्व करता है
  • एक std :: 2 के मान के लिए वेक्टर, जिसमें इंडेक्स होते हैं जो इस मान को संदर्भित करते हैं
  • अन्य मूल्यों के लिए एक नक्शा (या std :: वेक्टर>)

इस स्थिति में, सभी मान किसी दिए गए अनुक्रमणिका तक दिखाई देते हैं, इसलिए आप एक बिटसेट को भी हटा सकते हैं और अन्य में अनुपलब्ध होने के रूप में मान का प्रतिनिधित्व करते हैं।

यह आपको इस मामले के लिए कुछ मेमोरी को बचाएगा, हालांकि सबसे खराब केस को बदतर बना देगा। लुकअप करने के लिए आपको अधिक CPU पावर की भी आवश्यकता होगी।

मापने के लिए सुनिश्चित करें!


1
लोगों के लिए एक बिटसेट / शून्य। दो के लिए सूचकांकों का एक सेट। और बाकी के लिए एक विरल साहचर्य सरणी।
रेड। वेव

यह संक्षिप्त सारांश है
JVApen

ओपी को शर्तों को बताने दें, इसलिए वह प्रत्येक के वैकल्पिक कार्यान्वयन की खोज कर सकता है।
रेड। वेव

2

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

यह कहा, दो तंत्र हैं:

  • बिट सरण; यानी, यदि आपके पास केवल दो मूल्य थे, तो आप अपने सरणी को 8 के कारक से तुच्छ रूप से संपीड़ित कर सकते थे; यदि आपके पास 4 मान हैं (या "3 मान + सब कुछ") तो आप दो के एक कारक से संपीड़ित कर सकते हैं। जो सिर्फ परेशानी के लायक नहीं हो सकता है और इसके लिए बेंचमार्क की आवश्यकता होगी, खासकर यदि आपके पास वास्तव में यादृच्छिक एक्सेस पैटर्न हैं जो आपके कैश से बचते हैं और इसलिए एक्सेस समय को बिल्कुल भी नहीं बदलते हैं।
  • (index,value)या (value,index)टेबल। Ie, 1% मामले के लिए एक बहुत छोटी तालिका है, शायद 5% मामले के लिए एक तालिका (जिसमें केवल अनुक्रमणिका को सभी समान मान रखने की आवश्यकता है), और अंतिम दो मामलों के लिए एक बड़ी संकुचित बिट सरणी। और "टेबल" के साथ मेरा मतलब कुछ है जो अपेक्षाकृत जल्दी देखने की अनुमति देता है; यानी, शायद एक हैश, एक द्विआधारी पेड़, और इसी तरह, जो आपके पास उपलब्ध है और आपकी वास्तविक जरूरतों पर निर्भर करता है। यदि ये सबटाइब आपके 1/2 लेवल के कैश में फिट हो जाते हैं, तो आप भाग्यशाली हो सकते हैं।

1

मैं C से बहुत परिचित नहीं हूं, लेकिन C ++ में आप 0 - 255 की सीमा में पूर्णांक का प्रतिनिधित्व करने के लिए अहस्ताक्षरित चार का उपयोग कर सकते हैं ।

सामान्य इंट (फिर से, मैं जावा और सी ++ दुनिया से आ रहा हूं ) की तुलना में जिसमें 4 बाइट (32 बिट) की आवश्यकता होती है, एक अहस्ताक्षरित चार को 1 बाइट (8 बिट) की आवश्यकता होती है । इसलिए यह सरणी के कुल आकार को 75% तक कम कर सकता है।


यह शायद पहले से ही उपयोग के साथ मामला है uint8_t - 8 का मतलब 8 बिट्स है।
पीटर मोर्टेंसन

-4

आपने अपने सरणी की सभी वितरण विशेषताओं का संक्षिप्त वर्णन किया है; सरणी टॉस

आप आसानी से एक यादृच्छिक विधि के साथ सरणी को बदल सकते हैं जो सरणी के समान ही संभाव्य आउटपुट का उत्पादन करता है।

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


18
मुझे संदेह है कि "रैंडम एक्सेस" का उपयोग यहां यह बताने के लिए किया जा रहा था कि एक्सेस अप्रत्याशित हैं, यह नहीं कि वे वास्तव में यादृच्छिक हैं। (यानी यह "रैंडम एक्सेस फाइल्स" के अर्थ में है)
माइकल

हाँ, यह संभावना है। हालांकि ओपी स्पष्ट नहीं है। यदि ओपी की पहुंच किसी भी तरह से यादृच्छिक नहीं है, तो अन्य उत्तरों के अनुसार, विरल सरणी का कुछ रूप इंगित किया गया है।
दुओथोमस

1
मुझे लगता है कि आपके पास एक बिंदु है, क्योंकि ओपी ने संकेत दिया कि वह एक यादृच्छिक क्रम में पूरे सरणी पर लूप करेगा। इस मामले के लिए कि केवल वितरण को देखने की आवश्यकता है, यह एक अच्छा जवाब है।
इंगो शल्क-स्कूप
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.