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 -static
Ubuntu 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!
- Genel olarak, bu makinelerin çoğunda bir tür hızlanma vardır veya en azından eşit düzeydedirler.
- 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.
- "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.
- 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.