95 %의 경우에 값이 0 또는 1 일 때 매우 큰 배열에 대한 임의 액세스 최적화? 한 빨라야하며, 이는 RAM 대역폭에서 매우

매우 큰 배열에서 임의 액세스에 대한 가능한 최적화가 있습니까? (현재는을 사용 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

결과는 … 혼합입니다!

  1. 일반적으로 이러한 머신의 대부분에는 속도가 다소 떨어지거나 최소한 동등합니다.
  2. 어레이가 실제로 “스마트 구조”조회를 능가하는 두 가지 경우는 캐시가 많고 특히 사용량이 많지 않은 컴퓨터에 있습니다. 위의 Xeon E5-1650 (15MB 캐시)은 야간 유휴 상태입니다. Xeon E5-2697 (35MB 캐시)은 유휴 상태에서도 고성능 계산을위한 기계입니다. 원래 어레이는 거대한 캐시에 완전히 들어 맞기 때문에 컴팩트 한 데이터 구조는 복잡성을 증가시킵니다.
  3. “성능 스펙트럼”의 반대편에 있지만 어레이가 약간 더 빠를 경우 NAS에 전원을 공급하는 겸손한 Celeron이 있습니다. 캐시가 너무 적기 때문에 배열이나 “스마트 구조”가 전혀 맞지 않습니다. 캐시가 작은 다른 컴퓨터도 비슷한 성능을 발휘합니다.
  4. 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가 그렇게 큰 문제는 아닙니다.

귀하의 질문에는 두 가지 가정이 있습니다.

  1. 모든 비트를 사용하지 않기 때문에 데이터가 제대로 저장되지 않습니다
  2. 더 잘 저장하면 일이 더 빨라집니다.

나는이 두 가지 가정이 모두 틀렸다고 생각합니다. 대부분의 경우 데이터를 저장하는 적절한 방법은 가장 자연스러운 표현을 저장하는 것입니다. 귀하의 경우, 이것은 당신이 간 것입니다 : 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 비트 벡터 전략을 능가하지만 구현하기가 완전히 어려워집니다. 그러나 캐시를 맞추는 것 사이에 차이를 만들 수 있다면 시도해 볼 가치가 있습니다.