Một khả năng đơn giản xuất hiện trong đầu là giữ một mảng nén 2 bit cho mỗi giá trị cho các trường hợp phổ biến và 4 byte cho mỗi giá trị (24 bit cho chỉ số phần tử gốc, 8 bit cho giá trị thực, vì vậy (idx << 8) | value)
) đã sắp xếp mảng cho những người khác.
Khi bạn tìm kiếm một giá trị, trước tiên bạn thực hiện tra cứu trong mảng 2bpp (O (1)); nếu bạn tìm thấy 0, 1 hoặc 2 thì đó là giá trị bạn muốn; nếu bạn tìm thấy 3 thì có nghĩa là bạn phải tìm nó trong mảng thứ cấp. Tại đây, bạn sẽ thực hiện tìm kiếm nhị phân để tìm chỉ số sở thích của mình bị dịch chuyển trái 8 (O (log (n) với n nhỏ, vì đây phải là 1%) và trích xuất giá trị từ 4 - 4 byte điều.
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);
}
}
Đối với một mảng như mảng bạn đề xuất, điều này sẽ mất 10000000/4 = 2500000 byte cho mảng đầu tiên, cộng với 10000000 * 1% * 4 B = 400000 byte cho mảng thứ hai; do đó 2900000 byte, tức là ít hơn một phần ba của mảng ban đầu và phần được sử dụng nhiều nhất được giữ cùng nhau trong bộ nhớ, điều này rất tốt cho bộ nhớ đệm (thậm chí có thể phù hợp với L3).
Nếu bạn cần địa chỉ nhiều hơn 24 bit, bạn sẽ phải điều chỉnh "bộ nhớ thứ cấp"; Một cách đơn giản để mở rộng nó là có một mảng con trỏ 256 phần tử để chuyển qua 8 bit trên cùng của chỉ mục và chuyển tiếp đến một mảng được sắp xếp chỉ mục 24 bit như trên.
Điểm chuẩn nhanh
#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;
}
(mã và dữ liệu luôn được cập nhật trong Bitbucket của tôi)
Đoạn mã trên cư trú một mảng phần tử 10M với dữ liệu ngẫu nhiên được phân phối dưới dạng OP được chỉ định trong bài đăng của họ, khởi tạo cấu trúc dữ liệu của tôi và sau đó:
- thực hiện tra cứu ngẫu nhiên các yếu tố 10 triệu với cấu trúc dữ liệu của tôi
- thực hiện tương tự thông qua mảng ban đầu.
(lưu ý rằng trong trường hợp tra cứu tuần tự, mảng luôn thắng theo một số đo lớn, vì đó là tra cứu thân thiện với bộ nhớ cache nhất mà bạn có thể làm)
Hai khối cuối cùng được lặp lại 50 lần và tính thời gian; ở cuối, độ lệch trung bình và độ lệch chuẩn cho từng loại tra cứu được tính toán và in, cùng với việc tăng tốc (lookup_mean / Array_mean).
Tôi đã biên dịch mã ở trên với g ++ 5.4.0 ( -O3 -static
cộng với một số cảnh báo) trên Ubuntu 16.04 và chạy nó trên một số máy; hầu hết trong số họ đang chạy Ubuntu 16.04, một số Linux cũ hơn, một số Linux mới hơn. Tôi không nghĩ hệ điều hành nên có liên quan trong trường hợp này.
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
Kết quả là ... hỗn hợp!
- Nói chung, trên hầu hết các máy này đều có một số loại tăng tốc, hoặc ít nhất là chúng ngang tầm.
- Hai trường hợp mảng thực sự bỏ qua quá trình tra cứu "cấu trúc thông minh" nằm trên một máy có nhiều bộ đệm và không quá bận rộn: Xeon E5-1650 ở trên (bộ nhớ cache 15 MB) là máy dựng đêm, hiện tại khá nhàn rỗi; Xeon E5-2697 (bộ nhớ cache 35 MB) là một máy tính toán hiệu năng cao, trong một khoảnh khắc nhàn rỗi. Thật có ý nghĩa, mảng ban đầu hoàn toàn phù hợp với bộ nhớ cache khổng lồ của họ, vì vậy cấu trúc dữ liệu nhỏ gọn chỉ làm tăng thêm độ phức tạp.
- Ở phía đối diện của "phổ hiệu suất" - nhưng một lần nữa mảng nhanh hơn một chút, có Celeron khiêm tốn cung cấp năng lượng cho NAS của tôi; nó có rất ít bộ đệm mà cả mảng và "cấu trúc thông minh" đều không phù hợp với nó. Các máy khác có bộ nhớ cache đủ nhỏ thực hiện tương tự.
- Xeon X5650 phải được thận trọng - chúng là các máy ảo trên một máy chủ ảo ổ cắm kép khá bận rộn; cũng có thể là vậy, mặc dù trên danh nghĩa nó có một lượng bộ nhớ cache kha khá, trong suốt thời gian thử nghiệm, nó đã bị các máy ảo hoàn toàn không liên quan đánh lừa nhiều lần.