Vakaların% 95'indeki değer 0 veya 1 olduğunda, çok büyük bir dizide rastgele erişim için herhangi bir optimizasyon var mı?


133

Çok büyük bir dizide rastgele erişim için herhangi bir olası optimizasyon var mı (şu anda kullanıyorum uint8_tve neyin daha iyi olduğunu soruyorum)

uint8_t MyArray[10000000];

dizideki herhangi bir konumdaki değer

  • Tüm vakaların % 95'i için 0 veya 1 ,
  • 2 içinde % 4 olgu,
  • arasında 3 ve 255 diğer % 1 vakaların?

Peki bunun uint8_tiçin bir diziden daha iyi bir şey var mı? Tüm dizi üzerinde rastgele bir sırayla döngü yapmak mümkün olduğunca hızlı olmalıdır ve bu RAM bant genişliğinde çok ağırdır, bu nedenle bunu aynı anda farklı diziler için yapan birkaç iş parçacığına sahipken, şu anda tüm RAM bant genişliği hızla doyurulur.

Bu kadar büyük bir diziye (10 MB) sahip olmak çok verimsiz hissettirdiği için soruyorum çünkü% 5 dışında neredeyse tüm değerlerin 0 veya 1 olacağı biliniyor. Yani dizideki tüm değerlerin% 95'i gerçekte 8 bit yerine sadece 1 bit gerekir, bu bellek kullanımını neredeyse bir dereceye kadar azaltır. Bunun için gereken RAM bant genişliğini büyük ölçüde azaltacak ve sonuç olarak da rastgele erişim için önemli ölçüde daha hızlı olacak, daha verimli bir bellek çözümü olması gerektiği gibi geliyor.


36
İki bit (0/1 / hashtable'a bakın) ve 1'den büyük değerler için bir hashtable?
user253751

6
@ user202729 Neye bağlı? Sanırım bu, benim yaptığım gibi benzer bir şey yapmak zorunda olan herkes için ilginç bir soru, bu yüzden koduma çok özel bir yanıt değil, bunun için evrensel bir çözümden daha fazlasını görmek istiyorum. Bir şeye bağlıysa, neye bağlı olduğunu açıklayan bir cevaba sahip olmak iyi olur, böylece onu okuyan herkes kendi vakası için daha iyi bir çözüm olup olmadığını anlayabilir.
JohnAl

7
Esasen, sorduğunuz şeye seyreklik denir .
Mateen Ulhaq

5
Daha fazla bilgiye ihtiyaç var ... Erişim neden rastgele ve sıfır olmayan değerler bir model izliyor mu?
Ext3h

4
@IwillnotexistIdonotexist Bir ön hesaplama adımı iyi olur, ancak dizi yine de zaman zaman değiştirilmelidir, bu nedenle ön hesaplama adımı çok pahalı olmamalıdır.
JohnAl

Yanıtlar:


155

Akla gelen basit bir olasılık, genel durumlar için değer başına 2 bitlik sıkıştırılmış bir dizi ve değer başına ayrı bir 4 bayt (orijinal öğe dizini için 24 bit, gerçek değer için 8 bit, bu nedenle (idx << 8) | value)) için sıralı dizi tutmaktır. diğerleri.

Bir değer aradığınızda, önce 2bpp dizisinde (O (1)) bir arama yaparsınız; 0, 1 veya 2 bulursanız, istediğiniz değer budur; 3'ü bulursanız, ikincil dizide aramanız gerektiği anlamına gelir. Burada , ilgilendiğiniz dizini 8 (O (log (n) küçük bir n ile, çünkü bu% 1 olması gerekir) sola kaydırılmış olarak aramak için ikili bir arama yapacak ve değeri 4 bayt şey.

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

Önerdiğiniz gibi bir dizi için bu, ilk dizi için 10000000/4 = 2500000 bayt, artı ikinci dizi için 10000000 *% 1 * 4 B = 400000 bayt almalıdır; dolayısıyla 2900000 bayt, yani orijinal dizinin üçte birinden daha azı ve en çok kullanılan bölüm bir arada bellekte tutulur ve bu önbellekleme için iyi olmalıdır (L3'e bile sığabilir).

24 bitten fazla adreslemeye ihtiyacınız varsa, "ikincil depolamayı" ayarlamanız gerekir; bunu genişletmenin basit bir yolu, dizinin en üstteki 8 bitini değiştirmek ve yukarıdaki gibi 24 bitlik dizinlenmiş sıralı diziye iletmek için 256 elemanlı bir işaretçi dizisine sahip olmaktır.


Hızlı kıyaslama

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

(kod ve veriler her zaman Bitbucket'imde güncellenir)

Yukarıdaki kod, gönderilerinde OP belirtildiği gibi dağıtılan rastgele verilerle 10M öğe dizisini doldurur, veri yapımı başlatır ve ardından:

  • Veri yapımla rastgele 10 milyon öğe araması yapıyor
  • aynı şeyi orijinal dizi aracılığıyla yapar.

(Sıralı arama durumunda, yapabileceğiniz en önbellek dostu arama olduğundan, dizinin her zaman büyük ölçüde kazandığına dikkat edin)

Bu son iki blok 50 defa tekrarlanır ve zamanlanır; sonunda, her bir arama türü için ortalama ve standart sapma hesaplanır ve hızlandırma (aranan_ort / dizi_ortası) ile birlikte yazdırılır.

Yukarıdaki kodu -O3 -staticUbuntu 16.04 üzerinde g ++ 5.4.0 ( artı bazı uyarılar) ile derledim ve bazı makinelerde çalıştırdım; bunların çoğu Ubuntu 16.04, bazıları eski Linux, bazıları daha yeni Linux kullanıyor. İşletim sisteminin bu durumda hiç alakalı olması gerektiğini düşünmüyorum.

            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

Sonuçlar ... karışık!

  1. Genel olarak, bu makinelerin çoğunda bir tür hızlanma vardır veya en azından eşit düzeydedirler.
  2. Dizinin "akıllı yapı" aramasını gerçekten geride bıraktığı iki durum, çok fazla önbelleğe sahip ve özellikle meşgul olmayan makinelerde: Yukarıdaki Xeon E5-1650 (15 MB önbellek), şu anda oldukça boşta olan bir gece inşa makinesidir; Xeon E5-2697 (35 MB önbellek) boş bir anda da yüksek performanslı hesaplamalar için bir makinedir. Bu mantıklıdır, orijinal dizi tamamen büyük önbelleğine sığar, bu nedenle kompakt veri yapısı yalnızca karmaşıklık ekler.
  3. "Performans spektrumunun" zıt tarafında - ama yine dizinin biraz daha hızlı olduğu yerde, NAS'ıma güç veren mütevazı Celeron var; o kadar az önbelleğe sahip ki, ne dizi ne de "akıllı yapı" ona hiç uymuyor. Önbelleği yeterince küçük olan diğer makineler de benzer şekilde çalışır.
  4. Xeon X5650 biraz dikkatle alınmalıdır - oldukça yoğun bir çift soketli sanal makine sunucusundaki sanal makinelerdir; Normalde makul miktarda önbelleğe sahip olmasına rağmen, test sırasında birkaç kez tamamen ilgisiz sanal makineler tarafından önlenmekte olabilir.

7
@JohnAl Bir yapıya ihtiyacınız yok. A iyi uint32_tolacak. İkincil arabellekten bir öğeyi silmek, açıkça onu sıralı olarak bırakacaktır. Bir öğenin eklenmesi ve std::lower_boundardından yapılabilir insert(her şeyi eklemek ve yeniden sıralamak yerine). Güncellemeler, tam boyutlu ikincil diziyi çok daha çekici hale getiriyor - kesinlikle bununla başlayacağım.
Martin Bonner Monica'yı

6
@JohnAl Çünkü değer, (idx << 8) + valdeğer kısmı hakkında endişelenmenize gerek yok - sadece doğrudan bir karşılaştırma kullanın. Her zaman daha az ((idx+1) << 8) + valve daha az karşılaştıracak((idx-1) << 8) + val
Martin Bonner Monica'yı destekliyor

3
@JohnAl: bu yararlı olabilir, bir ilave populatedoldurmak gerekir fonksiyonu main_arrve sec_arrbu formata göre lookupbekler. Aslında denemedim, bu yüzden gerçekten doğru çalışmasını beklemeyin :-); her neyse, size genel bir fikir vermeli.
Matteo Italia

6
Bu + 1'i sadece kıyaslama için veriyorum. Verimlilik ve birden çok işlemci türü için de sonuçlarla ilgili bir soruda görmek güzel! Güzel!
Jack Aidley

2
@JohnAI Gerçek kullanım durumunuz için profilinizi belirlemelisiniz, başka hiçbir şey yapmamalısınız. Beyaz oda hızı önemli değil.
Jack Aidley

33

Başka bir seçenek olabilir

  • sonucun 0, 1 veya 2 olup olmadığını kontrol edin
  • düzenli bir arama yapmazsanız

Başka bir deyişle, şöyle bir şey:

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

burada bmapdeğer 3 anlamı ile eleman başına kullanım 2 bit "Diğer".

Bu yapının güncellenmesi önemsizdir,% 25 daha fazla bellek kullanır, ancak büyük kısmı yalnızca vakaların% 5'inde aranır. Elbette, her zamanki gibi, iyi bir fikir olup olmadığı birçok başka koşula bağlıysa, tek cevap gerçek kullanımla deney yapmaktır.


4
Rastgele erişim süresinden çok fazla şey kaybetmeden, mümkün olduğunca çok sayıda önbellek isabeti elde etmek için iyi bir uzlaşma olduğunu söyleyebilirim (azaltılmış yapı önbelleğe daha kolay sığabilir).
meneldal

Bunun daha da geliştirilebileceğini düşünüyorum. Geçmişte, şube tahmininden yararlanmanın çok yardımcı olduğu benzer ama farklı bir problemle başarı elde ettim. Bu bölmek için yardımcı olabilir if(code != 3) return code;içineif(code == 0) return 0; if(code==1) return 1; if(code == 2) return 2;
kutschkem

@kutschkem: bu durumda, __builtin_expect& co veya PGO da yardımcı olabilir.
Matteo Italia

23

Bu, somut bir cevaptan çok "uzun bir yorum" dur

Verileriniz iyi bilinen bir şey olmadıkça, herhangi birinin sorunuzu DOĞRUDAN yanıtlayabileceğinden şüpheliyim (ve açıklamanıza uyan hiçbir şeyin farkında değilim, ancak o zaman herkes için her tür veri modeli hakkında HER ŞEYİ bilmiyorum. kullanım türleri). Seyrek veri, yüksek performanslı hesaplamada yaygın bir sorundur, ancak tipik olarak "çok büyük bir dizimiz vardır, ancak yalnızca bazı değerler sıfır değildir".

Sizinki gibi iyi bilinmeyen kalıplar için, hiç kimse doğrudan hangisinin daha iyi olduğunu doğrudan BİLMEZ ve ayrıntılara bağlıdır: rastgele erişim ne kadar rastgele - sistem veri öğelerinin kümelerine erişiyor mu, yoksa tamamen rastgele mi? düzgün bir rasgele sayı üreteci. Tablo verileri tamamen rastgele mi, yoksa 0 dizileri ve diğer değerlerin saçılmasıyla 1 dizileri var mı? Çalışma uzunluğu kodlaması, makul derecede uzun 0 ve 1 dizileriniz varsa iyi çalışır, ancak "0/1 dama tahtasına" sahipseniz çalışmaz. Ayrıca, ilgili yere hızlı bir şekilde gidebilmeniz için bir "başlangıç ​​noktaları" tablosu tutmanız gerekir.

Uzun zamandan beri bazı büyük veritabanlarının RAM'de sadece büyük bir tablo olduğunu biliyorum (bu örnekte telefon santrali abone verileri) ve buradaki sorunlardan biri işlemcideki önbelleklerin ve sayfa tablosu optimizasyonlarının oldukça yararsız olmasıdır. Arayan, son zamanlarda birini arayanla çok nadiren aynıdır, herhangi bir türde önceden yüklenmiş veri yoktur, tamamen rastlantısaldır. Büyük sayfa tabloları, bu tür erişim için en iyi optimizasyondur.

Çoğu durumda, "hız ve küçük boyut" arasında taviz vermek, yazılım mühendisliğinde aralarından seçim yapmanız gereken şeylerden biridir [diğer mühendislikte bu çok da uzlaşma değildir]. Bu nedenle, "daha basit kod için hafızayı boşa harcamak" genellikle tercih edilen seçimdir. Bu anlamda, "basit" çözüm hız için oldukça daha iyidir, ancak RAM için "daha iyi" bir kullanımınız varsa, tablonun boyutunu optimize etmek size yeterli performans ve boyutta iyi bir gelişme sağlayacaktır. Bunu başarmanın birçok farklı yolu vardır - bir yorumda önerildiği gibi, en yaygın iki veya üç değerin depolandığı 2 bitlik bir alan ve ardından diğer değerler için bazı alternatif veri formatı - bir karma tablo benim olacaktır. ilk yaklaşım, ancak bir liste veya ikili ağaç da işe yarayabilir - yine, "0, 1 veya 2 değil" inizin nerede olduğuna göre değişir. Yine, bu, değerlerin tabloda nasıl "dağıldığına" bağlıdır - kümeler halinde mi yoksa daha eşit bir şekilde dağıtılmış bir model mi?

Ancak bununla ilgili bir sorun, verileri hala RAM'den okuyor olmanızdır. Daha sonra, "bu ortak bir değer değildir" ile başa çıkmak için bazı kodlar da dahil olmak üzere verileri işlemek için daha fazla kod harcarsınız.

En yaygın sıkıştırma algoritmalarındaki sorun, bunların paket açma dizilerine dayalı olmalarıdır, bu nedenle bunlara rasgele erişemezsiniz. Ve büyük verilerinizi tek seferde 256 girişlik parçalara ayırmanın ve 256'nın sıkıştırmasını bir uint8_t dizisinde açmanın, istediğiniz verileri getirmenin ve ardından sıkıştırılmamış verilerinizi atmanın ek yükü, size iyi bir sonuç vermesi pek olası değildir performans - bunun bir önemi olduğunu varsayarsak elbette.

Sonunda, test etmek, sorununuzu çözmeye yardımcı olup olmadığına veya bellek veriyolunun hala ana sınırlayıcı faktör olup olmadığına bakmak için muhtemelen yorumlardaki / yanıtlardaki fikirlerden birini veya birkaçını uygulamanız gerekecektir.


Teşekkürler! Sonunda, CPU'nun% 100'ü bu tür diziler üzerinde döngü yapmakla meşgul olduğunda (farklı diziler üzerinde farklı evreler) neyin daha hızlı olduğu ile ilgileniyorum. Şu anda, bir uint8_tdizi ile, RAM bant genişliği aynı anda ~ 5 iş parçacığı üzerinde çalıştıktan sonra (dört kanallı bir sistemde) doymuştur, bu nedenle 5'ten fazla iş parçacığı kullanmak artık herhangi bir fayda sağlamaz. Bunun RAM bant genişliği sorunlarına girmeden> 10 iş parçacığı kullanmasını isterim, ancak erişimin CPU tarafı o kadar yavaş olursa, 10 iş parçacığı daha önce 5 iş parçacığından daha az yapılırsa, bu açıkça ilerleme olmaz.
JohnAl

@JohnAl Kaç tane çekirdeğiniz var? CPU'ya bağlıysanız, çekirdeklerden daha fazla iş parçacığına sahip olmanın bir anlamı yoktur. Ayrıca, GPU programlamasına bakmanın zamanı olabilir mi?
Martin Bonner Monica'yı

@MartinBonner Şu anda 12 iş parçacığım var. Ve katılıyorum, bu muhtemelen bir GPU'da çok iyi çalışacaktır.
JohnAl

2
@JohnAI: Aynı verimsiz sürecin birden çok sürümünü birden çok iş parçacığı üzerinde çalıştırıyorsanız, her zaman sınırlı ilerleme göreceksiniz. Algoritmanızı paralel işleme için tasarlarken, bir depolama yapısını değiştirmekten daha büyük kazançlar olacaktır.
Jack Aidley

13

Geçmişte yaptığım şey, bir bit kümesinin önünde bir karma harita kullanmaktı.

Bu, Matteo'nun cevabına kıyasla boşluğu yarıya indirir, ancak "istisna" aramaları yavaşsa (yani birçok istisna vardır) daha yavaş olabilir.

Ancak çoğu zaman "önbellek kraldır".


2
Bir hashmap , Matteo'nun cevabına kıyasla alanı tam olarak nasıl yarıya indirirdi ? Bu karma haritada ne olmalı?
JohnAl

1
@JohnAl 2 bit bitvec yerine 1 bit bit kümesi = bitvec kullanma.
o11c

2
@ o11c Doğru anladığımdan emin değilim. Nerede 1 bitlik değer dizisi olması anlamına 0araçlar bakmakmain_arr ve 1araçlar bakmaksec_arr (Matteos kod durumda)? Bu, ek bir dizisinden bu yana, Matteos'un cevabından daha fazla alana ihtiyaç duyacaktır. Matteos cevabına kıyasla sadece yarı boşluğu kullanarak bunu nasıl yapacağını tam olarak anlamıyorum.
JohnAl

1
Bunu açıklar mısın? Sen expectional vakaları bakmak ilk ve ardından bitmap'te görünüyor? Öyleyse, hash'deki yavaş aramanın, bit eşlemin boyutunu küçültme tasarrufunu aşacağından şüpheleniyorum.
Martin Bonner Monica'yı destekliyor

Buna hashlinking denildiğini sanıyordum - ama google alakalı bir sonuç bulamadı, bu yüzden başka bir şey olmalı. Genelde çalışma şekli, büyük çoğunluğu örneğin 0..254 arasında olan değerleri tutan bir bayt dizisine sahip olmaktı. Sonra 255'i bayrak olarak kullanırsınız ve eğer bir 255 öğeniz varsa, ilişkili bir karma tablosunda gerçek değeri ararsınız. Birisi ona ne dendiğini hatırlayabilir mi? (Sanırım bunu eski bir IBM TR'de okudum.) Her neyse, bunu @ o11c'nin önerdiği şekilde de düzenleyebilirsiniz - her zaman önce hash'e bakın, eğer orada değilse, bit dizinize bakın.
davidbak

11

Verileriniz için bir model olmadıkça, makul bir hız veya boyut optimizasyonu olması olası değildir ve - normal bir bilgisayarı hedeflediğinizi varsayarsak - 10 MB zaten o kadar da önemli değil.

Sorularınızda iki varsayım var:

  1. Tüm bitleri kullanmadığınız için veriler yetersiz şekilde saklanıyor
  2. Daha iyi saklamak işleri hızlandırır.

Bu varsayımların her ikisinin de yanlış olduğunu düşünüyorum. Çoğu durumda, verileri depolamanın uygun yolu, en doğal temsili depolamaktır. Sizin durumunuzda, aradığınız şey bu: 0 ile 255 arasında bir sayı için bir bayt. Diğer tüm temsiller daha karmaşık olacak ve bu nedenle - diğer tüm şeyler eşit olacak şekilde - daha yavaş ve hataya daha yatkın olacaktır. Bu genel ilkeden sapmak için, verilerinizin% 95'inde potansiyel olarak altı "boşa harcanmış" bitten daha güçlü bir nedene ihtiyacınız var.

İkinci varsayımınız için, ancak ve ancak dizinin boyutunu değiştirmek, önemli ölçüde daha az önbellek kaybına neden olursa doğru olacaktır. Bunun gerçekleşip gerçekleşmeyeceği yalnızca çalışma kodunun profilini çıkararak kesin olarak belirlenebilir, ancak bence önemli bir fark yaratma olasılığı çok düşük. Her iki durumda da diziye rastgele erişeceğiniz için, işlemci her iki durumda da hangi veri bitlerini önbelleğe alacağını ve saklayacağını bilmekte zorlanacaktır.


8

Veriler ve erişimler tekdüze olarak rastgele dağıtılırsa, performans büyük olasılıkla erişimlerin hangi kısmının dış düzey önbellek kaybını önlediğine bağlı olacaktır. Hangi boyuttaki dizinin önbellekte güvenilir bir şekilde barındırılabileceğini bilmeyi gerektirecek optimizasyon. Önbelleğiniz her beş hücre için bir baytı barındıracak kadar büyükse, en basit yaklaşım bir baytın 0-2 aralığında beş taban üç kodlanmış değeri tutması olabilir (5 değerin 243 kombinasyonu vardır, bir bayta sığdır), bir taban-3 değeri "2" yi gösterdiğinde sorgulanacak 10.000.000 baytlık bir dizi ile birlikte.

Önbellek o kadar büyük değilse, ancak 8 hücre başına bir bayt barındırabiliyorsa, sekiz temel-3 değerinin 6.561 olası kombinasyonunun tümü arasından seçim yapmak için bir bayt değeri kullanmak mümkün olmayacaktır, ancak 0 veya 1'in 2'ye değiştirilmesi, aksi takdirde gereksiz bir aramaya neden olur, doğruluk 6.561'in tümünü desteklemeyi gerektirmez. Bunun yerine 256 en "kullanışlı" değere odaklanılabilir.

Özellikle 0, 1'den daha yaygınsa veya tam tersi, 5 veya daha az 1 içeren 0 ve 1 kombinasyonlarını kodlamak için 217 değer kullanmak, xxxx0000 ile xxxx1111 arasında kodlamak için 16 değer, 0000xxxx ile kodlamak için 16 değer kullanmak iyi bir yaklaşım olabilir. 1111xxxx ve biri xxxxxxxx için. Birinin bulabileceği başka herhangi bir kullanım için dört değer kalacaktır. Veriler açıklandığı gibi rastgele dağıtılırsa, tüm sorguların küçük bir çoğunluğu yalnızca sıfır ve bir içeren baytlara ulaşır (sekizli tüm grupların yaklaşık 2 / 3'ünde, tüm bitler sıfırlar ve birler ve yaklaşık 7 / 8'i) altı veya daha az 1 biti olacaktır); Dört x içeren bir bayta inmeyenlerin büyük çoğunluğu ve bir sıfıra veya bire inme şansı% 50 olacaktı. Bu nedenle, dört sorudan yalnızca biri geniş bir dizi araması gerektirecektir.

Veriler rastgele dağıtılırsa ancak önbellek sekiz öğe başına bir baytı işleyecek kadar büyük değilse, bu yaklaşımı her bir bayt sekizden fazla öğe işlerken kullanmaya çalışabilir, ancak 0 veya 1'e doğru güçlü bir önyargı yoksa , büyük dizide arama yapmak zorunda kalmadan işlenebilen değerlerin oranı, her bayt tarafından işlenen sayı arttıkça küçülecektir.


7

Ben ekleyeceğiniz @ o11c , çünkü ifadesi biraz kafa karıştırıcı olabilir. Son biti ve CPU döngüsünü sıkıştırmam gerekirse, aşağıdakileri yapardım.

% 5 "başka bir şey" durumunu tutan dengeli bir ikili arama ağacı oluşturarak başlayacağız . Her aramada, ağaçta hızlıca yürürsünüz: 10000000 öğeniz vardır: bunların% 5'i ağaçtadır: dolayısıyla ağaç veri yapısı 500000 öğe tutar. Bunu O (log (n)) zamanında yürümek size 19 iterasyon verir. Bu konuda uzman değilim, ancak sanırım orada bazı bellek verimli uygulamalar var. Tahmin edelim:

  • Dengeli ağaç, böylece alt ağaç konumu hesaplanabilir (indekslerin ağacın düğümlerinde saklanması gerekmez). Aynı şekilde bir yığın (veri yapısı) doğrusal bellekte depolanır.
  • 1 bayt değeri (2 ila 255)
  • Dizin için 3 bayt (10000000, 3 bayta uyan 23 bit alır)

Toplam, 4 bayt: 500000 * 4 = 1953 kB. Önbelleğe uyuyor!

Diğer tüm durumlar için (0 veya 1), bir bitvector kullanabilirsiniz. Rasgele erişim için% 5'lik diğer durumları dışarıda bırakamayacağınızı unutmayın: 1.19 MB.

Bu ikisinin kombinasyonu yaklaşık 3.099 MB kullanır. Bu tekniği kullanarak, 3.08 faktörlü bellek tasarrufu sağlayacaksınız.

Ancak bu, üzücü olan @ Matteo Italia'nın (2.76 MB kullanan) cevabını yenmiyor . Fazladan yapabileceğimiz bir şey var mı? En çok bellek tüketen kısım ağaçtaki 3 baytlık dizindir. Bunu 2'ye indirebilirsek, 488 kB tasarruf etmiş oluruz ve toplam bellek kullanımı 2.622 MB olur ki bu daha küçüktür!

Bunu nasıl yapabiliriz? İndekslemeyi 2 bayta düşürmeliyiz. Yine 10000000, 23 bit alır. 7 bit bırakabilmeliyiz. 10000000 eleman aralığını 78125 elemanlı 2 ^ 7 (= 128) bölgeye bölerek basitçe yapabiliriz. Şimdi bu bölgelerin her biri için ortalama 3906 element içeren dengeli bir ağaç oluşturabiliriz. Doğru ağacın seçilmesi, hedef dizinin basit bir şekilde 2 ^ 7'ye (veya bir bit kayması >> 7) bölünmesiyle yapılır . Artık depolanması gereken indeks kalan 16 bit ile temsil edilebilir. Saklanması gereken ağacın uzunluğu için bazı ek yükler olduğunu unutmayın, ancak bu önemsizdir. Ayrıca, bu bölme mekanizmasının ağaçta yürümek için gereken yineleme sayısını azalttığına dikkat edin, bu şimdi 7 yineleme daha az azaldı, çünkü 7 bit düşürdük: yalnızca 12 yineleme kaldı.

Sonraki 8 biti kesmek için işlemi teorik olarak tekrarlayabileceğinizi unutmayın, ancak bu, ortalama ~ 305 öğe içeren 2 ^ 15 dengeli ağaç oluşturmanızı gerektirir. Bu, başladığımız 19 yinelemeye kıyasla, ağaçta yürümek için yalnızca 4 yineleme ile 2,143 MB ile sonuçlanır.

Son bir sonuç olarak: Bu, 2-bit vektör stratejisini küçük bir bellek kullanımıyla yener, ancak uygulanması tam bir mücadeledir. Ancak, önbelleği yerleştirip yerleştirmeme arasındaki farkı yaratabilirse, denemeye değer olabilir.


1
Yiğit çaba!
davidbak

1
Şunu deneyin: Vakaların% 4'ü 2 değeri olduğundan ... bir dizi istisnai durum oluşturun (> 1). Gerçekten istisnai durumlar için (> 2) anlatıldığı gibi bir ağaç oluşturun. Küme ve ağaçta mevcutsa, ağaçtaki değeri kullanın; ağaçta değil sette mevcutsa, 2 değerini kullanın, aksi takdirde bitvector'unuzda (sette mevcut değil) araması yapın. Ağaç yalnızca 100000 öğe (bayt) içerecektir. Set, 500000 öğe içerir (ancak hiçbir değer içermez). Bu, artan maliyetini haklı çıkarırken boyutu küçültür mü? (Aramaların% 100'ü sette görünüyor; aramaların%
5'inin

Değişmez bir ağacınız olduğunda her zaman CFBS-sıralı bir dizi kullanmak istersiniz, bu nedenle düğümler için tahsis yoktur, sadece veriler.
o11c

5

Yalnızca okuma işlemlerini gerçekleştirirseniz, tek bir dizine değil, bir dizin aralığına değer atamak daha iyi olur.

Örneğin:

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

Bu bir yapı ile yapılabilir. Bir OO yaklaşımını seviyorsanız, buna benzer bir sınıf da tanımlamak isteyebilirsiniz.

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
}

Şimdi, bir aralıklar listesinde yineleme yapmanız ve dizininizin bunlardan birinde olup olmadığını kontrol etmeniz gerekiyor; bu, ortalama olarak çok daha az bellek yoğun olabilir ancak daha fazla CPU kaynağına mal olur.

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

Aralıkları azalan boyuta göre sıralarsanız, aradığınız öğenin erken bulunma olasılığını artırırsınız, bu da ortalama bellek ve CPU kaynak kullanımınızı daha da azaltır.

Ayrıca 1 boyutundaki tüm aralıkları kaldırabilirsiniz. Karşılık gelen değerleri bir haritaya koyun ve yalnızca aradığınız öğe aralıklarda bulunamıyorsa kontrol edin. Bu aynı zamanda ortalama performansı biraz yükseltmelidir.


4
İlginç bir fikir (+1), ancak çok fazla uzun 0'lar ve / veya 1'ler uzun süreler olmadıkça genel giderleri haklı çıkaracağından biraz şüpheliyim. Gerçekte, verilerin çalışma uzunluğu kodlamasını kullanmayı öneriyorsunuz. Bazı durumlarda iyi olabilir, ancak muhtemelen bu soruna genel bir yaklaşım değildir.
John Coleman

Sağ. Özellikle rasgele erişim için bu, basit bir diziden neredeyse kesinlikle daha yavaştır veya unt8_tçok daha az bellek alsa bile.
leftaroundabout

4

Uzun zaman önce, hatırlıyorum ...

Üniversitede, arabellek dizilerinden algoritma ile defalarca okumak zorunda olan bir ışın izleme programını hızlandırmak için bir görevimiz var. Bir arkadaşım bana her zaman 4Byte'ın katları olan RAM okumaları kullanmamı söyledi. Bu yüzden diziyi [x1, y1, z1, x2, y2, z2, ..., xn, yn, zn] modelinden [x1, y1, z1,0, x2, y2, z2 modeline değiştirdim 0, ..., xn, yn, zn, 0]. Her 3B koordinattan sonra boş bir alan eklediğim anlamına gelir. Bazı performans testlerinden sonra: Daha hızlıydı. Uzun lafın kısası: RAM'den dizinizden 4 Byte'ın çoğunu ve belki de doğru başlangıç ​​konumundan okuyun, böylece aranan dizinin içinde bulunduğu küçük bir küme ve cpu'daki bu küçük kümeden aranan dizini okuyabilirsiniz. (Sizin durumunuzda, doldurma alanları eklemenize gerek kalmayacak, ancak konsept açık olmalıdır)

Belki diğer katlar da yeni sistemlerde anahtar olabilir.

Bunun senin durumunda işe yarayıp yaramayacağını bilmiyorum, yani işe yaramazsa: Üzgünüm. İşe yararsa, bazı test sonuçlarını duymaktan mutluluk duyarım.

Not: Oh ve eğer herhangi bir erişim modeli veya yakınlarda erişilen endeksler varsa, önbelleğe alınmış kümeyi yeniden kullanabilirsiniz.

PPS: Çoklu faktör daha çok 16Byte gibi veya bunun gibi bir şey olabilir, tam olarak hatırlayabildiğim çok uzun zaman önce.


Muhtemelen genellikle 32 veya 64 bayt olan önbellekleri düşünüyorsunuz, ancak erişim rastgele olduğundan burada pek yardımcı olmayacak.
Surt

3

Buna bakarak verilerinizi bölebilirsiniz, örneğin:

  • endekslenen ve 0 değerini temsil eden bir bit kümesi (std :: vektör burada yararlı olacaktır)
  • endekslenen ve 1 değerini temsil eden bir bit kümesi
  • 2'nin değerleri için bu değere başvuran dizinleri içeren bir std :: vektör
  • diğer değerler için bir harita (veya std :: vector>)

Bu durumda, tüm değerler belirli bir dizine kadar görünür, böylece bit kümelerinden birini kaldırabilir ve diğerlerinde eksik olan değeri temsil edebilirsiniz.

Bu, en kötü durumu daha da kötüleştirse de, bu durum için size biraz bellek kazandıracaktır. Ayrıca aramaları yapmak için daha fazla CPU gücüne ihtiyacınız olacak.

Ölçtüğünüzden emin olun!


1
Birler / sıfırlar için bir bit kümesi. İkili dizinler kümesi. Ve geri kalanı için seyrek bir ilişkisel dizi.
Red.Wave

Kısa özet bu
JVApen

OP'nin terimleri bilmesine izin verin, böylece her birinin alternatif uygulamalarını arayabilir.
Red.Wave

2

Mats'ın yorum-yanıtında bahsettiği gibi, özellikle bilmeden aslında en iyi çözümün ne olduğunu söylemek zordur. ne tür verilere sahip (örneğin, uzun süreli 0'lar var mı, vb.) Ve erişim modelinizin neye benzediğini gibi ("rastgele", "her yerde" veya "kesinlikle tamamen doğrusal olmayan" veya "her değer tam olarak bir kez, yalnızca rastgele" veya ... anlamına gelir.

Bununla birlikte, akla gelen iki mekanizma vardır:

  • Bit dizileri; yani, sadece iki değere sahipseniz, dizinizi 8 çarpanıyla önemsiz bir şekilde sıkıştırabilirdiniz; 4 değeriniz varsa (veya "3 değer + diğer her şey") iki faktör ile sıkıştırabilirsiniz. Bu, zahmete değmeyebilir ve kıyaslamalara ihtiyaç duyabilir, özellikle önbelleklerinizden kaçan ve dolayısıyla erişim süresini hiç değiştirmeyen gerçekten rastgele erişim düzenleriniz varsa .
  • (index,value)veya (value,index)tablolar. Yani,% 1 durumu için çok küçük bir tablo, belki% 5 durum için bir tablo (tümünün aynı değere sahip olması nedeniyle dizinleri saklaması gerekir) ve son iki durum için büyük bir sıkıştırılmış bit dizisi var. Ve "tablo" ile nispeten hızlı aramaya izin veren bir şeyi kastediyorum; yani, neye sahip olduğunuza ve gerçek ihtiyaçlarınıza bağlı olarak, belki bir karma, bir ikili ağaç vb. Bu alt tablolar 1. / 2. seviye önbelleklerinize uyuyorsa, şanslı olabilirsiniz.

1

C'ye pek aşina değilim, ancak C ++ 'da 0 - 255 aralığındaki bir tamsayıyı temsil etmek için işaretsiz karakter kullanabilirsiniz .

4 bayt (32 bit) gerekli olan normal int ile karşılaştırıldığında (yine Java ve C ++ dünyasından geliyorum ), işaretsiz bir karakter 1 bayt (8 bit) gerektirir . bu nedenle dizinin toplam boyutunu% 75 oranında azaltabilir.


Bu muhtemelen kullanımda zaten böyledir uint8_t - 8, 8 bit demektir.
Peter Mortensen

-4

Dizinizin tüm dağıtım özelliklerini kısaca tanımladınız; diziyi fırlat .

Diziyi, diziyle aynı olasılıklı çıktıyı üreten rastgele bir yöntemle kolayca değiştirebilirsiniz.

Tutarlılık önemliyse (aynı rastgele dizin için aynı değeri üretir), tekrarlanan isabetleri izlemek için bir çiçeklenme filtresi ve / veya karma harita kullanmayı düşünün . Dizi erişiminiz gerçekten rasgele ise, bu tamamen gereksizdir.


18
Burada "rasgele erişim" in, erişimlerin gerçekte rasgele olduklarını değil, tahmin edilemez olduklarını belirtmek için kullanıldığından şüpheleniyorum. (yani "rasgele erişimli dosyalar" anlamında tasarlanmıştır)
Michael Kay

Evet, bu muhtemelen. Ancak OP net değil. OP'nin erişimleri herhangi bir şekilde rastgele değilse, diğer cevaplara göre bir tür seyrek dizi belirtilir.
Dúthomhas

1
Bence burada bir noktaya sahipsin, çünkü OP tüm dizi boyunca rastgele bir sırayla döngü yapacağını belirtti. Yalnızca dağılımların gözlemlenmesi gerektiği durumda, bu iyi bir cevaptır.
Ingo Schalk-Schupp
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.