Re[10]: Fast heap implementation
От: Lazin Россия http://evgeny-lazin.blogspot.com
Дата: 14.07.14 12:13
Оценка:
К>>>Пробовал ли вместо очереди сделать каскад из M-way merge, где M — маленькая константа?
L>>Меня пока ломает экспериментально устанавливать размер N, тем более, что на разных машинах это значение будет отличаться. В очереди конечно же лежит кэшированное значение и индекс источника.

К>Под каскадом я имел в виду не жадное, а ленивое слияние.

К>Это та же куча, но без движения от корня. (Точнее, от корня движутся только "дырки").
К>
К>0 -\___ n+1 _
К>1 -/         \___ n+n/2+1 ___
К>2 -\___ n+2 _/               \
К>3 -/
К>...                           ..... ___ 2n-1
К>    ___ n+n/2
К>n -/
К>


Ну, то есть каскад M-way merge-ей, но только pull интерфейс, вместо push?
Re[11]: Fast heap implementation
От: Evgeny.Panasyuk Россия  
Дата: 14.07.14 12:40
Оценка:
Здравствуйте, Lazin, Вы писали:

L>
L>    while(!heap.empty()) {
L>        auto item = heap.top();
L>        auto point = get<0>(item);
L>        int index = get<1>(item);
L>        out_iter->put(caller, point.value, page);
L>        heap.pop();
L>        if (iter[index] != ends[index]) {
L>            auto point = *iter[index];
L>            iter[index]++;
L>            heap.push(make_tuple(point, index));
L>        }
L>    }
L>


1. Для чего эта пляска с индексом и value? Для стабильности?
Или это попытка сэкономить несколько байт на x64? Если да, то тогда непонятно зачем добавлять value в элемент — достаточно просто индекса, так ещё компактнее.
Если нет, то проще же и robust'ней в очередь класть range'и: heap.push(make_iterator_range(begin(x), end(x))), например:
while(!q.empty())
{
    auto range = heap.top();  
    q.pop();

    //assert(!empty(range));
    *out++ = *begin(range);
    range = make_iterator_range(next(begin(range)), end(range));
    if(!empty(range))
        q.push(range);
}


2. Тут на каждой итерации добавляется один элемент, а можно добавлять целую серию, например:
while(!q.empty())
{
    auto range = heap.top();  
    q.pop();
    if(!q.empty())
    {
        auto ceiling = *begin(heap.top());
        auto last = upper_bound(range, ceiling); // can be linear or binary search
        copy(begin(range), last, out)

        range = make_iterator_range(last, end(range));
        if(!empty(range))
            q.push(range);
    }
    else
    {
        copy(range, out);
    }
}
(если нужна будет стабильность, то нужен lower_bound + какой-либо индекс, может подойдёт stabile policy из Boost.Heap)
Re[11]: Fast heap implementation
От: cures Россия cures.narod.ru
Дата: 14.07.14 23:26
Оценка:
Здравствуйте, Lazin, Вы писали:

Пошла весёлая пьянка

L> Я не вижу ничего криминального в таком вот использовании очереди.


Криминальное: N * log(K) сравнений — это в идеале (в худшем его случае), этот идеал достигается, например, при сбалансированном бинарном слиянии, там log(K) проходов, на каждом — одно сравнение на один элемент результата. В случае правильной кучи (не попа и пуша, а опускания) будет 2 * N * log(K) сравнений (каждый элемент может тонуть сразу вниз, в круговом сценарии, который я описал выше, каждое опускание на уровень требует 2 сравнения элементов — правого и левого сына, потом папы с победителем). При таком использовании кучи, как у Вас, это 3 * N * log(K) (вернувшийся элемент нужно поднять, это ещё N * log(K) сравнений).

C>> Ну логично, вместо одного-двух сравнений 3*log(N), теряем порядок.

L> Всего сравнений — O(N log K) (K — количество массивов, N — суммарное количество элементов). О каком порядке ты говоришь?

Да, с порядком погорячился, всего тройка. Будем считать это тернарным порядком

L> Т.е. всего мы перепишем (log K * объем данных) памяти, это примерно 10Гб, против одного.


Да, прочитать и записать этот гиг потребуется 10 раз, но они будут вычитываться и записываться 10 раз практически последовательно. Когда они в очереди, Вам придётся их много раз прочитать и сравнить в очереди, хоть это и L1, но время на вычисления с ними никуда не денешь, оно не параллелится. Плюс (для нас — минус их придётся по разу вычитать из глобальной памяти, хоть это и не по десять раз, но браться они будут в произвольном порядке, что может быть дороже, так как вместо пропускной способности мы сталкиваемся со временем произвольного доступа к плохо кешируемой памяти: 1000 участков в памяти, L1 это точно не потянет, L2 — тоже вряд ли, они все в разных страницах, тут даже не кэш данных нас будет бить, а tlb, оно всего на 64 емнип страниц.
Но в целом Вы оказываетесь почти правы: тыщу раз по метру я не пробовал, только тыщу раз по сто тыщ рандомных даблов, это примерно 0.8ГБ. На стареньком ксеноне 2.4ГГц простейшая специализированная куча справляется примерно за 8.5 секунд , простейшее двухвейное слияние — примерно за 5.5 секунды, разница всего в полтора раза. Но это в одну нить, а двухвейка хорошо параллелится, но я ещё под опенмп не подогнал. Плюс есть ещё мысль по оптимизации кучи, для избегания сравнения целочисленных вспомогательных величин (не элементов): добить очередь снизу бесконечностями. Пока это выглядит примерно так:
struct RangeGroup {
    size_t size;
    vector<vector<double> > rs;
    vector<size_t> ps;

    void reset() {
        for (size_t i = 0; i < ps.size(); i++)
            ps[i] = 0;
    }

    double get_next(size_t i) {
        assert (i < rs.size());
        double res = INF;
        if (ps[i] < rs[i].size()) {
            res = rs[i][ps[i]];
            ps[i]++;
        }
        return res;
    }

    void init_random(size_t n, size_t m) {
        size = n * m;
        rs.resize(n);
        ps.resize(n);
        for (unsigned int i = 0; i < n; i++) {
            rs[i].resize(m);
            for (unsigned j = 0; j < m; j++)
                rs[i][j] = drand48();
            std::stable_sort(&rs[i][0], &rs[i][m]);
        }
    }

    void init_good(size_t n, size_t m) {
        size = n * m;
        double rsize = 1.0 / size;
        rs.resize(n);
        ps.resize(n);
        for (unsigned int i = 0; i < n; i++) {
            rs[i].resize(m);
            for (unsigned j = 0; j < m; j++)
                rs[i][j] = (i * m + j) * rsize;
        }
    }

    void init_bad(size_t n, size_t m) {
        size = n * m;
        double rsize = 1.0 / size;
        rs.resize(n);
        ps.resize(n);
        for (unsigned int i = 0; i < n; i++) {
            rs[i].resize(m);
            for (unsigned j = 0; j < m; j++)
                rs[i][j] = (i + j * n) * rsize;
        }
    }

};


class MergerHeap
{
    struct Node {
        double v;
        unsigned int idx, fill;
    };

    vector<Node> nodes;

    void drawn_node(unsigned int i, unsigned int n)
    {
        assert (i < n);
        double v = nodes[i].v;
        unsigned int idx = nodes[i].idx;

        for (;;) {
            unsigned int ic = 2 * i + 1;
            if (ic >= n)
                break;
            ic += (nodes[ic].v > nodes[ic + 1].v);
            if (v <= nodes[ic].v)
                break;
            nodes[i] = nodes[ic];
            i = ic;
        }

        nodes[i].v = v;
        nodes[i].idx = idx;
    }


    void heapify()
    {
        unsigned int n = nodes.size() - 1;
        for (unsigned int i = n; i > 0;) {
            drawn_node(--i, n);
        }
    }

public:
    void merge(RangeGroup *rg, double *dst)
    {
        unsigned int n = rg->rs.size();
        rg->reset();
        assert (n >= 2);
        nodes.resize(n + 1);
        nodes[n].v = INF;
        nodes[n].idx = ~0;
        for (unsigned int i = 0; i < n; i++) {
            nodes[i].idx = i;
            nodes[i].v = rg->get_next(i);
        }

        heapify();

        for (;;) {
            unsigned int ic = 1 + (nodes[1].v > nodes[2].v);
            double v = nodes[0].v;
            unsigned int idx = nodes[0].idx;
            double nv = nodes[ic].v;

            for (;;) {
                if (v == INF)
                    return;
                *dst++ = v;
                v = rg->get_next(idx);
                if (v > nv)
                    break;
            }

            unsigned int i = 0;
            nodes[0].v = nv;
            nodes[0].idx = nodes[ic].idx;
            i = ic;
            for (;;) {
                ic = 2 * i + 1;
                if (ic >= n)
                    break;
                ic += (nodes[ic].v > nodes[ic + 1].v);
                if (v <= nodes[ic].v)
                    break;
                nodes[i].v = nodes[ic].v;
                nodes[i].idx = nodes[ic].idx;
                i = ic;
            }

            nodes[i].v = v;
            nodes[i].idx = idx;
        }
    }
};


heapify и drawn_element на производительность не влияют.
На хороших элементах куча отрабатывает за 0.26 секунды, есть куда расти
В двухвейном слиянии тоже есть куда расти: если мерж заменить на два тупых мемкопи, то имеем около 2.5 секунд, значит упираемся не в пропускную способность памяти, и ещё минимум двойку на нитях можно отскрести. Hugetlb, соответственно, поможет реже щёлкать страницами.

L>Я думаю тут можно поэкспериментировать с серией слияний, но не бинарных, а ограниченных неким числом, как предлагали выше в топике. Нужно только правильно выбрать константу. Ну и это не отменяет того, что мне нужна хорошая очередь с приоритетами.


Дело хорошее, попробуйте эту очередь, снизу бесконечностями я позже добью, но интересно, какие будут результаты при Вашем нынешнем подходе и с ней. "Вообще-то говоря, голубей ругают зря", может и бинарные подойдут, если окажутся медленнее, кину свой код, хоть он и тривиален. Кроме массива для результата я выделяю ещё один такого же размера, и между ними перекидываю при мерже, стараясь угадать, чтобы после последнего прохода всё оказалось в результате. Насколько сложные у Вас элементы? Если сравнение очень дорогое, то бинарный мерж побъёт всех, у него оптимальная константа.

P.S. Извиняюсь за простыню, не нашёл, как сделать скрытый текст, в списке тагов внизу его нет. В "оформляем сообщение красиво" этого тоже нет. И гугель молчит.
Re[12]: Fast heap implementation
От: cures Россия cures.narod.ru
Дата: 15.07.14 00:48
Оценка:
Здравствуйте, Evgeny.Panasyuk, Вы писали:

EP>1. Для чего эта пляска с индексом и value? Для стабильности?


Чтобы по этому индексу (или итератору) не ломиться в память каждый раз, когда с этим элементом надо сравнить. Пока до этого элемента дойдёт дело, с ним уже не один десяток раз могут успеть сравниться, а лазить за ним каждый раз в глобальную память накладно. Про стабильность пока никто ничего не говорил.

EP>Или это попытка сэкономить несколько байт на x64? Если да, то тогда непонятно зачем добавлять value в элемент — достаточно просто индекса, так ещё компактнее.


Это попытка сэкономить несколько (тысяч) байт в кэше первого уровня, а он — не резиновый.

EP>2. Тут на каждой итерации добавляется один элемент, а можно добавлять целую серию, например:


А смысл? Если им суждено на этом шаге всем добавиться, то они добавятся и так, без особого оверхеда (в очереди будет затронут только первый элемент). И в итоге они добавляться будут всё равно по одному, байт за байтом. Хоть я и соптимизировал этот случай, он представляется весьма нетипичным. Но попробуйте, вдруг ускорится.

EP>(если нужна будет стабильность, то нужен lower_bound + какой-либо индекс, может подойдёт stabile policy из Boost.Heap)


Если нужна будет стабильность, то перейдут на бинарное слияние
Re[10]: Fast heap implementation
От: cures Россия cures.narod.ru
Дата: 15.07.14 01:09
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Под каскадом я имел в виду не жадное, а ленивое слияние.

К>Это та же куча, но без движения от корня. (Точнее, от корня движутся только "дырки").
К>0 -\___ n+1 _
К>1 -/         \___ n+n/2+1 ___
К>2 -\___ n+2 _/               \
К>3 -/
К>...                           ..... ___ 2n-1
К>    ___ n+n/2
К>n -/


"Нам бы схемку аль чертёж", то бишь программку (приведённая дальше ведь про другое?), желательно с таймингами, и можно без хлеба А так не очень понятно, что за ленивые слияния, и в чём от них ускорение.

К>Кроме того, напрашивается такой ход: избавиться от движения значений в куче.


Ну от движения значений мы избавимся (они всё равно лежат рядом с индексами, оверхэд не такой большой), зато приобретаем косвенность при сравнениях в попе и пуше, особенно в попе, там на каждый сдвиг на один уровень вниз нужно два сравнения, то есть два раза сходить по индексам. Одно чтения и одну запись поменяли на два чтения, в чём профит? Запись обычно быстрее, её не ждут. А пуши можно совсем выкинуть, не нужны они.
Re[11]: Fast heap implementation
От: Кодт Россия  
Дата: 15.07.14 09:40
Оценка:
Здравствуйте, cures, Вы писали:

C>"Нам бы схемку аль чертёж", то бишь программку (приведённая дальше ведь про другое?), желательно с таймингами, и можно без хлеба А так не очень понятно, что за ленивые слияния, и в чём от них ускорение.


Доберутся руки, сделаю сравнение нескольких вариантов.

К>>Кроме того, напрашивается такой ход: избавиться от движения значений в куче.


C>Ну от движения значений мы избавимся (они всё равно лежат рядом с индексами, оверхэд не такой большой),


Это смотря какой размер этих значений. Если там, например, строки, то оверхед будет будь здоров.

C>зато приобретаем косвенность при сравнениях в попе и пуше, особенно в попе, там на каждый сдвиг на один уровень вниз нужно два сравнения, то есть два раза сходить по индексам. Одно чтения и одну запись поменяли на два чтения, в чём профит? Запись обычно быстрее, её не ждут. А пуши можно совсем выкинуть, не нужны они.


В смысле пуши выкинуть?
Перекуём баги на фичи!
Re[13]: Fast heap implementation
От: Evgeny.Panasyuk Россия  
Дата: 15.07.14 12:08
Оценка:
Здравствуйте, cures, Вы писали:

EP>>1. Для чего эта пляска с индексом и value? Для стабильности?

C>Чтобы по этому индексу (или итератору) не ломиться в память каждый раз, когда с этим элементом надо сравнить. Пока до этого элемента дойдёт дело, с ним уже не один десяток раз могут успеть сравниться, а лазить за ним каждый раз в глобальную память накладно.

Для тысячи диапазонов и sizeof(T) <= 64, нужна тысяча кэш-линий, это всего-то 64KB — в L2 уж точно поместится.

EP>>Или это попытка сэкономить несколько байт на x64? Если да, то тогда непонятно зачем добавлять value в элемент — достаточно просто индекса, так ещё компактнее

C>Это попытка сэкономить несколько (тысяч) байт в кэше первого уровня, а он — не резиновый.

Согласен, валидный аргумент — так все действия для поддержания кучи будут в L1. Сразу не подумал видимо потому, что предполагал что количество диапазонов небольшое.

EP>>2. Тут на каждой итерации добавляется один элемент, а можно добавлять целую серию, например:

C>А смысл? Если им суждено на этом шаге всем добавиться, то они добавятся и так, без особого оверхеда (в очереди будет затронут только первый элемент). И в итоге они добавляться будут всё равно по одному, байт за байтом.

При использовании upper_bound (либо вариации учитывающей характер данных и вероятное положение искомой точки) в среднем уменьшится количество сравнений — многие элементы вообще не будут сравниваться, а как сказал Lazin, у него "больше всего времени тратится на сравнения элементов".

EP>>(если нужна будет стабильность, то нужен lower_bound + какой-либо индекс, может подойдёт stabile policy из Boost.Heap)

C>Если нужна будет стабильность, то перейдут на бинарное слияние

Не вижу в этом необходимости — достаточно к каждому элементу добавить глобальный индекс (учитывающий диапазон).
При бинарном же слиянии, как заметили выше, будут лишние проходы по памяти, это при том что в целом алгоритм memory bandwidth bounded.
Re[12]: Fast heap implementation
От: Lazin Россия http://evgeny-lazin.blogspot.com
Дата: 15.07.14 12:36
Оценка:
C>Но в целом Вы оказываетесь почти правы: тыщу раз по метру я не пробовал, только тыщу раз по сто тыщ рандомных даблов, это примерно 0.8ГБ. На стареньком ксеноне 2.4ГГц простейшая специализированная куча справляется примерно за 8.5 секунд , простейшее двухвейное слияние — примерно за 5.5 секунды, разница всего в полтора раза. Но это в одну нить, а двухвейка хорошо параллелится, но я ещё под опенмп не подогнал. Плюс есть ещё мысль по оптимизации кучи, для избегания сравнения целочисленных вспомогательных величин (не элементов): добить очередь снизу бесконечностями. Пока это выглядит примерно так:

Тут где-то есть подвох, у меня бинарное слияние работает медленнее, чем N-way слияние, вот код: https://gist.github.com/Lazin/d23d94ebc36f8a8542f2
Я там специально генерирую не случайные данные, а очень неудобный для слияния паттерн. Результаты такие:

binary merge 25.59
K-way merge 19.91

Re[13]: Fast heap implementation
От: cures Россия cures.narod.ru
Дата: 15.07.14 23:06
Оценка:
Здравствуйте, Lazin, Вы писали:

L>Тут где-то есть подвох, у меня бинарное слияние работает медленнее, чем N-way слияние, вот код: https://gist.github.com/Lazin/d23d94ebc36f8a8542f2

L> binary merge 25.59
L> K-way merge 19.91

Странно, у меня на сервере Ваша программа даёт binary 14, k-way 17.5.
На ноуте в виртуалке без свопа не работает (пиковое требование к памяти больше 4 гигов), со свопом мерять смысла нет.
Основной подвох, видимо, в том, что Вы не резервируете память для добавления бак-инсерторами, это самая верхняя функция merge для бинарного слияния и main для очереди. Отсутствие предварительного резервирования, при стандартном алгоритме удвоения, может приводить к увеличению времени в 2-3 раза для критичных по памяти приложений и к существенному увеличению пикового потребления памяти. После добавления резервирования в этих двух местах имеем на сервере 7 и 16.25 секунд соответственно. Искомой тройки ещё нет
Башевская команда time показывает, что больше половины времени программа проводит в системном пространстве, там ей нечего делать, кроме как щёлкать страницами. Поэтому пускаем программу через hugectl --heap, получаем 4.3 секунды против 15.1. Тут уже больше тройки, но это потому, что очередь неэффективна, и двухметровые страницы ей не помогают (один ранг занимает 1.2 метра, а их 1024, и к ним бегаем по кругу), а гиговые я там включить не могу, попробуйте у себя, наверное, вернёте тройку. И попробуйте сделать нормальную очередь, интересно, что получится.

L>Я там специально генерирую не случайные данные, а очень неудобный для слияния паттерн.


Это тот же паттерн, что и в моём init_bad. Кстати, в среднем рандомные данные оказываются хуже него
В генерации данных та же фигня с резервированием, но ещё и заполняете их Вы поперёк памяти, лучше генерить этот же паттерн так, как сделано у меня, вдоль. Ну и да, для слияния достаточно выделить один дополнительный временный массив такого же размера, как и выходной (и выходной, разумеется, тоже), так уменьшится служебное время, которое тратится на выделения-освобождения.
Re[12]: Fast heap implementation
От: cures Россия cures.narod.ru
Дата: 15.07.14 23:21
Оценка: 30 (1)
Здравствуйте, Кодт, Вы писали:

К>Это смотря какой размер этих значений. Если там, например, строки, то оверхед будет будь здоров.


Если длинные строки, то можно сушить вёсла, тогда проще хранить на них указатели, но при этом получим полный трэш по памяти. Тут всего лишь 12 байт, очень удобно добавить 4-байтный индекс.

К>В смысле пуши выкинуть?


Один раз кладём в массив по первому элементу из каждого диапазона, хипифицируем массив, потом в цикле верхний элемент не выкидываем с последующей вставкой нового, а, если в диапазоне ещё остались числа, кладём следующее на верх и топим его. Если лишнего не выкидывать, то не придётся и вставлять
Кстати, в моей проге я, если элементов в диапазоне не осталось, тупо кладу вместо него бесконечность и топлю, что, наверное, неправильно: эти утопленники могут забить очередь так, что она станет почти линейной, на некоторых паттернах это может больно ударить. Лучше, поклав бесконечность, менять её с последним элементом в очереди и топить уже его: расходы те же, а очередь чистенькая.
Re[14]: Fast heap implementation
От: cures Россия cures.narod.ru
Дата: 15.07.14 23:43
Оценка:
Здравствуйте, Evgeny.Panasyuk, Вы писали:

EP>При использовании upper_bound (либо вариации учитывающей характер данных и вероятное положение искомой точки) в среднем уменьшится количество сравнений — многие элементы вообще не будут сравниваться, а как сказал Lazin, у него "больше всего времени тратится на сравнения элементов".


Тут надо быть очень осторожным: при тупом бинарном поиске, если реально выводится только один элемент, Вы с ходу получаете на каждый элемент результата log(N/K) сравнений, что даже больше, чем log(K). Можно попробовать вариацию, сначала увеличивающую баунд вдвое, а уже потом ищущую место. Но, имхо, оно того не стоит.
"Программисты славятся своим умением ошибочно угадывать узкие места в программе!" Как показывает вскрытие, больше всего времени тратится на беготню по глобальной памяти.

C>>Если нужна будет стабильность, то перейдут на бинарное слияние

EP>Не вижу в этом необходимости — достаточно к каждому элементу добавить глобальный индекс (учитывающий диапазон).

Ага, и добавить ещё сравнение индексов и сравнение элементов на равенство! Кстати, не затем ли в программе топикстартера в сами элементы добавлен второй элемент пары — uint32_t, в котором сейчас нули? Пары же сравниваются лексикографически, если бы его выкинуть, слияние бы существенно ускорилось.

EP>При бинарном же слиянии, как заметили выше, будут лишние проходы по памяти, это при том, что в целом алгоритм memory bandwidth bounded.


Не бандвидтх, а кэш-миссы и тлб-трэш, от них как раз слияние и спасает.
Re[15]: Fast heap implementation
От: Evgeny.Panasyuk Россия  
Дата: 16.07.14 00:51
Оценка:
Здравствуйте, cures, Вы писали:

EP>>При использовании upper_bound (либо вариации учитывающей характер данных и вероятное положение искомой точки) в среднем уменьшится количество сравнений — многие элементы вообще не будут сравниваться, а как сказал Lazin, у него "больше всего времени тратится на сравнения элементов".

C>Тут надо быть очень осторожным: при тупом бинарном поиске, если реально выводится только один элемент, Вы с ходу получаете на каждый элемент результата log(N/K) сравнений, что даже больше, чем log(K). Можно попробовать вариацию, сначала увеличивающую баунд вдвое, а уже потом ищущую место. Но, имхо, оно того не стоит.

Так я же и говорю, что возможны "вариация учитывающей характер данных и вероятное положение искомой точки" — например сначала сравнить каждый десятый элемент, или действительно сразу увеличивать шаг и т.п.
Предполагаю, что в среднем, за раз, из диапазона будет браться как минимум 4-20 элементов — и на этом можно неплохо сэкономить. Но, опять таки, нужно смотреть что за данные.

C>"Программисты славятся своим умением ошибочно угадывать узкие места в программе!" Как показывает вскрытие, больше всего времени тратится на беготню по глобальной памяти.


1. То что глобальная память тут решающий фактор — понятно сразу, поэтому нужно минимизировать проходы по ней.
2. Lazin сказал, что в его случае "больше всего времени тратится на сравнения элементов". И если это действительно так, то имеет смысл срезать количество сравнений.

C>>>Если нужна будет стабильность, то перейдут на бинарное слияние

EP>>Не вижу в этом необходимости — достаточно к каждому элементу добавить глобальный индекс (учитывающий диапазон).
C>Ага, и добавить ещё сравнение индексов и сравнение элементов на равенство! Кстати, не затем ли в программе топикстартера в сами элементы добавлен второй элемент пары — uint32_t, в котором сейчас нули?
C>Пары же сравниваются лексикографически, если бы его выкинуть, слияние бы существенно ускорилось.

Так я сразу спросил: "Для чего эта пляска с индексом и value? Для стабильности?".

EP>>При бинарном же слиянии, как заметили выше, будут лишние проходы по памяти, это при том, что в целом алгоритм memory bandwidth bounded.

C>Не бандвидтх, а кэш-миссы и тлб-трэш, от них как раз слияние и спасает.

В каком смысле cache-miss'ы? Кэш-линии каждого диапазона загружаются по разу, в редких случаях несколько раз (когда из какого-то диапазона долго не было минимальных значений, и его начальная кэш-линия вымылась из L2 и L3). А вот в случае 2-way merge, объём перекачанной памяти будет гарантированно больше.
На счёт TLB-Thrashing — не думаю что он тут играет решающую роль.
Re[16]: Fast heap implementation
От: cures Россия cures.narod.ru
Дата: 16.07.14 01:45
Оценка:
Здравствуйте, Evgeny.Panasyuk, Вы писали:

EP>Так я же и говорю, что возможны "вариация учитывающей характер данных и вероятное положение искомой точки" — например сначала сравнить каждый десятый элемент, или действительно сразу увеличивать шаг и т.п.

EP>Предполагаю, что в среднем, за раз, из диапазона будет браться как минимум 4-20 элементов — и на этом можно неплохо сэкономить. Но, опять таки, нужно смотреть что за данные.

Так данные (тестовые) он привёл, из каждого диапазона каждый раз берётся ровно по одному значению, и диапазоны при этом перебираются циклически.

EP>1. То что глобальная память тут решающий фактор — понятно сразу, поэтому нужно минимизировать проходы по ней.


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

EP>2. Lazin сказал, что в его случае "больше всего времени тратится на сравнения элементов". И если это действительно так, то имеет смысл срезать количество сравнений.


В его случае (с очередью) — да, на сравнение, ибо сравнений в очереди в 3 раза больше, чем в бинарном слиянии, плюс ещё два служебных целочисленных сравнения для очереди общего назначения, а полных проходов по памяти в 10 раз меньше. И в этом случае, действительно, тлб-трэшинг почти не влияет на производительность (меньше секунды из 16 в режиме системы), так как на один промах у нас приходится очень много своих вычислений.

EP>Так я сразу спросил: "Для чего эта пляска с индексом и value? Для стабильности?".


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

EP>>>При бинарном же слиянии, как заметили выше, будут лишние проходы по памяти, это при том, что в целом алгоритм memory bandwidth bounded.

C>>Не бандвидтх, а кэш-миссы и тлб-трэш, от них как раз слияние и спасает.

EP>В каком смысле cache-miss'ы? Кэш-линии каждого диапазона загружаются по разу, в редких случаях несколько раз (когда из какого-то диапазона долго не было минимальных значений, и его начальная кэш-линия вымылась из L2 и L3). А вот в случае 2-way merge, объём перекачанной памяти будет гарантированно больше.


А как ведут себя кэш-линии для страниц, выпавших из тлб? Ещё они могут вымываться из-за перекрытия по кэш-линиям. А вот L3 в пользовательских компьютерах обычно не бывает

Объём, конечно, больше (в 10 раз), но они при этом считываются подряд сразу по нескольку, за это время мы как раз успеем обсчитать очередную порцию. Но это на больших страницах, на маленьких, увы, обработка промаха по тлб очень дорогая, что и показало тестирование: при бинарном слиянии больше половины времени программа проводила в режиме системы, то есть переключала страницы. Но даже на маленьких страницах бинарное слияние оказалось в 2 раза быстрее (7 секунд против 16). А на больших — почти в 3.5 раза (4.3 секунды против 15).

EP>На счёт TLB-Thrashing — не думаю что он тут играет решающую роль.


В случае очереди — не играет. Но она и без того сильно проигрывает по времени. Если её сделать аккуратно, то она уложится секунд в десять против семи, что очень неплохо, если кастомера не удаётся уговорить докупить памяти и перейти на Linux. Или hugetlb есть и в форточках?
Re[17]: Fast heap implementation
От: Evgeny.Panasyuk Россия  
Дата: 16.07.14 02:15
Оценка:
Здравствуйте, cures, Вы писали:

C>А вот L3 в пользовательских компьютерах обычно не бывает


Это как не бывает? У меня появился аж четыре года назад, в Nehalem.
Re[14]: Fast heap implementation
От: Lazin Россия http://evgeny-lazin.blogspot.com
Дата: 16.07.14 12:01
Оценка:
Здравствуйте, cures, Вы писали:

C>Здравствуйте, Lazin, Вы писали:


L>>Тут где-то есть подвох, у меня бинарное слияние работает медленнее, чем N-way слияние, вот код: https://gist.github.com/Lazin/d23d94ebc36f8a8542f2

L>> binary merge 25.59
L>> K-way merge 19.91

C>Странно, у меня на сервере Ваша программа даёт binary 14, k-way 17.5.

C>На ноуте в виртуалке без свопа не работает (пиковое требование к памяти больше 4 гигов), со свопом мерять смысла нет.
C>Основной подвох, видимо, в том, что Вы не резервируете память для добавления бак-инсерторами, это самая верхняя функция merge для бинарного слияния и main для очереди. Отсутствие предварительного резервирования, при стандартном алгоритме удвоения, может приводить к увеличению времени в 2-3 раза для критичных по памяти приложений и к существенному увеличению пикового потребления памяти. После добавления резервирования в этих двух местах имеем на сервере 7 и 16.25 секунд соответственно. Искомой тройки ещё нет

Начал резервировать память, бинарное слияние действительно стало сильно быстрее.

C>Башевская команда time показывает, что больше половины времени программа проводит в системном пространстве, там ей нечего делать, кроме как щёлкать страницами.


С закомментированым k-way мержем:

zsh 1056 % perf stat  -e dTLB-load-misses,iTLB-load-misses ./fill_test
binary merge 13.01

 Performance counter stats for './fill_test':

        41,931,880 dTLB-load-misses                                            
            48,332 iTLB-load-misses                                            

      17.169628482 seconds time elapsed



С закоментированным бинарным мержем:

zsh 1057 % perf stat  -e dTLB-load-misses,iTLB-load-misses ./fill_test
K-way merge 20.33

 Performance counter stats for './fill_test':

       290,434,271 dTLB-load-misses                                            
            90,818 iTLB-load-misses                                            

      24.871572710 seconds time elapsed


Таки действительно TLB-промахи.
Re[14]: Fast heap implementation
От: Lazin Россия http://evgeny-lazin.blogspot.com
Дата: 16.07.14 12:07
Оценка:
C>И попробуйте сделать нормальную очередь, интересно, что получится.

А что имеется ввиду под "нормальной очередью"?
Re[15]: Fast heap implementation
От: Lazin Россия http://evgeny-lazin.blogspot.com
Дата: 16.07.14 12:30
Оценка:
Что интересно, с кэш промахами все наоборот:

Случай с закомментированым k-way мержем (работает бинарный поиск):

zsh 1062 % perf stat  -e cache-misses ./fill_test
binary merge 12.77

 Performance counter stats for './fill_test':

        44,452,101 cache-misses                                                

      16.894329776 seconds time elapsed


Закомментированый бинарный мерж:

zsh 1061 % perf stat  -e cache-misses ./fill_test
K-way merge 20.2

 Performance counter stats for './fill_test':

        28,456,613 cache-misses                                                

      24.533016829 seconds time elapsed


Т.е. k-way мерж делает меньше кэш промахов, но тем не менее, справляется медленнее. Хотя возможно, я что-нибудь не так измеряю.
Re[15]: Fast heap implementation
От: cures Россия cures.narod.ru
Дата: 16.07.14 22:25
Оценка:
Здравствуйте, Lazin, Вы писали:

L>Начал резервировать память, бинарное слияние действительно стало сильно быстрее.


Начали, но не везде Не уверен насчёт очереди, но в генераторе данных похоже ничего не резервируете, и по-прежнему заполняете данные поперёк памяти, о чём говорят уходящие туда 4 с лишним секунды. Отсутствие резервирования не только ухудшает производительность этого куска (в которой Вы и не заинтересованы), увеличивает пиковое потребление памяти и затрудняет профилирование, но и приводит к фрагментации памяти, которая непредсказуемым образом влияет на производительность слияний, что при тестировании совсем некстати.
Попробуйте такое:
    for (unsigned int i = 0; i < NUM_WAYS; i++) {
        ds[i].resize(LEN_WAYS);
        for (unsigned int j = 0; j < LEN_WAYS; j++)
            ds[i][j] = std::make_tuple(j * NUM_WAYS + i, 0);
    }


L>Т.е. k-way мерж делает меньше кэш промахов, но тем не менее, справляется медленнее. Хотя возможно, я что-нибудь не так измеряю.


Не так уж и удивительно, на 16 миллионов меньше промахов по кэшу данных и на 250 миллионов больше промахов по тлб. Ну и не забываем про втрое большее число сравнений элементов у очереди, а одно сравнение Ваших TKey требует трёх элементарных машинных сравнений в 64-битном режиме.
Странно другое, как мы на 102 миллиона элементов ухитрились получить 290 миллионов промахов по тлб на чтение? 90 можно списать на генератор данных и на незарезервированный выходной массив, но остаётся по 2 промаха на элемент. Это я могу объяснить только тем, что в каждую выбитую страницу упираются сразу две инструкции (бакграундная и фореграундная), и генерятся сразу два евента.
perf — интересная утилита, не слышал про неё. Но основным инструментом профилирования остаются секундомер и здравый смысл
Почему не хотите попробовать большие страницы? Они вроде бы "из коробки".

L>А что имеется ввиду под "нормальной очередью"?


Специализиованная именно для мержа очередь, без добавлений, но с утоплением, требующая по 2 сравнения элементов, а не по 3. Например, та, которую я привёл в коде сообщения выше
Автор: cures
Дата: 15.07.14
. Хотя её и можно ещё доработать, но для предварительных замеров она подойдёт. Она, конечно, для даблов, но поиском-заменой переделывается на TKey, и даже на шаблон. Только бесконечностью у TKey будет пара {~0ULL, ~0U}, её можно вынести в инстанциацию шаблонной функции.
Совсем приличный результат можно получить, если сделать два каскада слияний по 32 потока, 32 как раз чуть меньше, чем типичное количество страниц в тлб, да и кэшу данных будет полегче.
Но бинарное слияние всё равно трудно побороть, оно меняет дорогие сравнения на дешёвый последовательный доступ к памяти. Но если очереди разной длины, и их не такое круглое число, то возможны разные исходы.
Re[16]: Fast heap implementation
От: Lazin Россия http://evgeny-lazin.blogspot.com
Дата: 17.07.14 12:34
Оценка:
Здравствуйте, cures, Вы писали:

C>Здравствуйте, Lazin, Вы писали:


L>>Начал резервировать память, бинарное слияние действительно стало сильно быстрее.


C>Начали, но не везде Не уверен насчёт очереди, но в генераторе данных похоже ничего не резервируете, и по-прежнему заполняете данные поперёк памяти, о чём говорят уходящие туда 4 с лишним секунды. Отсутствие резервирования не только ухудшает производительность этого куска (в которой Вы и не заинтересованы), увеличивает пиковое потребление памяти и затрудняет профилирование, но и приводит к фрагментации памяти, которая непредсказуемым образом влияет на производительность слияний, что при тестировании совсем некстати.

C>Попробуйте такое:
C>
C>    for (unsigned int i = 0; i < NUM_WAYS; i++) {
C>        ds[i].resize(LEN_WAYS);
C>        for (unsigned int j = 0; j < LEN_WAYS; j++)
C>            ds[i][j] = std::make_tuple(j * NUM_WAYS + i, 0);
C>    }
C>


Переделал инициализацию, получил такие вот результаты:

binary merge 12.53

 Performance counter stats for './fill_test':

        32,974,456 dTLB-load-misses                                            
        33,419,552 cache-misses                                                

      13.752300615 seconds time elapsed


K-way merge 20.07

 Performance counter stats for './fill_test':

       279,699,671 dTLB-load-misses                                            
        15,369,581 cache-misses                                                

      21.386986439 seconds time elapsed


C>Специализиованная именно для мержа очередь, без добавлений, но с утоплением, требующая по 2 сравнения элементов, а не по 3. Например, та, которую я привёл в коде сообщения выше
Автор: cures
Дата: 15.07.14
. Хотя её и можно ещё доработать, но для предварительных замеров она подойдёт. Она, конечно, для даблов, но поиском-заменой переделывается на TKey, и даже на шаблон. Только бесконечностью у TKey будет пара {~0ULL, ~0U}, её можно вынести в инстанциацию шаблонной функции.


И вот что в итоге там получилось:

MergerHeap 9.05

 Performance counter stats for './fill_test':

       159,302,104 dTLB-load-misses                                            
        20,355,224 cache-misses                                                

      11.211531765 seconds time elapsed


gist — https://gist.github.com/Lazin/d23d94ebc36f8a8542f2

сейчас попробую с большими страницами сравнить
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.