LINUX.ORG.RU

Итерация по unordered_set быстрее чем по vector?

 ,


0

4

Ковыряю алгоритмы на Leetcode. Преобразую vector в unordered_set для кеширования и поиска элементов, ничего сложного. Далее собственно итератор по vector, и проверка есть ли каждый элемент в unordered_set и далее логика если есть.

Запуск кода вываливается на платформе с ошибкой мол алгоритм отработал слишком медленно.

Барабанная дробь - меняем итерацию по массиву на итерацию по unordered_set - алгоритм укладывается во временные рамки. Что за? Есть разумное объяснение? А то все говорят что итерации по массиву быстрее быть не может за счёт кеширования в CPU и предвыборки элементов массива загодя в кеш. Оказывается может.

int func(vector<int>& nums) {
    unordered_set<int> set;

    for (auto n : nums) {
        set.insert(n);
    }

    // Итерация по nums медленнее чем по 'set' !!!
    for (auto n : nums) {
    ...
Ответ на: комментарий от m0xf

Немного не точно объяснил, но суть с проверкой не меняется от этого.

Тормозит не итерация, а проверка каждого элемента в unordered_set.

Проверка осталась внутри for, она никуда делась. Проверяются абсолютно все элементы по алгоритму.

Для ускорения алгоритма меняем только одно слово. Вместо nums в for пишем set. Всё.

former_anonymous ★★★
() автор топика

вы код покажите, где оно «быстрей». set сделан для того, чтобы быстро определять наличие элемента в данном множестве, а не для быстрой «итерации» по всем элементам этого множества.

в силу специфики реализации скорость итерации там не может(с чего бы?) быть выше скорости итерации на непрерывном массиве.

скорее всего ваш код делает не то, что вам кажется.

alysnix ★★★
()

итератор по vector, и проверка есть ли каждый элемент в unordered_set и далее логика если есть

По этому коду - всегда есть, можно не проверять. Ваш К.О.;-)

AntonI ★★★★★
()
Ответ на: комментарий от alysnix

Задача https://leetcode.com/problems/longest-consecutive-sequence

Локальный код:

#include <vector>
#include <unordered_set>

using namespace std;

int longestConsecutive(const vector<int>& nums) {
    unordered_set<int> set;

    for (auto n : nums) {
        set.insert(n);
    }

    int longest = 0;

    // Итерация по nums медленнее чем по 'set' !!!
    for (auto n : nums) {
        // Start of a chain
        if (set.find(n - 1) == set.end()) {
            int length = 0;
            while (set.find(n + length) != set.end()) {
                length++;
            }
            longest = max(longest, length);
        }
    }

    return longest;
}

Тест:

nums содержит 50’000 элементов https://pastebin.com/diqtqMP4

Время отработки с nums в for - порядка 5000 мс на моём ПК.

Время отработки с set в for - порядка 10 мс на моём ПК.

Код main на Qt:


int main(int argc, char *argv[])
{
    const vector<int> nums = ...

    qint64 v = QDateTime::currentMSecsSinceEpoch();

    int longest = longestConsecutive(nums);

    qDebug() << nums.size() << longest << QDateTime::currentMSecsSinceEpoch() - v;

    return 0;
}

former_anonymous ★★★
() автор топика
Последнее исправление: former_anonymous (всего исправлений: 2)
Ответ на: комментарий от cobold

Если nums помещается в L1, а set вытесняется в L2, то вот и потеря производительности. Если мы итерируем по set вместо nums, то весь set помещается в L1, и более мы никуда не лезем. Отсюда и прирост производительности в 50 раз

former_anonymous ★★★
() автор топика
Последнее исправление: former_anonymous (всего исправлений: 1)
Ответ на: комментарий от former_anonymous

вы ищете максимальную монотонно возрастающую подстроку чисел в исходном массиве

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

while (set.find(n + length) != set.end()) {
                length++;
            }

например если текущее число 10 - вы ищете 11 во всей последовательности. а не на следующем месте. то есть вы решаете какую-то не ту задачу.

alysnix ★★★
()
Ответ на: комментарий от alysnix

вы ищете 11 во всей последовательности

по условиям задачи так и должно быть.

Для исходного массива [8, 5, 10, 6, 20, 7] монотонная последовательность будет [5,6,7,8]

former_anonymous ★★★
() автор топика
Ответ на: комментарий от former_anonymous

а, ну да. это я условие криво прочитал. тогда код примерно правильный. и что? это быстрей, чем какой код? откуда там еще некая «итерация по массиву»?

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

откуда у вас берется какая-то «итерация по массиву»? если вы пытаетесь найти перебором - находится ли след. число в массиве, то это очевидно неправильно, с точки зрения производительности. а через множество - правильно. и кеши там не причем.

alysnix ★★★
()
Ответ на: комментарий от former_anonymous
Время отработки с nums в for - порядка 5000 мс на моём ПК.

Время отработки с set в for - порядка 10 мс на моём ПК.

никакие кеши такие цифры не дадут. разница в 500 раз что-ли???

это или систематическая некая ошибка, ну или на крайняк - какая-то мегаоптимизация(там какие ключи компиляции-то?), когда компилятор, видя что работают только в set, сумел свернуть код так, что получилась такое вот ускорение. хотя уж слишком хорошо получилось.

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

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от alysnix

Дело не в stl, а в cache-friendly коде, который да, может дать 500 раз прироста. Магия! Возьми код и скомпилируй у себя, экспериментируй 🙂

former_anonymous ★★★
() автор топика
Последнее исправление: former_anonymous (всего исправлений: 1)
Ответ на: комментарий от former_anonymous

Магия!

с какой стати set стал cache friendly. он не более friedly чем vector, а то и менее, поскольку сложней устроен.

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от former_anonymous

Вставка в хэштаблицу имеет амортизированную O(1), в худшем случае - квадрат, если очень постараться то квадрат логарифмов.

Сперва докажи, что у тебя хорошие хэши подходящие под задачу.

anonymous
()
Ответ на: комментарий от alysnix

Код который использует контейнеры должен быть cache-friendly. Когда ты итерируешь по nums, а внутри цикла обращаешься к set, то nums и set постоянно выталкивают друг друга на кеш другого уровня. Теоретически, просто исходя из их размеров. Если ты итерируешь по set, то весь этот set спокойно лежит в L1 весь цикл, и цикл работает со скоростью света.

former_anonymous ★★★
() автор топика
Ответ на: комментарий от former_anonymous

Когда ты итерируешь по nums, а внутри цикла обращаешься к set, то nums и set постоянно выталкивают друг друга на кеш другого уровня.

когда у тебя итерация и по nums и set, у тебя просто в два раза больше размер данных, чем только по nums.

можно сказать, что в первом случае эффективный размер кеша в два раза меньше, чем во втором.

но это не даст 500 раз по производительности. и не будут они выталкивать «друг друга», а разлягутся в своих половинках неким статистическим образов.

alysnix ★★★
()
Ответ на: комментарий от former_anonymous

Я конечно давно в крестах не ковырялся, но может у тебя при векторе auto n копирует значение из массива в локальную переменную на стеке, а потом использует (не константа). А в случае с сетом загружает сразу в регистр (как константу).

no-such-file ★★★★★
()
Ответ на: комментарий от no-such-file

но может у тебя при векторе auto n копирует значение из массива в локальную переменную на стеке

не. это если вообще нет оптимизации. с оптимизацией он будет держать переменные в регистрах. и даже если б от что-то клал на стек - 500 раз это бы не дало. ну максимум какие-нить 20 процентов может и дало бы.

alysnix ★★★
()
Ответ на: комментарий от former_anonymous

Латентность доступа между L1 и L2 отличается в 2.5-3 раза, а не в 50. А между L1 и L3 в 10 раз, а не в 50. В конце концов возьмите профайлер да посмотрите, больше разговоров

cobold ★★★★★
()
Последнее исправление: cobold (всего исправлений: 1)
Ответ на: комментарий от former_anonymous

Локально или нет - без разницы, см. код, добавил. Разница - НА ПОРЯДОК.

У меня разницы вообще нет (в пределах погрешности). Упс? Я правда вектор забил от 0 до N-1. Если есть дубли, то разумеется set будет короче и это может сыграть.

Но вообще на таких смешных размерах (5e4) че то достоверно померять сложно.

AntonI ★★★★★
()
Последнее исправление: AntonI (всего исправлений: 2)