Pull to refresh

Comments 25

а ссылку на pdf можно, а то так можно забыть с чего начал:-)

Спасибо.

Наверное, так, без оговорок, не совсем.верно писать?

  • Алгоритм O(n), оптимально использующий кэш, может победить алгоритм O(log n), использующий кэш плохо

При больших n вряд-ли.

Всегда найдётся достаточно большое n, что O(log(n)) будет меньше O(n). Но это в теории.
На практике реальная система может "всегда" работать с меньшими n.

Определение такого критического n для выбранных архитектур на мой взгляд интересная задача

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

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

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

Логично
Мы все же говорили про общее программирование, но крупные решения типа поиска могут работать и на специализированном

Почему же не отражён? По определению O(1) - какая-то константа. Алгоритм, выполняющийся за O(1) может выполняться хоть мгновенно, хоть 2 года.

Элемент массива 4 байта, sizeof(node_t) 12 байт (если указатель x64) плюс служебные данные при выделении памяти, т.е. надо в 4-5 раз больше памяти для связного списка. Даже если все элементы списка в куче будут последовательно друг-за-другом, то для 10К элементов только чтение из памяти потребует лишние 3-4М тактов.

Невнимательно прочитал, там 100К элементов, тогда лишние 30-40М тактов.

Для чистоты эксперимента я бы сравнивал список не с массивом int[], а с массивом node_t[], так хотя бы расход памяти сопоставимый. Иначе ускорение за счет меньшей сложности алгоритма нейтрализуется бОльшим объемом используемой памяти.

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

Я к тому чтобы автор привел тест к более корректному виду, а то взял какой-то девайс с медленной памятью, сравнивает чтение для двух разных типов хранилищ, отличающихся в 5 раз по объему используемой памяти, и делает вывод: " O(log n) смогло победить O(1) "

Имеенно платформозависимая сложность и исследуется...

Если тормозит память, то надо оптимизировать работу с памятью. Как вариант можно ускорить двоичный поиск за счет промежуточного индекса размером в несколько кэшлиний: как понял у автора сортированный массив ~500 элементов, разбиваем его на блоки по 4 (индексы 0-3, 4-7 и т.д.) всего 125 блоков, далее делаем массив куда пишем ID первого устройства в блоке, это и есть индекс, он займет всего 8 кэшлиний. Для поиска сначала ищем в индексе равное или ближайшее меньшее ID, индекс (i) найденного элемента будет номером блока, далее если ID совпало то берем array[i*4], если меньше проверяем ID в array[i*4+2], затем если там ID больше то array[i*4+1] иначе array[i*4+3].

В данном случае индекс с большой вероятностью будет в кэше, т.к. он не большой и к нему будет постоянное обращение.

примерно пятьсот элементов, каждый с 32-битным ID устройства и указателем на данные конфигурации

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

Сложность обоих O(n). В учебниках написано, что их показатели будут схожими

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

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

Двоичный поиск занимает O(log n), что теоретически хуже, чем O(1). Так написано в учебниках. Мой преподаватель алгоритмов был бы разочарован. ...
Они подразумевают, что все операции доступа к памяти стоят одинаково. Они подразумевают, что операции выполняются изолированно. Они подразумевают работу на идеальном компьютере, которого не существует.

Конечно преподаватель будет разочарован, у O-нотации другой смысл. Она ничего не подразумевает. Она описывает асимптотику и не говорит про реальное время. Она отвечает на вопрос "как изменится вычислетельная сложность алгоритма при изменении размера входных данных, когда n достаточно большое"

При фиксированном N O-нотация не обещает, что O(1) будет быстрее O(log N). Она обещает, что в пределе при N стремящемся к бесконечности, O(1) будет быстрее.

Доказательство того, что какая-нибудь quick sort в среднем работает за O(N log N), уже довольно сложное, если там пытаться ещё какие-то особенности процессора учитывать, то это будет ад. Вдобавок один и тот же математический алгоритм (типа хеш-таблиц) можно сильно по-разному сделать и получить сильно разную производительность. А ещё в зависимости от входных данных одна и та же хеш-таблица тоже будет иметь разную производительность. Одна и та же хеш-таблица с 70% заполненности и с 10% будет иметь сильно разное количество коллизий и скорость работы.

Например, есть подходы типа perfect hash: https://en.wikipedia.org/wiki/Perfect_hash_function - там коллизий вообще не будет и поиск в таблице гарантированно за O(1)

Я думаю, автор имеет в виду, что не стоит только на О нотацию смотреть при выборе алгоритма.

Ещё нужно учитывать константу, железо и ограничения на n.

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

То есть время загрузки составляло около 100 секунд?

Но в результате загрузчик оказался на 40% быстрее.

То есть новый загрузчик с бинарным поиском выполнялся за 60 секунд, и тогда наступило счастье?

В оригинале не на три порядка, а в три раза.

Будьте внимательнее.

Прикольная статья, за 20 лет я смотрю ничего не изменилось 😃. Крис Касперский по этой теме целую книгу написал "Техника оптимизации программ. Эффективное использование памяти".

https://www.livelib.ru/book/1000024314-tehnika-optimizatsii-programm-effektivnoe-ispolzovanie-pamyati-cdrom-kris-kasperski

За 20 лет всё стало хуже, доля разработчиков понимающих среду, в которой работает их ПО, упала очень сильно. Да и сама среда - это теперь не только железо, операционка и стандартная библиотека, но ещё и четыре слоя виртуализации, а также десяток слоёв разнообразной индирекции.

Вот +1000 про уровни кэша. Если нужна число дробилка, то компактный цикл со сложностью n! даёт фору сложности n2 при n до 8. Добавьте в алгоритм немного жадности и 8 превратится в 80-200.

Дешёвые по числу итерацией алгоритмы почти всегда кушают существенно больше памяти как для команд так и для данных.

Для баз данных история аналогичная. Бинарный индекс обеспечивает логарифмическую сложность при поиске, но вставка и чтение с определённого объёма начинают стоить в разы дороже при использовании индекса.

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

Если нужен реальный пример грамотной работы с кэшем -- замена бинарного дерева поиска на B-дерево, в котором размер узла может быть выставлен в соответствие размеру кэш линии.

Тут достаточно только "примерно пятьсот элементов". Остальную часть статьи можете удалить, это и так известно всем программистам. Напомню, что log2(500) ~ 9, а в хэш-таблице не именно 1 операция, как минимум ещё вычисление хэша, которое опускают при больших N.

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

Sign up to leave a comment.

Articles