Kemungkinan sederhana yang muncul di pikiran adalah untuk menjaga array terkompresi 2 bit per nilai untuk kasus-kasus umum, dan 4 byte terpisah per nilai (24 bit untuk indeks elemen asli, 8 bit untuk nilai aktual, jadi (idx << 8) | value)
) array yang diurutkan untuk yang lain.
Ketika Anda mencari nilai, pertama-tama Anda melakukan pencarian di array 2bpp (O (1)); jika Anda menemukan 0, 1 atau 2 itu nilai yang Anda inginkan; jika Anda menemukan 3 itu berarti Anda harus mencarinya di array sekunder. Di sini Anda akan melakukan pencarian biner untuk mencari indeks minat Anda bergeser ke kiri oleh 8 (O (log (n) dengan n kecil, karena ini harus menjadi 1%), dan ekstrak nilainya dari 4- byte byte.
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);
}
}
Untuk array seperti yang Anda usulkan, ini harus mengambil 10000000/4 = 2500000 byte untuk array pertama, ditambah 10000000 * 1% * 4 B = 400000 byte untuk array kedua; karenanya 2900000 byte, yaitu kurang dari sepertiga dari array asli, dan bagian yang paling sering digunakan disimpan dalam memori, yang seharusnya bagus untuk caching (bahkan mungkin cocok dengan L3).
Jika Anda membutuhkan pengalamatan lebih dari 24-bit, Anda harus mengubah "penyimpanan sekunder"; cara sepele untuk memperluasnya adalah memiliki array pointer elemen 256 untuk beralih di atas 8 bit indeks dan meneruskan ke array diurutkan diindeks 24-bit seperti di atas.
Tolok ukur cepat
#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;
}
(kode dan data selalu diperbarui di Bitbucket saya)
Kode di atas mengisi array elemen 10M dengan data acak yang didistribusikan sebagai OP yang ditentukan dalam pos mereka, menginisialisasi struktur data saya dan kemudian:
- melakukan pencarian acak elemen 10M dengan struktur data saya
- melakukan hal yang sama melalui array asli.
(perhatikan bahwa dalam kasus pencarian berurutan array selalu menang dengan ukuran besar, karena ini adalah pencarian yang paling ramah terhadap cache yang dapat Anda lakukan)
Dua blok terakhir ini diulang 50 kali dan waktunya; pada akhirnya, mean dan standar deviasi untuk setiap jenis pencarian dihitung dan dicetak, bersama dengan speedup (lookup_mean / array_mean).
Saya mengkompilasi kode di atas dengan g ++ 5.4.0 ( -O3 -static
, ditambah beberapa peringatan) di Ubuntu 16.04, dan menjalankannya di beberapa mesin; kebanyakan dari mereka menjalankan Ubuntu 16.04, beberapa Linux yang lebih tua, beberapa Linux yang lebih baru. Saya tidak berpikir OS harus relevan sama sekali dalam hal ini.
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
Hasilnya ... campuran!
- Secara umum, pada sebagian besar mesin ini ada semacam speedup, atau setidaknya mereka setara.
- Dua kasus di mana array benar-benar mengalahkan "struktur cerdas" lookup berada pada mesin dengan banyak cache dan tidak terlalu sibuk: Xeon E5-1650 di atas (15 MB cache) adalah mesin build malam, saat ini cukup menganggur; Xeon E5-2697 (35 MB cache) adalah mesin untuk kalkulasi kinerja tinggi, juga pada saat idle. Masuk akal, array asli cocok sepenuhnya dalam cache besar mereka, sehingga struktur data yang ringkas hanya menambah kompleksitas.
- Di sisi berlawanan dari "spektrum kinerja" - tetapi di mana lagi array sedikit lebih cepat, ada Celeron sederhana yang memberi kekuatan pada NAS saya; ia memiliki sangat sedikit cache sehingga array maupun "struktur pintar" tidak cocok sama sekali. Mesin lain dengan cache cukup kecil melakukan hal yang sama.
- Xeon X5650 harus diambil dengan hati-hati - mereka adalah mesin virtual pada server mesin virtual dual-socket yang cukup sibuk; mungkin saja itu, meskipun secara nominal ia memiliki jumlah cache yang layak, selama waktu pengujian itu akan didahului oleh mesin virtual yang sama sekali tidak terkait beberapa kali.