Eine einfache Möglichkeit, die in den Sinn kommt, besteht darin, ein komprimiertes Array von 2 Bits pro Wert für die allgemeinen Fälle und ein getrenntes Array mit 4 Bytes pro Wert (24 Bit für den ursprünglichen Elementindex, 8 Bit für den tatsächlichen Wert usw. (idx << 8) | value)
) für das zu sortieren andere.
Wenn Sie einen Wert nachschlagen, führen Sie zuerst eine Suche im 2bpp-Array durch (O (1)). Wenn Sie 0, 1 oder 2 finden, ist dies der gewünschte Wert. Wenn Sie 3 finden, bedeutet dies, dass Sie es im sekundären Array nachschlagen müssen. Hier führen Sie eine binäre Suche durch, um nach dem Index Ihres Interesses zu suchen, der um 8 nach links verschoben ist (O (log (n) mit einem kleinen n, da dies 1% sein sollte), und extrahieren Sie den Wert aus dem 4- Byte Ding.
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);
}
}
Für ein Array wie das von Ihnen vorgeschlagene sollte dies 10000000/4 = 2500000 Bytes für das erste Array plus 10000000 * 1% * 4 B = 400000 Bytes für das zweite Array dauern. Daher werden 2900000 Bytes, dh weniger als ein Drittel des ursprünglichen Arrays, und der am häufigsten verwendete Teil im Speicher zusammengehalten, was für das Caching gut sein sollte (es kann sogar für L3 passen).
Wenn Sie mehr als 24-Bit-Adressierung benötigen, müssen Sie den "Sekundärspeicher" optimieren. Eine einfache Möglichkeit, es zu erweitern, besteht darin, ein Zeigerarray mit 256 Elementen zu haben, um die oberen 8 Bits des Index umzuschalten und wie oben beschrieben an ein indiziertes sortiertes 24-Bit-Array weiterzuleiten.
Schneller Benchmark
#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;
}
(Code und Daten werden in meinem Bitbucket immer aktualisiert)
Der obige Code füllt ein 10M-Element-Array mit zufälligen Daten, die als OP in ihrem Beitrag angegeben verteilt sind, initialisiert meine Datenstruktur und dann:
- führt eine zufällige Suche von 10 Millionen Elementen mit meiner Datenstruktur durch
- macht das gleiche durch das ursprüngliche Array.
(Beachten Sie, dass bei einer sequentiellen Suche das Array immer um ein Vielfaches gewinnt, da dies die cachefreundlichste Suche ist, die Sie durchführen können.)
Diese beiden letzten Blöcke werden 50 Mal wiederholt und zeitlich festgelegt. Am Ende werden der Mittelwert und die Standardabweichung für jede Art der Suche berechnet und zusammen mit der Beschleunigung (lookup_mean / array_mean) gedruckt.
Ich habe den obigen Code mit g ++ 5.4.0 ( -O3 -static
plus einige Warnungen) unter Ubuntu 16.04 kompiliert und auf einigen Computern ausgeführt. Die meisten von ihnen verwenden Ubuntu 16.04, einige ältere Linux, andere neuere Linux. Ich denke nicht, dass das Betriebssystem in diesem Fall überhaupt relevant sein sollte.
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
Die Ergebnisse sind ... gemischt!
- Im Allgemeinen gibt es auf den meisten dieser Maschinen eine Art Beschleunigung, oder zumindest sind sie gleichwertig.
- Die beiden Fälle, in denen das Array die Suche nach "intelligenten Strukturen" wirklich übertrifft, sind auf Computern mit viel Cache und nicht besonders ausgelastet: Der Xeon E5-1650 oben (15 MB Cache) ist eine Nacht-Build-Maschine, die derzeit recht untätig ist. Der Xeon E5-2697 (35 MB Cache) ist eine Maschine für Hochleistungsberechnungen, auch im Leerlauf. Es macht Sinn, dass das ursprüngliche Array vollständig in den riesigen Cache passt, sodass die kompakte Datenstruktur nur die Komplexität erhöht.
- Auf der anderen Seite des "Leistungsspektrums" - aber wo das Array wieder etwas schneller ist, gibt es den bescheidenen Celeron, der mein NAS antreibt; Es hat so wenig Cache, dass weder das Array noch die "intelligente Struktur" überhaupt hineinpassen. Andere Computer mit ausreichend kleinem Cache arbeiten ähnlich.
- Der Xeon X5650 muss mit einiger Vorsicht betrachtet werden - es handelt sich um virtuelle Maschinen auf einem ziemlich ausgelasteten Server mit zwei Sockets für virtuelle Maschinen. Es kann durchaus sein, dass es während des Tests, obwohl es nominell eine anständige Menge an Cache hat, mehrmals von völlig unabhängigen virtuellen Maschinen vorgezogen wird.