ความเป็นไปได้ง่ายๆที่ควรคำนึงถึงคือเก็บอาร์เรย์ที่บีบอัดไว้ 2 บิตต่อค่าสำหรับกรณีทั่วไปและ 4 ไบต์ที่แยกจากกันต่อค่า (24 บิตสำหรับดัชนีองค์ประกอบดั้งเดิม 8 บิตสำหรับค่าจริงดังนั้น(idx << 8) | value)
) เรียงลำดับอาร์เรย์สำหรับ คนอื่น ๆ
เมื่อคุณค้นหาค่าคุณต้องทำการค้นหาในอาร์เรย์ 2bpp (O (1)) ก่อน ถ้าคุณพบว่า 0, 1 หรือ 2 เป็นค่าที่คุณต้องการ ถ้าคุณพบ 3 หมายความว่าคุณต้องค้นหาในอาร์เรย์รอง ที่นี่คุณจะทำการค้นหาไบนารีเพื่อค้นหาดัชนีความสนใจของคุณเลื่อนไปทางซ้ายด้วย 8 (O (log (n) ด้วย n ขนาดเล็กเนื่องจากควรเป็น 1%) และดึงค่าจาก 4- ไบต์ thingie
std::vector<uint8_t> main_arr;
std::vector<uint32_t> sec_arr;
uint8_t lookup(unsigned idx) {
uint8_t v = (main_arr[idx>>2]>>(2*(idx&3)))&3;
if(v != 3) return v;
auto ptr = std::lower_bound(sec_arr.begin(), sec_arr.end(), idx<<8);
#ifdef _DEBUG
if(ptr == sec_arr.end()) std::abort();
if((*ptr >> 8) != idx) std::abort();
#endif
return (*ptr) & 0xff;
}
void populate(uint8_t *source, size_t size) {
main_arr.clear(); sec_arr.clear();
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(in >= 3) {
sec_arr.push_back((idx << 8) | in);
in = 3;
}
target |= in << ((idx & 3)*2);
}
}
สำหรับอาร์เรย์เช่นอาร์เรย์ที่คุณเสนอควรใช้เวลา 10,000000 / 4 = 2500000 ไบต์สำหรับอาร์เรย์แรกบวก 10,000000 * 1% * 4 B = 400000 ไบต์สำหรับอาร์เรย์ที่สอง ดังนั้น 2900000 ไบต์นั่นคือน้อยกว่าหนึ่งในสามของอาร์เรย์ดั้งเดิมและส่วนที่ใช้มากที่สุดจะถูกเก็บไว้ด้วยกันในหน่วยความจำซึ่งควรจะดีสำหรับการแคช (อาจพอดีกับ L3 ด้วยซ้ำ)
หากคุณต้องการแอดเดรสมากกว่า 24 บิตคุณจะต้องปรับแต่ง "ที่เก็บข้อมูลสำรอง" วิธีที่ไม่สำคัญในการขยายคือการมีอาร์เรย์พอยน์เตอร์ 256 องค์ประกอบเพื่อสลับบน 8 บิตบนสุดของดัชนีและส่งต่อไปยังอาร์เรย์ที่จัดเรียงดัชนี 24 บิตตามด้านบน
มาตรฐานด่วน
#include <algorithm>
#include <vector>
#include <stdint.h>
#include <chrono>
#include <stdio.h>
#include <math.h>
using namespace std::chrono;
struct XorShift32 {
typedef uint32_t result_type;
static uint32_t min() { return 1; }
static uint32_t max() { return uint32_t(-1); }
uint32_t y;
XorShift32(uint32_t seed = 0) : y(seed) {
if(y == 0) y = 2463534242UL;
}
uint32_t operator()() {
y ^= (y<<13);
y ^= (y>>17);
y ^= (y<<15);
return y;
}
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) {
uint8_t v = (main_arr[idx>>2]>>(2*(idx&3)))&3;
if(v != 3) return v;
auto ptr = std::lower_bound(sec_arr.begin(), sec_arr.end(), idx<<8);
#ifdef _DEBUG
if(ptr == sec_arr.end()) std::abort();
if((*ptr >> 8) != idx) std::abort();
#endif
return (*ptr) & 0xff;
}
void populate(uint8_t *source, size_t size) {
main_arr.clear(); sec_arr.clear();
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(in >= 3) {
sec_arr.push_back((idx << 8) | in);
in = 3;
}
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;
else if(v < 4080218931) v = 1;
else if(v < 4252017623) v = 2;
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;
}
(รหัสและข้อมูลอัปเดตอยู่เสมอใน Bitbucket ของฉัน)
โค้ดด้านบนจะเติมอาร์เรย์องค์ประกอบ 10M พร้อมข้อมูลแบบสุ่มที่กระจายตาม OP ที่ระบุในโพสต์เริ่มต้นโครงสร้างข้อมูลของฉันจากนั้น:
- ทำการค้นหาองค์ประกอบ 10M แบบสุ่มด้วยโครงสร้างข้อมูลของฉัน
- ทำเช่นเดียวกันกับอาร์เรย์เดิม
(โปรดสังเกตว่าในกรณีของการค้นหาตามลำดับอาร์เรย์จะชนะด้วยการวัดขนาดใหญ่เสมอเนื่องจากเป็นการค้นหาที่เป็นมิตรกับแคชมากที่สุดที่คุณสามารถทำได้)
สองบล็อกสุดท้ายนี้ซ้ำ 50 ครั้งและหมดเวลา ในตอนท้ายค่าเฉลี่ยและส่วนเบี่ยงเบนมาตรฐานสำหรับการค้นหาแต่ละประเภทจะถูกคำนวณและพิมพ์พร้อมกับ speedup (lookup_mean / array_mean)
ฉันรวบรวมโค้ดด้านบนด้วย g ++ 5.4.0 ( -O3 -static
รวมถึงคำเตือนบางอย่าง) บน Ubuntu 16.04 และรันบนบางเครื่อง ส่วนใหญ่ใช้ Ubuntu 16.04, Linux รุ่นเก่าบางรุ่น, Linux รุ่นใหม่บางรุ่น ฉันไม่คิดว่าระบบปฏิบัติการควรจะเกี่ยวข้องเลยในกรณีนี้
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 MB) เป็นเครื่องสร้างกลางคืนในขณะนี้ค่อนข้างไม่ได้ใช้งาน Xeon E5-2697 (แคช 35 MB) เป็นเครื่องสำหรับการคำนวณประสิทธิภาพสูงในช่วงเวลาที่ไม่ได้ใช้งานเช่นกัน มันสมเหตุสมผลอาร์เรย์ดั้งเดิมพอดีกับแคชขนาดใหญ่ดังนั้นโครงสร้างข้อมูลขนาดกะทัดรัดจึงเพิ่มความซับซ้อนเท่านั้น
- ที่ด้านตรงข้ามของ "สเปกตรัมประสิทธิภาพ" - แต่ที่อีกครั้งอาร์เรย์จะเร็วขึ้นเล็กน้อยมี Celeron ผู้ต่ำต้อยที่ขับเคลื่อน NAS ของฉัน มันมีแคชน้อยมากจนทั้งอาร์เรย์หรือ "โครงสร้างอัจฉริยะ" ไม่พอดีกับมันเลย เครื่องอื่น ๆ ที่มีแคชเล็กพอจะทำงานได้ในทำนองเดียวกัน
- Xeon X5650 ต้องใช้ความระมัดระวัง - เป็นเครื่องเสมือนบนเซิร์ฟเวอร์เครื่องเสมือนซ็อกเก็ตคู่ที่ค่อนข้างยุ่ง อาจเป็นไปได้ว่าแม้ว่าในนามจะมีแคชในปริมาณที่เหมาะสม แต่ในช่วงเวลาของการทดสอบจะถูกจับจองโดยเครื่องเสมือนที่ไม่เกี่ยวข้องโดยสิ้นเชิงหลายครั้ง