การเพิ่มประสิทธิภาพใด ๆ สำหรับการเข้าถึงแบบสุ่มในอาร์เรย์ขนาดใหญ่มากเมื่อค่าใน 95% ของเคสเป็น 0 หรือ 1?


133

มีการเพิ่มประสิทธิภาพที่เป็นไปได้สำหรับการเข้าถึงแบบสุ่มในอาร์เรย์ขนาดใหญ่มากหรือไม่ (ปัจจุบันฉันใช้uint8_tและกำลังถามว่ามีอะไรดีกว่า)

uint8_t MyArray[10000000];

เมื่อค่าที่ตำแหน่งใด ๆ ในอาร์เรย์คือ

  • 0หรือ1สำหรับ95%ของทุกกรณี
  • 2ใน4%ของกรณี
  • ระหว่าง3ถึง255ในอีก1%ของกรณี?

มีอะไรดีไปกว่าuint8_tอาร์เรย์ที่จะใช้สำหรับสิ่งนี้หรือไม่? ควรเร็วที่สุดเท่าที่จะเป็นไปได้ในการวนซ้ำอาร์เรย์ทั้งหมดตามลำดับแบบสุ่มและนี่หนักมากกับแบนด์วิดท์ RAM ดังนั้นเมื่อมีเธรดมากกว่าสองสามเธรดที่ทำเช่นนั้นในเวลาเดียวกันสำหรับอาร์เรย์ที่แตกต่างกันปัจจุบันแบนด์วิดท์ RAM ทั้งหมด อิ่มตัวอย่างรวดเร็ว

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


36
สองบิต (0/1 / ดูแฮชแท็ก) และแฮชแท็กสำหรับค่าที่มากกว่า 1?
user253751

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

7
โดยพื้นฐานแล้วสิ่งที่คุณขอถึงจะเรียกว่าsparsity
Mateen Ulhaq

5
ต้องการข้อมูลเพิ่มเติม ... เหตุใดการเข้าถึงจึงเป็นแบบสุ่มและค่าที่ไม่ใช่ศูนย์เป็นไปตามรูปแบบหรือไม่?
Ext3h

4
@IwillnotexistIdonotexist ขั้นตอนการคำนวณล่วงหน้าจะดี แต่อาร์เรย์ควรได้รับการแก้ไขเป็นครั้งคราวดังนั้นขั้นตอนการคำนวณล่วงหน้าไม่ควรแพงเกินไป
JohnAl

คำตอบ:


155

ความเป็นไปได้ง่ายๆที่ควรคำนึงถึงคือเก็บอาร์เรย์ที่บีบอัดไว้ 2 บิตต่อค่าสำหรับกรณีทั่วไปและ 4 ไบต์ที่แยกจากกันต่อค่า (24 บิตสำหรับดัชนีองค์ประกอบดั้งเดิม 8 บิตสำหรับค่าจริงดังนั้น(idx << 8) | value)) เรียงลำดับอาร์เรย์สำหรับ คนอื่น ๆ

เมื่อคุณค้นหาค่าคุณต้องทำการค้นหาในอาร์เรย์ 2bpp (O (1)) ก่อน ถ้าคุณพบว่า 0, 1 หรือ 2 เป็นค่าที่คุณต้องการ ถ้าคุณพบ 3 หมายความว่าคุณต้องค้นหาในอาร์เรย์รอง ที่นี่คุณจะทำการค้นหาไบนารีเพื่อค้นหาดัชนีความสนใจของคุณเลื่อนไปทางซ้ายด้วย 8 (O (log (n) ด้วย n ขนาดเล็กเนื่องจากควรเป็น 1%) และดึงค่าจาก 4- ไบต์ thingie

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

สำหรับอาร์เรย์เช่นอาร์เรย์ที่คุณเสนอควรใช้เวลา 10,000000 / 4 = 2500000 ไบต์สำหรับอาร์เรย์แรกบวก 10,000000 * 1% * 4 B = 400000 ไบต์สำหรับอาร์เรย์ที่สอง ดังนั้น 2900000 ไบต์นั่นคือน้อยกว่าหนึ่งในสามของอาร์เรย์ดั้งเดิมและส่วนที่ใช้มากที่สุดจะถูกเก็บไว้ด้วยกันในหน่วยความจำซึ่งควรจะดีสำหรับการแคช (อาจพอดีกับ L3 ด้วยซ้ำ)

หากคุณต้องการแอดเดรสมากกว่า 24 บิตคุณจะต้องปรับแต่ง "ที่เก็บข้อมูลสำรอง" วิธีที่ไม่สำคัญในการขยายคือการมีอาร์เรย์พอยน์เตอร์ 256 องค์ประกอบเพื่อสลับบน 8 บิตบนสุดของดัชนีและส่งต่อไปยังอาร์เรย์ที่จัดเรียงดัชนี 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 พร้อมข้อมูลแบบสุ่มที่กระจายตาม OP ที่ระบุในโพสต์เริ่มต้นโครงสร้างข้อมูลของฉันจากนั้น:

  • ทำการค้นหาองค์ประกอบ 10M แบบสุ่มด้วยโครงสร้างข้อมูลของฉัน
  • ทำเช่นเดียวกันกับอาร์เรย์เดิม

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

สองบล็อกสุดท้ายนี้ซ้ำ 50 ครั้งและหมดเวลา ในตอนท้ายค่าเฉลี่ยและส่วนเบี่ยงเบนมาตรฐานสำหรับการค้นหาแต่ละประเภทจะถูกคำนวณและพิมพ์พร้อมกับ speedup (lookup_mean / array_mean)

ฉันรวบรวมโค้ดด้านบนด้วย g ++ 5.4.0 ( -O3 -staticรวมถึงคำเตือนบางอย่าง) บน Ubuntu 16.04 และรันบนบางเครื่อง ส่วนใหญ่ใช้ Ubuntu 16.04, Linux รุ่นเก่าบางรุ่น, Linux รุ่นใหม่บางรุ่น ฉันไม่คิดว่าระบบปฏิบัติการควรจะเกี่ยวข้องเลยในกรณีนี้

            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 MB) เป็นเครื่องสร้างกลางคืนในขณะนี้ค่อนข้างไม่ได้ใช้งาน Xeon E5-2697 (แคช 35 MB) เป็นเครื่องสำหรับการคำนวณประสิทธิภาพสูงในช่วงเวลาที่ไม่ได้ใช้งานเช่นกัน มันสมเหตุสมผลอาร์เรย์ดั้งเดิมพอดีกับแคชขนาดใหญ่ดังนั้นโครงสร้างข้อมูลขนาดกะทัดรัดจึงเพิ่มความซับซ้อนเท่านั้น
  3. ที่ด้านตรงข้ามของ "สเปกตรัมประสิทธิภาพ" - แต่ที่อีกครั้งอาร์เรย์จะเร็วขึ้นเล็กน้อยมี Celeron ผู้ต่ำต้อยที่ขับเคลื่อน NAS ของฉัน มันมีแคชน้อยมากจนทั้งอาร์เรย์หรือ "โครงสร้างอัจฉริยะ" ไม่พอดีกับมันเลย เครื่องอื่น ๆ ที่มีแคชเล็กพอจะทำงานได้ในทำนองเดียวกัน
  4. Xeon X5650 ต้องใช้ความระมัดระวัง - เป็นเครื่องเสมือนบนเซิร์ฟเวอร์เครื่องเสมือนซ็อกเก็ตคู่ที่ค่อนข้างยุ่ง อาจเป็นไปได้ว่าแม้ว่าในนามจะมีแคชในปริมาณที่เหมาะสม แต่ในช่วงเวลาของการทดสอบจะถูกจับจองโดยเครื่องเสมือนที่ไม่เกี่ยวข้องโดยสิ้นเชิงหลายครั้ง

7
@JohnAl คุณไม่จำเป็นต้องมีโครงสร้าง A uint32_tจะไม่เป็นไร การลบองค์ประกอบออกจากบัฟเฟอร์รองจะทำให้มันถูกจัดเรียงอย่างชัดเจน การแทรกองค์ประกอบสามารถทำได้ด้วยstd::lower_boundแล้วinsert(แทนที่จะต่อท้ายและเรียงลำดับใหม่ทั้งหมด) การอัปเดตทำให้อาร์เรย์ทุติยภูมิขนาดเต็มน่าสนใจยิ่งขึ้น - ฉันจะเริ่มด้วยสิ่งนั้นอย่างแน่นอน
Martin Bonner สนับสนุน Monica

6
@JohnAl เพราะมูลค่าคือ(idx << 8) + valคุณไม่ต้องกังวลเกี่ยวกับส่วนของมูลค่าเพียงแค่ใช้การเปรียบเทียบแบบตรง มันจะเสมอเปรียบเทียบน้อยกว่า((idx+1) << 8) + valและน้อยกว่า((idx-1) << 8) + val
มาร์ตินเนอร์ที่สนับสนุนโมนิกา

3
@JohnAl: หากเป็นประโยชน์ฉันได้เพิ่มpopulateฟังก์ชันที่ควรเติมข้อมูลmain_arrและsec_arrเป็นไปตามรูปแบบที่lookupคาดไว้ ฉันไม่ได้ลองจริงๆดังนั้นอย่าคาดหวังว่ามันจะทำงานได้อย่างถูกต้องจริงๆ :-); อย่างไรก็ตามควรให้แนวคิดทั่วไปแก่คุณ
Matteo Italia

6
ฉันให้ +1 นี้เพื่อการเปรียบเทียบเท่านั้น ยินดีที่ได้เห็นคำถามเกี่ยวกับประสิทธิภาพและผลลัพธ์สำหรับโปรเซสเซอร์หลายประเภทด้วยเช่นกัน! ดี!
Jack Aidley

2
@JohnAI คุณควรกำหนดโปรไฟล์สำหรับกรณีการใช้งานจริงของคุณและไม่มีอะไรอื่น ความเร็วห้องสีขาวไม่สำคัญ
Jack Aidley

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ใช้ 2 บิตต่อองค์ประกอบโดยมีค่า 3 หมายถึง "อื่น ๆ "

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


4
ฉันจะบอกว่านั่นเป็นการประนีประนอมที่ดีในการรับแคช Hit ให้ได้มากที่สุด (เนื่องจากโครงสร้างที่ลดลงสามารถพอดีกับแคชได้ง่ายขึ้น) โดยไม่เสียเวลาในการเข้าถึงแบบสุ่มมากนัก
meneldal

ฉันคิดว่าสิ่งนี้สามารถปรับปรุงเพิ่มเติมได้ ฉันประสบความสำเร็จในอดีตด้วยปัญหาที่คล้ายกัน แต่แตกต่างกันซึ่งการใช้ประโยชน์จากการคาดเดาสาขาช่วยได้มาก อาจช่วยแยกออกif(code != 3) return code;เป็นif(code == 0) return 0; if(code==1) return 1; if(code == 2) return 2;
kutschkem

@kutschkem: ในกรณีนั้น__builtin_expect& co หรือ PGO ก็ช่วยได้เช่นกัน
Matteo Italia

23

นี่เป็น "ความคิดเห็นยาว ๆ " มากกว่าคำตอบที่เป็นรูปธรรม

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

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

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

ในหลาย ๆ กรณีการประนีประนอมระหว่าง "ความเร็วและขนาดเล็ก" เป็นหนึ่งในสิ่งที่คุณต้องเลือกระหว่างวิศวกรรมซอฟต์แวร์ [ในวิศวกรรมอื่น ๆ ก็ไม่จำเป็นต้องประนีประนอมกันมากนัก] ดังนั้น "การสิ้นเปลืองหน่วยความจำสำหรับโค้ดที่ง่ายกว่า" จึงเป็นทางเลือกที่ดีกว่า ในแง่นี้วิธีแก้ปัญหา "แบบธรรมดา" น่าจะดีกว่าสำหรับความเร็ว แต่ถ้าคุณใช้ RAM "ดีกว่า" การปรับขนาดของตารางให้เหมาะสมจะทำให้คุณมีประสิทธิภาพที่เพียงพอและมีการปรับปรุงขนาดที่ดี มีหลายวิธีที่คุณสามารถทำได้ตามที่แนะนำไว้ในความคิดเห็นช่อง 2 บิตที่เก็บค่าที่พบบ่อยที่สุดสองหรือสามค่าจากนั้นรูปแบบข้อมูลทางเลือกสำหรับค่าอื่น ๆ ตารางแฮชจะเป็นของฉัน แนวทางแรก แต่รายการหรือต้นไม้ไบนารีอาจใช้งานได้เช่นกัน - อีกครั้ง ขึ้นอยู่กับรูปแบบที่ "not 0, 1 หรือ 2" ของคุณอยู่ อีกครั้งขึ้นอยู่กับว่าค่านั้น "กระจัดกระจาย" อย่างไรในตาราง - อยู่ในกลุ่มหรือมีรูปแบบการกระจายอย่างเท่าเทียมกันมากกว่ากัน?

แต่ปัญหาคือคุณยังอ่านข้อมูลจาก RAM อยู่ จากนั้นคุณจะใช้จ่ายโค้ดมากขึ้นในการประมวลผลข้อมูลรวมถึงโค้ดบางส่วนเพื่อรับมือกับ "นี่ไม่ใช่ค่าทั่วไป"

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

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


ขอบคุณ! ในท้ายที่สุดฉันแค่สนใจว่าอะไรจะเร็วขึ้นเมื่อ 100% ของ CPU ยุ่งอยู่กับการวนลูปเหนืออาร์เรย์ดังกล่าว (เธรดที่แตกต่างกันในอาร์เรย์ที่แตกต่างกัน) ขณะนี้ด้วยuint8_tอาร์เรย์แบนด์วิดท์ RAM จะอิ่มตัวหลังจาก ~ 5 เธรดกำลังทำงานในเวลาเดียวกัน (บนระบบควอดแชนเนล) ดังนั้นการใช้มากกว่า 5 เธรดจึงไม่ให้ประโยชน์อีกต่อไป ฉันต้องการให้สิ่งนี้ใช้> 10 เธรดโดยไม่พบปัญหาแบนด์วิดท์ RAM แต่ถ้าด้าน CPU ของการเข้าถึงช้ามากจน 10 เธรดทำงานได้น้อยกว่า 5 เธรดก่อนหน้านี้จะไม่คืบหน้าอย่างเห็นได้ชัด
JohnAl

@JohnAl คุณมีกี่คอร์? หากคุณถูกผูกไว้กับ CPU ไม่มีประเด็นใดที่จะมีเธรดมากกว่าคอร์ นอกจากนี้อาจถึงเวลาดูการเขียนโปรแกรม GPU?
Martin Bonner สนับสนุน Monica

@MartinBonner ตอนนี้ฉันมี 12 กระทู้ และฉันเห็นด้วยสิ่งนี้อาจทำงานได้ดีมากบน GPU
JohnAl

2
@JohnAI: หากคุณกำลังเรียกใช้กระบวนการที่ไม่มีประสิทธิภาพเดียวกันหลายเวอร์ชันในหลายเธรดคุณจะเห็นความคืบหน้าที่ จำกัด เสมอ จะมีชัยชนะที่ยิ่งใหญ่กว่าในการออกแบบอัลกอริทึมของคุณสำหรับการประมวลผลแบบขนานมากกว่าการปรับแต่งโครงสร้างการจัดเก็บ
Jack Aidley

13

สิ่งที่ฉันเคยทำในอดีตคือการใช้แฮชแมปหน้าบิตเซ็ต

สิ่งนี้จะลดพื้นที่ลงครึ่งหนึ่งเมื่อเทียบกับคำตอบของมัตเตโอ แต่อาจช้ากว่าหากการค้นหา "ข้อยกเว้น" ช้า (กล่าวคือมีข้อยกเว้นมากมาย)

อย่างไรก็ตามบ่อยครั้ง "cache is king"


2
แฮชแมปจะลดพื้นที่ลงครึ่งหนึ่งได้อย่างไรเมื่อเทียบกับคำตอบของมัตเตโอ ? สิ่งที่ควรมีในแฮชแมปนั้น
JohnAl

1
@JohnAl ใช้บิตเซ็ต 1 บิต = bitvec แทน bitvec 2 บิต
o11c

2
@ o11c ไม่แน่ใจว่าเข้าใจถูกหรือเปล่า คุณหมายถึงมีอาร์เรย์ของค่า 1 บิตโดยที่0หมายถึงดูmain_arrและ1หมายถึงดูที่sec_arr (ในกรณีของโค้ด Matteos)? นั่นจะต้องมีพื้นที่โดยรวมมากกว่าคำตอบของ Matteos เนื่องจากอาร์เรย์เพิ่มเติมหนึ่งชุด ฉันไม่ค่อยเข้าใจว่าคุณจะทำอย่างไรโดยใช้พื้นที่เพียงครึ่งเดียวเมื่อเทียบกับคำตอบของ Matteos
JohnAl

1
คุณช่วยชี้แจงเรื่องนี้ได้ไหม คุณมองขึ้นกรณี expectional แรกและจากนั้นก็มองไปในบิตแมป? ถ้าเป็นเช่นนั้นฉันสงสัยว่าการค้นหาช้าในแฮชจะทำให้ประหยัดได้มากขึ้นในการลดขนาดบิตแมป
Martin Bonner สนับสนุน Monica

ฉันคิดว่าสิ่งนี้เรียกว่าแฮชลิงก์ - แต่ Google ไม่พบ Hit ที่เกี่ยวข้องดังนั้นจึงต้องเป็นอย่างอื่น วิธีการทำงานโดยทั่วไปคือการพูดอาร์เรย์แบบไบต์ที่จะเก็บค่าที่ส่วนใหญ่อยู่ระหว่าง 0..254 จากนั้นคุณจะใช้ 255 เป็นแฟล็กและหากคุณมีองค์ประกอบ 255 คุณจะต้องค้นหาค่าที่แท้จริงในตารางแฮชที่เกี่ยวข้อง มีใครจำได้ไหมว่ามันชื่อว่าอะไร? (ฉันคิดว่าฉันอ่านเกี่ยวกับเรื่องนี้ใน IBM TR รุ่นเก่า) อย่างไรก็ตามคุณสามารถจัดเรียงตามที่ @ o11c แนะนำ - ค้นหาในแฮชก่อนเสมอหากไม่มีให้ดูในบิตอาร์เรย์ของคุณ
davidbak

11

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

คำถามของคุณมีสองสมมติฐาน:

  1. ข้อมูลถูกจัดเก็บไม่ดีเนื่องจากคุณไม่ได้ใช้บิตทั้งหมด
  2. การจัดเก็บให้ดีขึ้นจะทำให้สิ่งต่างๆเร็วขึ้น

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

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


8

หากข้อมูลและการเข้าถึงมีการกระจายแบบสุ่มอย่างสม่ำเสมอประสิทธิภาพอาจขึ้นอยู่กับส่วนของการเข้าถึงที่หลีกเลี่ยงการพลาดแคชระดับภายนอก การเพิ่มประสิทธิภาพนั้นจะต้องรู้ว่าอาร์เรย์ขนาดใดที่สามารถรองรับในแคชได้อย่างน่าเชื่อถือ หากแคชของคุณมีขนาดใหญ่พอที่จะรองรับหนึ่งไบต์สำหรับทุกๆห้าเซลล์วิธีที่ง่ายที่สุดคือให้หนึ่งไบต์เก็บค่าที่เข้ารหัสห้าฐานสามค่าไว้ในช่วง 0-2 (มี 243 ชุดค่าผสม 5 ค่าดังนั้นจะ พอดีกับไบต์) พร้อมกับอาร์เรย์ 10,000,000 ไบต์ที่จะถูกสอบถามเมื่อใดก็ตามที่ค่าฐาน 3 ระบุว่า "2"

หากแคชไม่ใหญ่ขนาดนั้น แต่สามารถรองรับได้หนึ่งไบต์ต่อ 8 เซลล์ก็จะไม่สามารถใช้ค่าหนึ่งไบต์เพื่อเลือกจากทั้งหมด 6,561 ชุดค่าฐาน 3 ที่เป็นไปได้ทั้งหมด 6,561 ชุด แต่เนื่องจากผลกระทบเพียงอย่างเดียวของ การเปลี่ยน 0 หรือ 1 เป็น 2 จะทำให้เกิดการค้นหาที่ไม่จำเป็นอย่างอื่นความถูกต้องไม่ต้องการการสนับสนุนทั้งหมด 6,561 แต่เราสามารถมุ่งเน้นไปที่ค่าที่ "มีประโยชน์" ที่สุด 256

โดยเฉพาะอย่างยิ่งถ้า 0 ธรรมดามากกว่า 1 หรือในทางกลับกันวิธีการที่ดีอาจใช้ 217 ค่าในการเข้ารหัสชุดค่าผสมของ 0 และ 1 ที่มี 1 หรือน้อยกว่า 16 ค่าเพื่อเข้ารหัส xxxx0000 ถึง xxxx1111, 16 เพื่อเข้ารหัส 0000xxxx ผ่าน 1111xxxx และอีกอันสำหรับ xxxxxxxx ค่าสี่ค่าจะยังคงอยู่สำหรับสิ่งที่ใช้อื่น ๆ อาจพบ หากข้อมูลมีการกระจายแบบสุ่มตามที่อธิบายไว้แบบสอบถามทั้งหมดส่วนใหญ่เล็กน้อยจะตีเป็นไบต์ซึ่งมีเพียงศูนย์และหนึ่ง (ประมาณ 2/3 ของกลุ่มทั้งหมดแปดบิตทั้งหมดจะเป็นศูนย์และประมาณ 7/8 ของ ซึ่งจะมีหกหรือน้อยกว่า 1 บิต); ส่วนใหญ่ที่ไม่ได้ลงจอดในไบต์ซึ่งมี x สี่ตัวและมีโอกาส 50% ที่จะลงจอดที่ศูนย์หรือหนึ่ง ดังนั้นแบบสอบถามประมาณหนึ่งในสี่เท่านั้นที่จำเป็นต้องมีการค้นหาอาร์เรย์ขนาดใหญ่

หากข้อมูลถูกกระจายแบบสุ่ม แต่แคชไม่ใหญ่พอที่จะจัดการกับหนึ่งไบต์ต่อแปดองค์ประกอบเราสามารถลองใช้แนวทางนี้กับแต่ละไบต์ที่จัดการมากกว่าแปดรายการ แต่เว้นแต่จะมีความเอนเอียงที่รุนแรงไปทาง 0 หรือไปทาง 1 เศษส่วนของค่าที่สามารถจัดการได้โดยไม่ต้องทำการค้นหาในอาร์เรย์ขนาดใหญ่จะลดขนาดลงเมื่อจำนวนที่จัดการโดยแต่ละไบต์เพิ่มขึ้น


7

ฉันจะเพิ่มคำตอบของ@ o11cเนื่องจากคำพูดของเขาอาจจะสับสนเล็กน้อย ถ้าฉันต้องการบีบบิตสุดท้ายและรอบ CPU ฉันจะทำสิ่งต่อไปนี้

เราจะเริ่มต้นด้วยการสร้างแผนผังการค้นหาแบบไบนารีที่สมดุลซึ่งเก็บกรณี "อย่างอื่น" ไว้ 5% สำหรับการค้นหาทุกครั้งคุณจะเดินไปตามต้นไม้อย่างรวดเร็ว: คุณมี 10,000,000 องค์ประกอบ: 5% อยู่ในต้นไม้ด้วยเหตุนี้โครงสร้างข้อมูลต้นไม้จึงมี 500000 องค์ประกอบ การเดินสิ่งนี้ในเวลา O (log (n)) ทำให้คุณทำซ้ำได้ 19 ครั้ง ฉันไม่ใช่ผู้เชี่ยวชาญเรื่องนี้ แต่ฉันเดาว่ามีการใช้งานหน่วยความจำที่มีประสิทธิภาพอยู่ที่นั่น ลองเดาดู:

  • ต้นไม้ที่สมดุลดังนั้นจึงสามารถคำนวณตำแหน่งทรีย่อยได้ (ดัชนีไม่จำเป็นต้องเก็บไว้ในโหนดของต้นไม้) วิธีเดียวกับที่ฮีป (โครงสร้างข้อมูล) ถูกเก็บไว้ในหน่วยความจำเชิงเส้น
  • ค่า 1 ไบต์ (2 ถึง 255)
  • 3 ไบต์สำหรับดัชนี (10,000,000 ใช้เวลา 23 บิตซึ่งพอดีกับ 3 ไบต์)

ผลรวม 4 ไบต์: 500000 * 4 = 1953 kB พอดีแคช!

สำหรับกรณีอื่น ๆ ทั้งหมด (0 หรือ 1) คุณสามารถใช้ bitvector โปรดทราบว่าคุณไม่สามารถละทิ้งเคสอื่น ๆ อีก 5% สำหรับการเข้าถึงแบบสุ่ม: 1.19 MB

การรวมกันของทั้งสองนี้ใช้ประมาณ 3,099 MB เมื่อใช้เทคนิคนี้คุณจะประหยัดหน่วยความจำปัจจัย 3.08

อย่างไรก็ตามสิ่งนี้ไม่สามารถเอาชนะคำตอบของ@Matteo Italia (ซึ่งใช้ 2.76 MB) น่าเสียดาย มีอะไรที่เราสามารถทำได้เพิ่มเติมหรือไม่? ส่วนที่ใช้หน่วยความจำมากที่สุดคือดัชนี 3 ไบต์ในแผนภูมิ ถ้าเราลดลงเป็น 2 ได้เราจะประหยัดได้ 488 kB และการใช้หน่วยความจำทั้งหมดจะเท่ากับ 2.622 MB ซึ่งเล็กกว่า!

เราจะทำเช่นนี้ได้อย่างไร? เราต้องลดการสร้างดัชนีเป็น 2 ไบต์ อีกครั้ง 10,000,000 ใช้เวลา 23 บิต เราต้องสามารถดร็อป 7 บิตได้ เราสามารถทำได้โดยการแบ่งช่วงของ 10,000,000 องค์ประกอบออกเป็น 2 ^ 7 (= 128) ภูมิภาคของ 78125 องค์ประกอบ ตอนนี้เราสามารถสร้างต้นไม้ที่สมดุลสำหรับแต่ละภูมิภาคเหล่านี้โดยเฉลี่ยแล้ว 3906 องค์ประกอบ การเลือกต้นไม้ที่ถูกต้องทำได้โดยการแบ่งดัชนีเป้าหมายอย่างง่าย ๆ ด้วย 2 ^ 7 (หรือบิตชิฟต์>> 7) ตอนนี้ดัชนีที่จำเป็นในการจัดเก็บสามารถแสดงด้วย 16 บิตที่เหลือ โปรดทราบว่ามีค่าใช้จ่ายบางส่วนสำหรับความยาวของต้นไม้ที่ต้องจัดเก็บ แต่ก็ไม่สำคัญ นอกจากนี้โปรดทราบว่ากลไกการแยกนี้ช่วยลดจำนวนการวนซ้ำที่ต้องการในการเดินต้นไม้ซึ่งตอนนี้ลดการทำซ้ำน้อยลงเหลือ 7 ครั้งเนื่องจากเราลดลง 7 บิต: เหลือการทำซ้ำ 12 ครั้งเท่านั้น

โปรดทราบว่าในทางทฤษฎีคุณสามารถทำขั้นตอนซ้ำเพื่อตัด 8 บิตถัดไปได้ แต่คุณต้องสร้างต้นไม้ที่สมดุล 2 ^ 15 โดยเฉลี่ยประมาณ 305 องค์ประกอบ สิ่งนี้จะส่งผลให้มีขนาด 2.143 MB โดยมีการทำซ้ำเพียง 4 ครั้งในการเดินต้นไม้ซึ่งเป็นการเพิ่มความเร็วที่มากเมื่อเทียบกับการทำซ้ำ 19 ครั้งที่เราเริ่มต้นด้วย

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


1
องอาจพยายาม!
davidbak

1
ลองสิ่งนี้: เนื่องจาก 4% ของกรณีและปัญหาเป็นค่า 2 ... สร้างชุดของกรณีพิเศษ (> 1) สร้างต้นไม้ตามที่อธิบายไว้สำหรับกรณีพิเศษจริงๆ (> 2) ถ้าอยู่ใน set และ tree ให้ใช้ value ใน tree หากมีอยู่ในชุดและไม่ใช่ต้นไม้ให้ใช้ค่า 2 มิฉะนั้น (ไม่มีอยู่ในชุด) ค้นหาใน bitvector ของคุณ ทรีจะมีเพียง 100,000 องค์ประกอบ (ไบต์) ชุดประกอบด้วย 500000 องค์ประกอบ (แต่ไม่มีค่าเลย) สิ่งนี้ช่วยลดขนาดในขณะที่แสดงต้นทุนที่เพิ่มขึ้นหรือไม่? (100% ของการค้นหาดูเป็นชุด 5% ของการค้นหาต้องดูในแผนภูมิด้วย)
davidbak

คุณต้องการใช้อาร์เรย์ที่เรียงลำดับ CFBS เสมอเมื่อคุณมีโครงสร้างที่ไม่เปลี่ยนรูปดังนั้นจึงไม่มีการจัดสรรสำหรับโหนดเพียงแค่ข้อมูล
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
}

ตอนนี้คุณต้องทำซ้ำตามรายการช่วงเวลาและตรวจสอบว่าดัชนีของคุณอยู่ในช่วงใดช่วงหนึ่งซึ่งอาจใช้หน่วยความจำน้อยกว่ามากโดยเฉลี่ย แต่ต้องใช้ทรัพยากร CPU มากขึ้น

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

หากคุณจัดลำดับช่วงเวลาตามขนาดจากมากไปหาน้อยคุณจะเพิ่มความเป็นไปได้ที่รายการที่คุณกำลังมองหาจะถูกพบในช่วงต้นซึ่งจะทำให้หน่วยความจำเฉลี่ยและการใช้ทรัพยากร CPU ของคุณลดลง

คุณยังสามารถลบช่วงเวลาทั้งหมดด้วยขนาด 1 ใส่ค่าที่เกี่ยวข้องลงในแผนที่และตรวจสอบเฉพาะในกรณีที่ไม่พบรายการที่คุณกำลังมองหาในช่วงเวลานั้น ๆ นอกจากนี้ยังควรเพิ่มประสิทธิภาพโดยเฉลี่ยอีกเล็กน้อย


4
ความคิดที่น่าสนใจ (+1) แต่ฉันค่อนข้างสงสัยว่ามันจะปรับค่าโสหุ้ยได้เว้นแต่จะมีการวิ่ง 0 และ / หรือการรัน 1 ที่ยาวเป็นจำนวนมาก คุณกำลังแนะนำให้ใช้การเข้ารหัสข้อมูลแบบรันไทม์ อาจดีในบางสถานการณ์ แต่อาจไม่ใช่แนวทางทั่วไปที่ดีสำหรับปัญหานี้
John Coleman

ขวา. โดยเฉพาะอย่างยิ่งสำหรับการเข้าถึงแบบสุ่มสิ่งนี้เกือบจะช้ากว่าอาร์เรย์ธรรมดาหรือunt8_tแม้ว่าจะใช้หน่วยความจำน้อยกว่ามากก็ตาม
leftaround ประมาณ

4

นานมาแล้วฉันจำได้ ...

ในมหาวิทยาลัยเรามีภารกิจในการเร่งโปรแกรม ray tracer ซึ่งต้องอ่านอัลกอริทึมซ้ำแล้วซ้ำอีกจากอาร์เรย์บัฟเฟอร์ เพื่อนบอกให้ใช้ RAM ที่อ่านเป็นทวีคูณของ 4Bytes เสมอ ดังนั้นฉันจึงเปลี่ยนอาร์เรย์จากรูปแบบของ [x1, y1, z1, x2, y2, z2, ... , xn, yn, zn] เป็นรูปแบบของ [x1, y1, z1,0, x2, y2, z2 , 0, ... , xn, yn, zn, 0] หมายความว่าฉันเพิ่มฟิลด์ว่างหลังพิกัด 3 มิติแต่ละอัน หลังจากการทดสอบประสิทธิภาพบางส่วน: เร็วขึ้น เรื่องสั้นสั้น ๆ : อ่านหลาย ๆ 4 ไบต์จากอาร์เรย์ของคุณจาก RAM และอาจมาจากตำแหน่งเริ่มต้นที่ถูกต้องด้วยดังนั้นคุณจึงอ่านคลัสเตอร์เล็ก ๆ ที่ดัชนีการค้นหาอยู่ในนั้นและอ่านดัชนีการค้นหาจากคลัสเตอร์เล็ก ๆ นี้ในซีพียู (ในกรณีของคุณคุณไม่จำเป็นต้องแทรกช่องกรอกข้อมูล แต่แนวคิดควรชัดเจน)

บางทีการทวีคูณอื่น ๆ อาจเป็นกุญแจสำคัญในระบบที่ใหม่กว่า

ฉันไม่รู้ว่าจะใช้ได้กับกรณีของคุณหรือไม่ดังนั้นหากไม่ได้ผล: ขออภัย หากได้ผลฉันยินดีที่จะได้ยินเกี่ยวกับผลการทดสอบบางอย่าง

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

PPS: อาจเป็นไปได้ว่าตัวประกอบหลาย ๆ ตัวนั้นเหมือน 16 ไบต์หรืออะไรทำนองนั้นมันนานเกินไปแล้วที่ฉันจำได้แม่น


คุณอาจกำลังคิดถึงแคชไลน์ซึ่งโดยปกติจะมีขนาด 32 หรือ 64 ไบต์ แต่จะไม่ช่วยได้มากนักเนื่องจากการเข้าถึงเป็นแบบสุ่ม
Surt

3

เมื่อพิจารณาถึงสิ่งนี้คุณสามารถแยกข้อมูลของคุณได้เช่น:

  • บิตเซ็ตที่ได้รับการจัดทำดัชนีและแสดงถึงค่า 0 (std :: vector จะมีประโยชน์ที่นี่)
  • บิตเซ็ตที่ได้รับการจัดทำดัชนีและแสดงถึงค่า 1
  • std :: vector สำหรับค่า 2 ซึ่งมีดัชนีที่อ้างถึงค่านี้
  • แผนที่สำหรับค่าอื่น ๆ (หรือ std :: vector>)

ในกรณีนี้ค่าทั้งหมดจะปรากฏจนถึงดัชนีที่กำหนดดังนั้นคุณสามารถลบบิตเซ็ตหนึ่งออกและแสดงค่าตามที่ขาดหายไปในอีกค่าหนึ่ง

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

วัดใจ!


1
บิตเซ็ตสำหรับคน / ศูนย์ ชุดของดัชนีสำหรับสอง และอาร์เรย์เชื่อมโยงแบบเบาบางสำหรับส่วนที่เหลือ
Red.Wave

นั่นคือสรุปสั้น ๆ
JVApen

แจ้งให้ OP ทราบข้อกำหนดเพื่อที่เขาจะได้ค้นหาการใช้งานทางเลือกของแต่ละข้อ
แดงเวฟ

2

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

ที่กล่าวว่ามีกลไกสองอย่างในใจ:

  • อาร์เรย์บิต กล่าวคือถ้าคุณมีเพียงสองค่าคุณสามารถบีบอัดอาร์เรย์ของคุณได้เล็กน้อยด้วยค่าตัวคูณ 8 ถ้าคุณมี 4 ค่า (หรือ "3 ค่า + อย่างอื่น") คุณสามารถบีบอัดด้วยตัวคูณสอง ซึ่งก็อาจจะไม่คุ้มค่าปัญหาและจะต้องมาตรฐานโดยเฉพาะอย่างยิ่งถ้าคุณมีจริงๆรูปแบบการเข้าถึงแบบสุ่มซึ่งหลบหนีแคชของคุณและด้วยเหตุนี้ไม่เปลี่ยนเวลาในการเข้าถึงในทุก
  • (index,value)หรือ(value,index)ตาราง กล่าวคือมีตารางขนาดเล็กมากหนึ่งตารางสำหรับกรณี 1% อาจเป็นตารางหนึ่งตารางสำหรับกรณี 5% (ซึ่งจำเป็นต้องจัดเก็บดัชนีเนื่องจากทั้งหมดมีค่าเท่ากัน) และอาร์เรย์บิตบีบอัดขนาดใหญ่สำหรับสองกรณีสุดท้าย และด้วย "ตาราง" ฉันหมายถึงสิ่งที่ช่วยให้สามารถค้นหาได้ค่อนข้างรวดเร็ว เช่นอาจจะเป็นแฮชต้นไม้ไบนารีเป็นต้นขึ้นอยู่กับสิ่งที่คุณมีและความต้องการที่แท้จริงของคุณ หากตารางย่อยเหล่านี้ตรงกับแคชระดับที่ 1/2 ของคุณคุณอาจโชคดี

1

ฉันไม่ค่อยคุ้นเคยกับ C แต่ในC ++คุณสามารถใช้ถ่านที่ไม่ได้ลงนามเพื่อแทนจำนวนเต็มในช่วง 0-255

เมื่อเทียบกับintปกติ(อีกครั้งฉันมาจากโลกJavaและC ++ ) ซึ่งต้องการ4 ไบต์ (32 บิต) ถ่านที่ไม่ได้ลงชื่อต้องใช้1 ไบต์ (8 บิต) ดังนั้นจึงอาจลดขนาดทั้งหมดของอาร์เรย์ลง 75%


นั่นอาจเป็นกรณีที่มีการใช้uint8_t - 8 หมายถึง 8 บิต
Peter Mortensen

-4

คุณได้อธิบายลักษณะการกระจายทั้งหมดของอาร์เรย์ของคุณอย่างรวบรัด โยนอาร์เรย์

คุณสามารถแทนที่อาร์เรย์ได้อย่างง่ายดายด้วยวิธีการสุ่มที่สร้างผลลัพธ์ที่น่าจะเป็นเช่นเดียวกับอาร์เรย์

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


18
ฉันสงสัยว่ามีการใช้ "การเข้าถึงโดยสุ่ม" ที่นี่เพื่อระบุว่าการเข้าถึงนั้นไม่สามารถคาดเดาได้ไม่ใช่ว่าเป็นการเข้าถึงแบบสุ่มจริงๆ (กล่าวคือตั้งใจในความหมายของ "ไฟล์การเข้าถึงโดยสุ่ม")
Michael Kay

ใช่นั่นเป็นไปได้ OP ไม่ชัดเจน หากการเข้าถึงของ OP ไม่เป็นแบบสุ่มจะมีการระบุอาร์เรย์แบบกระจัดกระจายบางรูปแบบตามคำตอบอื่น ๆ
Dúthomhas

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