একটি সাধারণ সম্ভাবনা যা মনে আসে তা হ'ল সাধারণ ক্ষেত্রে 2 টি বিটের সংকীর্ণ অ্যারে রাখা হয় এবং মূল্য অনুসারে পৃথক 4 বাইট (মূল উপাদান সূচকের জন্য 24 বিট, প্রকৃত মানের জন্য 8 বিট, তাই (idx << 8) | value)
) সাজানো অ্যারে রাখা হয় অন্য একটা.
আপনি যখন কোনও মান সন্ধান করেন, আপনি প্রথমে 2bpp অ্যারে (ও (1)) এ অনুসন্ধান করেন; যদি আপনি 0, 1 বা 2 খুঁজে পান তবে এটি আপনার পছন্দ মত মান; আপনি যদি 3 টি খুঁজে পান তবে এর অর্থ এটি আপনাকে সেকেন্ডারি অ্যারেতে সন্ধান করতে হবে। এখানে আপনি 8 (O (লগ (এন) দ্বারা একটি ছোট এন দিয়ে, যেমন এটি 1% হওয়া উচিত) দ্বারা বাম-স্থানান্তরিত আপনার আগ্রহের সূচকটি সন্ধান করতে বাইনারি অনুসন্ধান করবেন এবং 4- এর থেকে মানটি বের করবেন বাইট জিনিস।
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);
}
}
আপনার প্রস্তাবিত একটি অ্যারের জন্য, এটি প্রথম অ্যারের জন্য 10000000/4 = 2500000 বাইট, এবং দ্বিতীয় অ্যারের জন্য 10000000 * 1% * 4 বি = 400000 বাইট নিতে হবে; সুতরাং 2900000 বাইট, অর্থাত্ মূল অ্যারের এক তৃতীয়াংশেরও কম, এবং সর্বাধিক ব্যবহৃত অংশটি মেমরিতে একসাথে রাখা হয়, যা ক্যাশে দেওয়ার জন্য ভাল হওয়া উচিত (এটি এমনকি এল 3 ফিটও হতে পারে)।
আপনার যদি 24-বিটের বেশি ঠিকানা প্রয়োজন হয় তবে আপনাকে "গৌণ স্টোরেজ" টিপতে হবে; এটি প্রসারিত করার তুচ্ছ উপায় হ'ল সূচকটির শীর্ষ 8 টি বিটকে স্যুইচ করার জন্য 256 এলিমেন্ট পয়েন্টার অ্যারে রাখা এবং উপরে 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;
}
(কোড এবং ডেটা সর্বদা আমার বিটবকেটে আপডেট হয়)
উপরের কোডটি তাদের পোস্টে উল্লিখিত ওপি হিসাবে বিতরণ করা এলোমেলো ডেটা সহ 10 এম এলিমেন্ট অ্যারেকে পপুলেট করে, আমার ডেটা কাঠামো সূচনা করে এবং তারপরে:
- আমার ডেটা স্ট্রাকচার সহ 10 এম উপাদানগুলির এলোমেলোভাবে অনুসন্ধান করে
- মূল অ্যারের মাধ্যমে একই কাজ করে।
(লক্ষ করুন যে ক্রমবর্ধমান সন্ধানের ক্ষেত্রে অ্যারে সর্বদা বিশাল পরিমাপের সাথে জয়ী হয়, কারণ এটি আপনি করতে পারেন এমন সবচেয়ে ক্যাশে-বান্ধব অনুসন্ধান)
এই শেষ দুটি ব্লক 50 বার পুনরাবৃত্তি এবং সময়সীমা; শেষে, স্পিডআপের সাথে (লুকিং_মিয়ান / অ্যারে_মিয়ান) পাশাপাশি প্রতিটি ধরণের অনুসন্ধানের গড় এবং মান বিচ্যুতি গণনা করা এবং মুদ্রণ করা হয়।
আমি উপরের কোডটি -O3 -static
উবুন্টু 16.04 এ জি ++ 5.4.0 ( আরও কিছু সতর্কতা) দিয়ে সংকলন করেছি এবং এটি কয়েকটি মেশিনে চালিয়েছি ; তাদের বেশিরভাগ উবুন্টু 16.04 চলছে, কিছু পুরানো লিনাক্স, কিছু নতুন লিনাক্স। আমি মনে করি না এই ক্ষেত্রে ওএসটি মোটেই প্রাসঙ্গিক হওয়া উচিত।
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
ফলাফল ... মিশ্র!
- সাধারণভাবে, এই মেশিনগুলির বেশিরভাগ ক্ষেত্রেই একরকম স্পিডআপ থাকে, বা কমপক্ষে সেগুলি পার হয়।
- অ্যারে সত্যই "স্মার্ট স্ট্রাকচার" অনুসন্ধানকে ট্রাম্প করে এমন দুটি ক্ষেত্রে প্রচুর ক্যাশেযুক্ত মেশিনগুলিতে রয়েছে এবং বিশেষত ব্যস্ত নয়: উপরে Xeon E5-1650 (15 এমবি ক্যাশে) একটি নাইট বিল্ড মেশিন, এই মুহূর্তে বেশ অলস; Xeon E5-2697 (35 মেগাবাইট ক্যাশে) একটি নিষ্ক্রিয় মুহুর্তে উচ্চ পারফরম্যান্স গণনার জন্য একটি মেশিন। এটি অর্থবোধ করে না, মূল অ্যারেটি তাদের বিশাল ক্যাশে পুরোপুরি ফিট করে, সুতরাং কমপ্যাক্ট ডেটা স্ট্রাকচারটি কেবল জটিলতা যুক্ত করে।
- "পারফরম্যান্স বর্ণালী" এর বিপরীত দিকে - তবে যেখানে অ্যারেটি আরও দ্রুততর হয় সেখানে নম্র সেলেনরন রয়েছে যা আমার এনএএসকে শক্তি দেয়; এর এত ছোট ক্যাশে রয়েছে যে অ্যারে বা "স্মার্ট স্ট্রাকচার" এটিকে মোটেই ফিট করে না। যথেষ্ট ছোট ক্যাশেযুক্ত অন্যান্য মেশিনগুলি একইভাবে সম্পাদন করে।
- Xeon X5650 অবশ্যই কিছু সতর্কতার সাথে নেওয়া উচিত - এগুলি বেশ ব্যস্ত ডুয়াল-সকেট ভার্চুয়াল মেশিন সার্ভারে ভার্চুয়াল মেশিন; এটি বেশ ভাল হতে পারে, যদিও নামমাত্র এটির একটি শালীন পরিমাণের পরিমাণ থাকে, পরীক্ষার সময় এটি বেশিরভাগ সময় সম্পূর্ণ সম্পর্কযুক্ত ভার্চুয়াল মেশিন দ্বারা পচে যায়।