Search
Write a publication
Pull to refresh

Comments 36

К счастью, в большинстве языков програмирования есть замечательная функция seek()

Есть ещё mmap() с которой бегать по файлу несколько проще.

HDD диски не очень любят читать "назад"

Оно, конечно, ещё встречается (скажем, на моём домашнем ноуте), но на реальной рабочей машине...

Одни и те же позиции считываются многократно. А это плохо.

Дисковый кеш операционки не помогает?

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

Если попробовать, так (нужна БД которая имеет нормальную поддержку работы с uuid - Postgres)

CREATE TABLE hashes(
   id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
   val int
);

и вот тут делать поиск по uuid.

Есть ещё mmap() с которой бегать по файлу несколько проще.

Лимитировано размером адресного пространства процесса. Хотя на 64-бит стало существенно проще. :)

Ну так 64 бита не год и не два назад появилось. Сейчас, чтобы файл не замапился, надо сильно постараться, начиная с выбора ФС.

Тут скорее не "когда 64-бит появился", а что сейчас уже можно не оглядываться на 32-бит.

1) Да, mmap() может быть быстрее, но с ней достаточно просто напортачить, так как она может менять не только данные в файле, но и любые данные за его пределами. К тому же возникнут сложности при многопоточности (у меня она реализована). Но вариант тоже неплохой.
2) Я уточнял, что это актуально для больших строк. Замедление происходит при переходе из одного блока (8КБ кажется) в предыдущий.
3) Кэш операционки сохраняет очень большие фрагменты - по 64КБ или 256КБ обычно. Учитывая, что он занимает по умолчанию до 10% оперативки, максимально удасться кэшировать 250000 - 60000 позиций при 16GB оперативной памяти. Но фактически меньше, так как он постоянно очищается.

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

так как она может менять не только данные в файле, но и любые данные за его пределами

Это как? Начнём с того, что файл вы вроде только читаете.

К тому же возникнут сложности при многопоточности

Опять же, какие проблемы с одновременным чтением?

Учитывая, что он занимает по умолчанию до 10% оперативки

Да может и почти всю. Вот у знакомого на домашнем сервере 64Gb на 32битной Windows (c PAE, естественно) с минимумом живых процессов, память в основном кеширует дисковые запросы.

Согласен, что мелкая гранулярность в самописном кеше позволяет более эффективно использовать память.

На самом деле, проще открыть в бинарном режиме 'rb' и читать не строки, а байты

Ну и конечно поиск сломается, если мы захотим найти первую строку

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

C rb нельзя использовать readline и придётся писать велосипед. Ну а поиск золотым сечением это хорошая идея, спасибо за совет. Я его не тестировал на скорость, но насколько я помню, он актуален, когда много времени занимает сравнение элементов, а не чтение.

Тест несправедлив к базе данных. find_hashes_csv - это по сути один запрос, а база каждую итерацию вынуждена выполнять много оверхеда, типа парсинга SQL-запроса, обмен данными между процессами. И и со всем этим она умудряется работать лишь немного медленнее чем ваш алгоритм.
Предлагаю, справедливости ради, переписать тест для базы чтобы запросы отправлялись батчами.

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

Цитирую прорекламированный бот:

? Всё что вам нужно - отправить файл с электронными почтами и паролями боту.
? Файл может быть в одном из следующих форматов:
├ email:password
├ email;password
├ email password
└ email

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

Если ты пересылаешь свой пароль для проверки не утек ли он - правильный ответ - ТЕПЕРЬ точно утек.

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

Одни и те же позиции считываются многократно. А это плохо. Если мы используем SSD, такой подход может изностить его со временем

Что? Где вы нашли ssd, который изнашивает чтение?

Это называется "read disturb". При частом чтении одной позиции заряд ячейки и соседний может измениться и что бы избежать потери данных, их приходится перезаписывать. Причём сразу блоком. Источник - уроки информатики. Не знаю, насколько это актуально для современных дисков, но решил не проверять.

Хорошая попытка, но матчасть слабовата.

Во-первых, вам не нужен кэш - ОС и так кэширует данные с диска, причем сразу страницами по 4 КиБ.
Во-вторых, никак не обыгран readahead — когда программа читает с какого-то места в файле, ОС, разумно полагая, что чтение продолжится и дальше, читает не только то что запросили, а достаточно много сверх того (128 КиБ по дефолту в линуксе, емнип). То есть можно сикать по файлу так, чтобы в память поднималось по 64К сзади и 64К спереди, для пущей эффективности.

Там сверху упоминали mmap - может быть, работать так точно удобнее, но по факту скорее всего не быстрее, чем обычный файловый IO, а может быть и медленнее - надо бенчмаркать.

И да, ничего удивительного в том, что вы обыграли MySQL, ведь у вас hash_value - не VARCHAR, а TEXT (это очень неэффективный способ хранить строки), и индекс по нему - полнотекстовый, а не b-tree или какой там по дефолту :)

mysql 8.0+ (возможно даже так будет лучше)

CREATE TABLE hashes (
    uuid BINARY(16) PRIMARY KEY,
    value INT
);

Кэш нужен, так как операционка кэширует большими блоками - по 64 или 256 килобайтов. А значит в кэш, который занимает не более 10% оперативки, влезет всего несколько десятков тысяч позиций. Если кэшировать только одну строку, это будет быстрее и влезут десятки миллионов позиций. Но для небольших файлов и кэша операционки хватит.

Про readahead - это скорее проблема, поищу потом способ это отключить. Но ты прав, можно и использовать в свою пользу. И кстати это ещё один аргумент за кэширование средствами программы.

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

По поводу хранения в виде текста - в моём случае строки могут быть любой длины и varchar не подходит.

Я не знаю как в винде, боженька миловал, но в линуксе кэш занимает всю память :) и он там, несколько упрощая, тоже LRU. И если уж вы решили поработать со своим кэшированием, то возможно стоит поиграться вокруг O_DIRECT, потому что иначе все чтения все равно будут в пейджкэш подниматься

Про индекс вы правы, по TEXT может быть и обычный, я правда что-то не вижу длины индексируемого префикса в DDL, он там вроде обязательный. В любом случае spatial типы в мускуле - такое себе.

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

А вот за статью - минус. В основном, вот за это:

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

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

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

А есть еще и хэш-таблицы и другие структуры...

Почему ваши тесты говорят обратное?
Ну, например, на sql вы сделали две таблицы вместо одной. Одну - где кластерный индекс по id, а в листьях - hash_value. Вторую (индекс) - это таблица, где кластерный индекс по hash_value, а в данных - id. В итоге поиск идет в начале по одной таблице, а потом - по другой. Создайте таблицу в БД правильно, одну, с кластерным индексом по hash_value, это возможно в my_sql. Скорее всего, одного этого будет достаточно, что бы sql опередил ваш поиск.
Еще, скорее всего, вы читаете из дискового кэша и преимущество B-деревьев уменьшается. Попробуйте сбросить кэши перед тестом. Ну или увеличите количество записей, например, до миллиарда, что бы они не помещались в кэш.

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

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

Почитайте этот том 3 Кнута, там все это есть. И двоичный поиск есть, и сравнение эффективности всех этих алгоритмов.

И предлагаю провести тест с такой таблицей:

CREATE TABLE hashes (
  uuid BINARY(16) PRIMARY KEY,
  value INT) WITHOUT ROWID;

Здесь конструкция WITHOUT ROWID говорит sql lite, что не надо создавать свой ключ и отдельный индекс. Будет одна таблица с кластерным индексом по uuid. Объем данных должен сократится в два раза и ускорится поиск. Подозреваю, что тесты очень сильно изменятся. Причем с ростом количества записей SQL будет их извлекать почти с той же скоростью, а двоичный поиск тормозить все сильнее и сильнее.

Автор явно работает с какой-то очень простой структурой, и неясно - а зачем здесь вообще строки? Бинарная структура фиксированного размера уменьшит размер данных в несколько раз (что особо актуально при терабайтных размерах данных) и позволит сделать нормальный бинарный поиск.

Можно сделать свой индекс по файлу так, чтобы он помещался в памяти. 2.5 ТБ - это 2.5 миллиона мегабайтных блоков. Сначала вести бинарный поиск блока, а потом прочитать 1Мб блок в память и искать в нем.

В общем, результат в 20 rpc не особо впечатляет.

PS. Кэширование открытия файла??? Почему не открыть файл один раз и не передать его в функцию поиска?

PPS. Нелюбовь к SQL не разделяю. Надо еще разобраться - почему так медленно получилось? Если индекс в памяти - должно намного быстрее работать.

20 поисков в секунду на HDD это практически потолок, если не использовать зашкаливающие по сложности алгоритмы. 90% времени и так уходит на перемещение головки. При этом база у меня не одна, а разделена на 5 частей, в каждой из которых поиск происходит независимо, если слить будет 100 в секунду, но это сортировка 40 миллиардов строк, что долго.
Про кэширование файла - у меня файл не один, а примерно 10000 во всех базах. Тоже в алфавитном порядке. Примерно то что ты и предложил с блоками. Все файлы одновременно ты не откроешь да и толку от этого ноль.

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

Точно!
Количество чтений - это логарифм от количества записей. Основание логарифма - это коэффициент ветвления дерева. Для двоичного поиска он равен двум. А для B-дерева намного больше, обычно десятки. Именно по этому двоичный поиск целесообразно использовать там, где стоимость чтения невелика - в памяти. Для диска это плохой вариант.

20 поисков в секунду на HDD это практически потолок

Не потолок. Типичный HDD дает около 100-200 iops. Судя по скорости, сейчас у вас на один поиск расходуется, примерно 5-10 дисковых операций (реальных, а не вызовов чтения из программы).

Индекс на 40 млрд записей блоками по 64кБ (~1000 записей, оптимальный размер блока надо подобрать экспериментально) - это всего 40 млн блоков, информация о которых элементарно умещается в памяти. Т.е. на каждую операцию поиска будет тратиться ровно одна операция ввода-вывода, весь остальной поиск - в памяти. Это увеличит rps в 5-10 раз при условии, что все упирается в скорость диска. И реализуется такой велосипед совершенно несложно. Даже деревья не нужны, просто плоский сортированный список блоков в памяти, по которому проходим бинарным поиском...

При этом база у меня не одна, а разделена на 5 частей, в каждой из которых поиск происходит независимо, если слить будет 100 в секунду

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

В теории это всё хорошо. А на практике - ты когда-нибудь пробовал отсортировать 2,5TB данных? Я пробовал и предпочитаю тот алгоритм, в котором сложнее всего напортачить, что бы не пришлось всё переделывать. И я измерял скорости. Узкое место - ввод вывод, так как на SSD скорость выше на порядки. И большая часть времени уходит именно на челночный бег головки по диску.

Может это просто опечатка, но в тексте вы указываете что используете для теста MySQL, однако в коде у вас используется sqlite3. Чем вы это объясните?

<занудство>

Я один в первом абзаце увидел "расскажу как ускорить бинарный поиск в сотни раз", а в конце сравнение резульиатов менее чем в 2 раза? Понимаю, что не все рассказали, но хотелось бы получать то, что заявлено или тогда уже переформулировать, что "в конце цикла статей вы узнаете как ускорить бинарный поиск в сотни раз".

</занудство>

Так-с. Внесу немного свою лепту:

Предложение 1: Экономим место в СУБД

В СУБД ты можешь делать не create table mytable(hash text, password text); create index on mytable(hash);

Гораздо круче сделать так: create table mytable(password text); create index on mytable(md5(password));

Получается, что ты хэш будешь хранить только в индексе. Для поиска: `select from mytable where md5(password) = ?`

Предложение 2:
Если ты пользуешься всё-же поиском только одного единственного значения ( в том смысле, что не делаешь поиск нескольких хэшей одновременно), то рекомендую посмотреть в сторону хэш-индексов ( `create index on mytable using hash(md5(password))` )
То же самое можешь сделать и на уровне csv.

Поиск по хэшу идёт быстрее, но есть некоторые недостатки ( см ниже)

Предложение 3:
А вот если ты вдруг захочешь искать несколько строк ( представим на миг, что ты делаешь сервис по дэхэшированию):
Ты приятно удивишься, что поиск N значений разом идёт на много быстрее, чем N поисков по одному ( даже если убрать овертайм на запросы и т.д.)
Но при хэш-индексе ( и п.2) такая фигня не прокатит :)

В случае, если подобное захочешь делать с файлами - берёшь входной массив хэшей, сортируешь по возрастанию и делаешь поиск ( когда нашёл первый хэш, то второй 100% будет идти после него, соответственно `lef{i+1}> = mid{i}+1`

UPD: Интересно узнать, а конфиг СУБД был стандартный или ей разрешалось тоже забирать память из системы ?)

Велосипеды там, где они вообще не нужны.

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

Sign up to leave a comment.

Articles