매우 큰 배열에서 임의 액세스에 대한 가능한 최적화가 있습니까? (현재는을 사용 uint8_t
하고 무엇이 더 좋은지 묻고 있습니다)
uint8_t MyArray[10000000];
배열의 임의 위치의 값이
- 모든 경우의 95 % 에 대해 0 또는 1
- 사례의 4 % 에서 2
- 다른 1 % 의 경우 3 에서 255 사이 ?
그렇다면 uint8_t
이것을 사용하기 위해 배열 보다 좋은 것이 있습니까? 전체 배열을 임의의 순서로 반복하는 것이 가능한 한 빨라야하며, 이는 RAM 대역폭에서 매우 무거 우므로 여러 배열에 대해 동시에 여러 개의 스레드가있는 경우 현재 전체 RAM 대역폭 빠르게 포화됩니다.
실제로 5 %를 제외한 거의 모든 값이 0 또는 1이라는 것을 알 때 큰 배열 (10 MB)을 갖는 것은 매우 비효율적이라고 생각하기 때문에 배열의 모든 값의 95 %가 실제로 8 비트 대신 1 비트 만 있으면 메모리 사용량이 거의 1 배 줄어 듭니다. 이에 필요한 RAM 대역폭을 크게 줄이는 더 효율적인 메모리 솔루션이 필요하므로 임의 액세스의 경우 훨씬 더 빠릅니다.
답변
기억할 수있는 간단한 가능성은 일반적인 경우 값당 2 비트의 압축 배열을 유지하고 값당 4 바이트 (원래 요소 인덱스의 경우 24 비트, 실제 값의 경우 8 비트 (idx << 8) | value)
)로 분리 된 배열을 유지하는 것입니다. 다른 것들.
값을 찾을 때 먼저 2bpp 배열에서 조회를 수행합니다 (O (1)). 0, 1 또는 2를 찾으면 원하는 값입니다. 3을 찾으면 2 차 배열에서 찾아야한다는 의미입니다. 여기에서 이진 검색을 수행하여 8만큼 왼쪽으로 이동 한 관심 지수 (이것은 1 % 여야하므로 작은 n을 갖는 O (log (n))를 찾고 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 B = 400000 바이트가 필요합니다. 따라서 2900000 바이트, 즉 원래 어레이의 3 분의 1 미만이며 가장 많이 사용 된 부분은 모두 메모리에 함께 보관되므로 캐싱에 적합해야합니다 (L3에 적합 할 수도 있음).
24 비트 이상의 주소 지정이 필요한 경우 “보조 저장소”를 조정해야합니다. 그것을 확장하는 간단한 방법은 256 요소 포인터 배열을 사용하여 인덱스의 상위 8 비트를 전환하고 위와 같이 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;
}
(코드와 데이터는 항상 내 Bitbucket에서 업데이트됩니다)
위의 코드는 게시물에 지정된 OP로 분산 된 임의의 데이터로 10M 요소 배열을 채우고 내 데이터 구조를 초기화 한 다음 :
- 내 데이터 구조로 10M 요소의 무작위 조회를 수행합니다.
- 원래 배열을 통해 동일하게 수행합니다.
(순차적 조회의 경우 배열은 항상 캐시에 친숙한 조회이므로 엄청나게 이깁니다.)
이 마지막 두 블록은 50 번 반복되고 시간이 정해집니다. 마지막으로, 각 유형의 조회에 대한 평균 및 표준 편차가 속도 향상 (lookup_mean / array_mean)과 함께 계산 및 인쇄됩니다.
-O3 -static
우분투 16.04에서 g ++ 5.4.0 ( 및 일부 경고)으로 위의 코드를 컴파일하고 일부 컴퓨터에서 실행했습니다. 그들 대부분은 Ubuntu 16.04, 일부 오래된 Linux, 일부 새로운 Linux를 실행하고 있습니다. 이 경우 OS가 전혀 관련이 없다고 생각합니다.
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 (15MB 캐시)은 야간 유휴 상태입니다. Xeon E5-2697 (35MB 캐시)은 유휴 상태에서도 고성능 계산을위한 기계입니다. 원래 어레이는 거대한 캐시에 완전히 들어 맞기 때문에 컴팩트 한 데이터 구조는 복잡성을 증가시킵니다.
- “성능 스펙트럼”의 반대편에 있지만 어레이가 약간 더 빠를 경우 NAS에 전원을 공급하는 겸손한 Celeron이 있습니다. 캐시가 너무 적기 때문에 배열이나 “스마트 구조”가 전혀 맞지 않습니다. 캐시가 작은 다른 컴퓨터도 비슷한 성능을 발휘합니다.
- Xeon X5650은주의해서 사용해야합니다. 매우 바쁜 듀얼 소켓 가상 머신 서버의 가상 머신입니다. 명목상으로는 상당한 양의 캐시가 있지만 테스트 시간 동안 완전히 관련되지 않은 가상 머신에 의해 여러 번 선점됩니다.
답변
다른 옵션은
- 결과가 0, 1 또는 2인지 확인
- 정기적 인 조회를하지 않으면
다른 말로하면 :
unsigned char lookup(int index) {
int code = (bmap[index>>2]>>(2*(index&3)))&3;
if (code != 3) return code;
return full_array[index];
}
여기서 bmap
“other”를 의미하는 값 3을 갖는 요소 당 2 비트를 사용합니다.
이 구조는 업데이트하기가 쉽지 않고 25 % 더 많은 메모리를 사용하지만 대부분의 경우 5 % 만 검색됩니다. 물론, 평소와 같이, 그것이 좋은 아이디어인지 아닌지는 다른 많은 조건에 의존하므로 유일한 대답은 실제 사용법을 실험하는 것입니다.
답변
이것은 구체적인 답변보다 “긴 의견”에 가깝습니다.
귀하의 데이터가 잘 알려진 것이 아닌 한, 누군가가 귀하의 질문에 직접 대답 할 수 있을지 의심 스럽습니다. 사용 사례의 종류). 희소 데이터는 고성능 컴퓨팅에서 일반적인 문제이지만 일반적으로 “매우 큰 배열을 갖지만 일부 값은 0이 아닙니다”입니다.
내가 생각하는 것과 같은 잘 알려지지 않은 패턴의 경우, 아무도 더 나은 것을 직접 알 수 없으며 세부 사항에 따라 다릅니다. 임의의 액세스가 임의의 수-시스템이 데이터 항목의 클러스터에 액세스합니까? 균일 한 난수 생성기. 테이블 데이터가 완전히 무작위입니까, 다른 값이 산란되어 0의 시퀀스와 1의 시퀀스가 있습니까? 실행 길이 인코딩은 0과 1의 합리적으로 긴 시퀀스가있는 경우에는 잘 작동하지만 “체커 보드 0/1″의 경우에는 작동하지 않습니다. 또한 “시작점”테이블을 유지해야하므로 관련 장소로 합리적으로 빠르게 이동할 수 있습니다.
오랜 시간 동안 일부 큰 데이터베이스는 RAM의 큰 테이블 (이 예에서는 전화 교환 가입자 데이터) 일 뿐이며 프로세서의 캐시 및 페이지 테이블 최적화는 쓸모가 없다는 문제 중 하나입니다. 발신자는 최근에 누군가를 호출 한 사람과 거의 같지 않으므로 사전로드 된 데이터가 없으며 순전히 무작위입니다. 큰 페이지 테이블은 해당 유형의 액세스에 가장 적합한 최적화입니다.
많은 경우에있어서, “속도와 작은 크기”사이의 타협은 소프트웨어 엔지니어링에서 선택해야하는 것 중 하나입니다 (다른 엔지니어링에서는 반드시 그렇게 타협 할 필요는 없습니다). 따라서 “더 간단한 코드를위한 메모리 낭비”가 선호되는 경우가 많습니다. 이런 의미에서 “간단한”솔루션은 속도면에서 훨씬 우수하지만, RAM에 “더 나은”사용을하는 경우 테이블 크기를 최적화하면 충분한 성능과 크기를 개선 할 수 있습니다. 주석에서 제안한 것처럼 2 ~ 3 개의 가장 일반적인 값이 저장되는 2 비트 필드와 다른 값에 대한 대체 데이터 형식 인 해시 테이블은 내 방법이 될 수 있습니다. 첫 번째 방법이지만 목록 또는 이진 트리도 다시 작동 할 수 있습니다. “0, 1 또는 2가 아닌”의 패턴에 따라 다릅니다. 다시, 그것은 테이블에서 값이 어떻게 흩어져 있는지에 달려 있습니다-그것들은 클러스터에 있습니까, 아니면 더 고르게 분포 된 패턴입니까?
그러나 문제는 여전히 RAM에서 데이터를 읽는다는 것입니다. 그런 다음 “이것은 일반적인 값이 아닙니다”에 대처하기 위해 일부 코드를 포함하여 데이터 처리에 더 많은 코드를 소비하고 있습니다.
가장 일반적인 압축 알고리즘의 문제점은 언 패킹 시퀀스를 기반으로하므로 임의로 액세스 할 수 없다는 것입니다. 그리고 빅 데이터를 한 번에 256 개의 항목으로 분할하고 256을 uint8_t 배열로 압축 해제하고, 원하는 데이터를 가져온 다음 압축되지 않은 데이터를 버리는 오버 헤드는 좋은 결과를 내지 못할 것입니다. 물론 성능이 중요하다고 가정합니다.
결국, 당신은 아마도 테스트하기 위해 의견 / 응답에 하나 또는 몇 가지 아이디어를 구현해야 할 것입니다, 문제 해결에 도움이되는지, 또는 메모리 버스가 여전히 주요 제한 요소인지 확인하십시오.
답변
내가 과거에 한 일은 비트 세트 앞에서 해시 맵을 사용하는 것 입니다.
이것은 Matteo의 답변에 비해 공간을 절반으로 줄이지 만 “예외”조회가 느리면 (즉, 많은 예외가있는 경우) 느려질 수 있습니다.
그러나 종종 “캐시는 왕이다”.
답변
데이터에 패턴이 없다면 합리적인 속도 나 크기 최적화가있을 가능성이 낮으며, 일반적인 컴퓨터를 대상으로 가정 할 경우 10MB가 그렇게 큰 문제는 아닙니다.
귀하의 질문에는 두 가지 가정이 있습니다.
- 모든 비트를 사용하지 않기 때문에 데이터가 제대로 저장되지 않습니다
- 더 잘 저장하면 일이 더 빨라집니다.
나는이 두 가지 가정이 모두 틀렸다고 생각합니다. 대부분의 경우 데이터를 저장하는 적절한 방법은 가장 자연스러운 표현을 저장하는 것입니다. 귀하의 경우, 이것은 당신이 간 것입니다 : 0에서 255 사이의 숫자 바이트. 다른 표현은 더 복잡하므로 다른 모든 것들은 동일합니다-느리고 오류가 발생하기 쉽습니다. 이 일반적인 원칙에서 벗어나려면 데이터의 95 %에서 잠재적으로 6 개의 “낭비”비트보다 더 강력한 이유가 필요합니다.
두 번째 가정의 경우, 어레이의 크기를 변경하여 캐시 누락이 실질적으로 줄어드는 경우에만 해당됩니다. 이것이 일어날 지의 여부는 작업 코드를 프로파일 링하여 결정될 수 있지만 실질적인 차이는 거의 없을 것입니다. 두 경우 모두 어레이에 무작위로 액세스하기 때문에 프로세서는 캐시 할 데이터 비트를 알고 두 경우 모두 유지하려고 애를 씁니다.
답변
데이터와 액세스가 균일하게 무작위로 분배되는 경우 성능은 아마도 외부 액세스 캐시 누락을 피하는 액세스 비율에 따라 달라질 수 있습니다. 이를 최적화하려면 캐시에 어떤 크기의 어레이를 안정적으로 수용 할 수 있는지 알아야합니다. 캐시가 5 개의 셀마다 1 바이트를 수용하기에 충분히 큰 경우, 가장 간단한 방법은 1 바이트가 0-2 범위의 5 개의 기본 3 인코딩 된 값을 보유하도록하는 것입니다 (5 개의 값의 243 조합이 있으므로 기본 -3 값이 “2”를 나타낼 때마다 쿼리되는 10,000,000 바이트 배열과 함께 바이트에 맞추기).
캐시가 크지 않지만 8 셀 당 1 바이트를 수용 할 수 있다면 1 바이트 값을 사용하여 8 개의 기본 3 값의 6,561 개의 가능한 조합 중에서 선택할 수는 없지만 유일한 효과는 0 또는 1을 2로 변경하면 불필요하게 조회가 발생하고 정확성을 위해 6,561을 모두 지원할 필요는 없습니다. 대신 256 개의 “유용한”값에 집중할 수 있습니다.
특히 0이 1보다 흔하거나 그 반대 일 경우, 217 개의 값을 사용하여 5 개 이하의 1을 포함하는 0과 1의 조합을 인코딩하고 16 개의 값을 xxxx0000에서 xxxx1111로, 16을 0000xxxx에서 1111xxxx, 하나는 xxxxxxxx입니다. 다른 용도로는 찾을 수있는 네 가지 값이 남아 있습니다. 설명 된대로 데이터가 무작위로 분포 된 경우 모든 쿼리의 거의 대부분이 0과 1을 포함하는 바이트에 도달합니다 (8 개 그룹의 모든 그룹의 약 2/3에서 모든 비트는 0과 1, 약 7/8). 그것들은 6 비트 이하의 1 비트를 가질 것이다); 4 개의 x를 포함하는 바이트에 착륙하지 않았고 50 %의 확률로 0 또는 1에 착륙 할 가능성이 큰 사람들. 따라서 4 개의 쿼리 중 약 1 개만 큰 배열 조회가 필요합니다.
데이터가 무작위로 분배되었지만 캐시가 8 개 요소 당 1 바이트를 처리하기에 충분히 크지 않은 경우 8 개 이상의 항목을 처리하는 각 바이트에이 방법을 사용할 수 있지만 0 또는 1에 대한 강한 편향이없는 한 큰 배열에서 조회를 수행하지 않고 처리 할 수있는 값의 비율은 각 바이트에서 처리하는 수가 증가함에 따라 줄어 듭니다.
답변
그의 말이 약간 혼란 스러울 수 있기 때문에 @ o11c 의 답변에 추가 할 것 입니다. 마지막 비트와 CPU 사이클을 짜야하는 경우 다음을 수행하십시오.
우리는 5 % “다른 것”사례를 보유한 균형 잡힌 이진 검색 트리를 구성하는 것으로 시작합니다 . 모든 조회에 대해 빠르게 트리를 탐색합니다. 10000000 개의 요소가 있습니다. 5 %는 트리에 있습니다. 따라서 트리 데이터 구조에는 500000 개의 요소가 있습니다. 이것을 O (log (n)) 시간 안에 걸 으면 19 회 반복됩니다. 나는 이것에 전문가가 아니지만 메모리 효율적인 구현이 있다고 생각합니다. 추측하자 :
- 균형 트리, 하위 트리 위치를 계산할 수 있습니다 (표시는 트리의 노드에 저장할 필요가 없습니다). 힙 (데이터 구조)이 선형 메모리에 저장되는 것과 같은 방식입니다.
- 1 바이트 값 (2 ~ 255)
- 인덱스의 3 바이트 (10000000은 23 비트, 3 바이트에 해당)
총계, 4 바이트 : 500000 * 4 = 1953kB 캐시에 맞습니다!
다른 모든 경우 (0 또는 1)의 경우 비트 벡터를 사용할 수 있습니다. 랜덤 액세스의 경우 5 %의 다른 경우는 1.19MB로 지정할 수 없습니다.
이 두 가지 조합은 약 3,099MB를 사용합니다. 이 기술을 사용하면 3.08의 메모리를 절약 할 수 있습니다.
그러나 이것은 불행한 @Matteo Italia (2.76 MB 사용) 의 대답을 능가하지 않습니다 . 추가로 할 수있는 일이 있습니까? 가장 많은 메모리를 소비하는 부분은 트리에서 3 바이트의 인덱스입니다. 이 값을 2로 줄이면 488kB를 절약하고 총 메모리 사용량은 2.622MB입니다.
우리는 이것을 어떻게합니까? 인덱싱을 2 바이트로 줄여야합니다. 다시 10000000은 23 비트를 사용합니다. 7 비트를 떨어 뜨릴 수 있어야합니다. 10000000 요소의 범위를 78125 요소의 2 ^ 7 (= 128) 영역으로 분할하여 간단히 수행 할 수 있습니다. 이제 각 지역마다 평균 3906 개의 요소가있는 균형 잡힌 트리를 만들 수 있습니다. 올바른 트리를 선택하는 것은 대상 인덱스를 2 ^ 7 (또는 bitshift >> 7
) 로 간단히 나눔으로써 수행 됩니다. 이제 저장에 필요한 인덱스는 나머지 16 비트로 표현할 수 있습니다. 저장해야하는 트리 길이에 약간의 오버 헤드가 있지만, 이는 무시할 수 있습니다. 또한이 분할 메커니즘은 트리를 걷는 데 필요한 반복 횟수를 줄입니다. 이제 7 비트가 떨어지기 때문에 이제 7 반복 횟수가 줄어 듭니다 .12 반복 만 남았습니다.
이론적으로 다음 8 비트를 차단하기 위해 프로세스를 반복 할 수 있지만 평균 ~ 305 개의 요소로 2 ^ 15 개의 균형 잡힌 트리를 만들어야합니다. 이로 인해 2.143MB가 발생하여 트리를 걸을 때 단 4 번의 반복 만 수행하며, 이는 처음 시작한 19 개의 반복에 비해 상당히 빠른 속도입니다.
마지막 결론으로, 이것은 약간의 메모리 사용으로 2 비트 벡터 전략을 능가하지만 구현하기가 완전히 어려워집니다. 그러나 캐시를 맞추는 것 사이에 차이를 만들 수 있다면 시도해 볼 가치가 있습니다.