Любая оптимизация для произвольного доступа в очень большом массиве, когда значение в 95% случаев равно 0 или 1? - PullRequest
0 голосов
/ 14 мая 2018

Есть ли какая-либо возможная оптимизация для произвольного доступа на очень большом массиве (в настоящее время я использую uint8_t, и я спрашиваю, что лучше)

uint8_t MyArray[10000000];

когда значение в любой позиции в массиве равно

  • 0 или 1 для 95% всех случаев,
  • 2 в 4% дел,
  • между 3 и 255 in другие 1% случаев?

Итак, есть ли что-нибудь лучше, чем массив uint8_t, который можно использовать для этого? Должен быть как можно более быстрый цикл по всему массиву в случайном порядке, и это очень сильно влияет на пропускную способность ОЗУ, поэтому при наличии нескольких потоков, делающих это одновременно для разных массивов, в настоящее время вся пропускная способность ОЗУ быстро насыщается.

Я спрашиваю, поскольку такой большой массив (10 МБ) кажется очень неэффективным, когда на самом деле известно, что почти все значения, кроме 5%, будут либо 0, либо 1. Так что, когда 95% всех значений в массиве на самом деле потребуется только 1 бит вместо 8 бит, это уменьшит использование памяти почти на порядок. Такое ощущение, что должно быть более эффективное решение для памяти, которое значительно уменьшило бы требуемую для этого пропускную способность ОЗУ и, как следствие, значительно ускорило бы произвольный доступ.

Ответы [ 13 ]

0 голосов
/ 14 мая 2018

Глядя на это, вы можете разделить ваши данные, например:

  • набор битов, который индексируется и представляет значение 0 (здесь будет полезно std :: vector)
  • битовый набор, который индексируется и представляет значение 1
  • a std :: vector для значений 2, содержащий индексы, которые ссылаются на это значение
  • карта для других значений (илиstd :: vector>)

В этом случае все значения отображаются до заданного индекса, так что вы можете даже удалить один из наборов битов и представить значение, отсутствующее в других.

Это сэкономит вам немного памяти для этого случая, хотя сделает худший случай хуже.Вам также потребуется больше ресурсов процессора для поиска.

Обязательно измерьте!

0 голосов
/ 14 мая 2018

Это скорее "длинный комментарий", чем конкретный ответ

Если ваши данные не являются чем-то общеизвестным, я сомневаюсь, что кто-либо может НАПРЯМУЮ ответить на ваш вопрос (и я не знаю овсе, что соответствует вашему описанию, но тогда я не знаю ВСЕ о всевозможных шаблонах данных для всех видов сценариев использования).Разреженные данные - распространенная проблема в высокопроизводительных вычислениях, но обычно это «у нас очень большой массив, но только некоторые значения не равны нулю».

Для не очень известных шаблонов, таких, как, я думаю, у вас,никто не будет ЗНАЕТ напрямую, что лучше, и это зависит от деталей: насколько случайным является произвольный доступ - это система, осуществляющая доступ к кластерам элементов данных, или она полностью случайна, как из универсального генератора случайных чисел.Являются ли данные таблицы полностью случайными или есть последовательности 0, а затем последовательности 1 с разбросом других значений?Кодирование длины прогона будет работать хорошо, если у вас достаточно длинные последовательности 0 и 1, но не будет работать, если у вас «шахматная доска 0/1».Кроме того, вам придется вести таблицу «начальных точек», чтобы вы могли довольно быстро добраться до нужного места.

Я давно знаю, что некоторые большие базы данных - это просто большиетаблица в ОЗУ (данные абонента телефонной станции в этом примере), и одна из проблем заключается в том, что оптимизация кэшей и таблиц страниц в процессоре довольно бесполезна.Вызывающая сторона настолько редко совпадает с той, которая недавно вызывала кого-то, поэтому предварительно загруженных данных нет, они просто случайные.Большие таблицы страниц - лучшая оптимизация для такого типа доступа.

Во многих случаях компромисс между «скоростью и небольшим размером» - это одна из тех вещей, которые вам нужно выбирать в разработке программного обеспечения [в другихинженерия это не обязательно так много компромисса].Таким образом, «тратить память на более простой код» часто является предпочтительным выбором.В этом смысле «простое» решение, скорее всего, лучше по скорости, но если у вас «лучше» использовать ОЗУ, то оптимизация по размеру таблицы даст вам достаточную производительность и хорошее улучшение по размеру.Есть много разных способов достижения этого - как предложено в комментарии, 2-битное поле, в котором хранятся два или три наиболее распространенных значения, а затем некоторый альтернативный формат данных для других значений - хеш-таблица будет моейПервый подход, но список или двоичное дерево может работать тоже - опять же, это зависит от того, где находятся ваши «не 0, 1 или 2».Опять же, это зависит от того, как значения «разбросаны» по таблице - они в кластерах или это скорее равномерно распределенный шаблон?

Но проблема в том, что вы все еще читаете данные изБАРАН.Затем вы тратите больше кода на обработку данных, включая некоторый код, чтобы справиться с «это не обычное значение».

Проблема с большинством распространенных алгоритмов сжатия заключается в том, что они основаны на распаковке последовательностей, поэтому выне может получить к ним произвольный доступ.Слишком маловероятно, что издержки разделения ваших больших данных на куски, скажем, 256 записей за раз, и распаковки 256 в массив uint8_t, выборки нужных вам данных и отбрасывания несжатых данных, вряд ли дадут вампроизводительность - если, конечно, это имеет какое-то значение.

В конце вам, вероятно, придется реализовать одну или несколько идей в комментариях / ответах, чтобы проверить, посмотрите, поможет ли это решить вашу проблему,или если шина памяти по-прежнему является основным ограничивающим фактором.

0 голосов
/ 14 мая 2018

Простая возможность, которая приходит на ум, - это сохранить сжатый массив из 2 бит на значение для общих случаев и отдельный 4 байта на значение (24 бита для индекса исходного элемента, 8 бит для фактического значения, поэтому (idx << 8) | value)) отсортированный массив для остальных.

Когда вы ищите значение, вы сначала делаете поиск в массиве 2bpp (O (1));если вы найдете 0, 1 или 2, это значение, которое вы хотите;если вы найдете 3, это означает, что вы должны искать его во вторичном массиве.Здесь вы выполните бинарный поиск, чтобы найти index вашего интереса, сдвинутый влево на 8 (O (log (n) с небольшим n, как это должно быть 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 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;

/// 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)

Код выше заполняет элемент 10MМассив ent со случайными данными, распределенными как OP, указанный в их посте, инициализирует мою структуру данных и затем:

  • выполняет случайный поиск 10M элементов с моей структурой данных
  • делает то же самое черезисходный массив.

(обратите внимание, что в случае последовательного поиска массив всегда выигрывает с огромной мерой, так как это самый дружественный к кешу поиск, который вы можете сделать)

Эти последниедва блока повторяются 50 раз и синхронизируются;в конце вычисляются и печатаются среднее и стандартное отклонение для каждого типа поиска, а также ускорение (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

Результаты ... смешанные!

  1. В целом, на большинстве этих машинесть некоторое ускорение, или, по крайней мере, они на уровне
  2. Два случая, когда массив действительно превосходит поиск «умной структуры», находятся на машинах с большим объемом кеша и не особенно загружены:Xeon E5-1650 и выше (кэш 15 МБ) - это машина для ночной сборки, на данный момент довольно простаивающая;Xeon E5-2697 (кэш-память 35 МБ) - это машина для высокопроизводительных вычислений, в том числе и в свободное время.Это имеет смысл, исходный массив полностью помещается в их огромный кэш, поэтому компактная структура данных только добавляет сложности.
  3. На противоположной стороне «спектра производительности» - но в этом случае массив немного быстрее,есть скромный Celeron, который питает мой NAS;у него настолько мало кеша, что ни массив, ни «умная структура» не помещаются в нем вообще.Другие машины с достаточно маленьким кешем работают аналогично.
  4. Xeon X5650 следует воспринимать с некоторой осторожностью - это виртуальные машины на довольно загруженном сервере с двумя сокетами;вполне возможно, что хотя номинально он имеет приличный объем кеша, во время теста он несколько раз вытесняется совершенно не связанными виртуальными машинами.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...