Ответ 1
Я предполагаю в своем ответе, что вы пытаетесь свести к минимуму время работы и не хотите тратить слишком много времени на создание кода для этого. Одна вещь, с которой я боролся сначала, когда училась писать высокоэффективные алгоритмы, заключалась в том, что иногда несколько проходов могут быть более эффективными. В этом случае, я бы сказал, в принципе, вы хотите иметь два прохода:
Сначала создайте фильтр, который позволяет игнорировать большинство (если не все) не дублированных шаблонов. Для этого:
- Выделить два битных массива (и при этом учитывать размеры кеша). Первый будет простым фильтром цветения. Второй будет дублирующим фильтром цветения.
- Когда вы проходите структуру на первом проходе, для каждой индексируемой структуры вычисляйте хэш-значение. Выберите соответствующий бит в первом фильтре цветения и установите его. Если этот бит уже был установлен, также установите соответствующий бит в дубликатном фильтре цветения.
На вашем втором проходе вам нужно будет выполнить "тяжелый" процесс фактического подтверждения матчей. Для этого выполните следующие действия:
- Снова сканируйте график и запишите любые структуры, которые соответствуют фильтру дублирования цветков, сгенерированному в первый проход.
- Поместите те, которые соответствуют хэш-таблице (в идеале, используя разные биты из вычисленного хэша)
- Когда обнаружен дубликат, сохраните эту информацию, где бы вы ее не собирали.
Этот алгоритм будет работать очень быстро на больших наборах данных, поскольку он значительно снизит давление на соответствующий уровень кеша. Есть также несколько улучшений, которые вы можете сделать, чтобы сделать его лучше в разных обстоятельствах.
- Чтобы повысить производительность многопоточной системы, на самом деле безопасно распараллеливать первый шаг. Чтобы сделать это, дайте каждому потоку (или компьютеру в кластере) фрагмент графика. Каждый должен вычислить свою собственную копию двух цветов. Затем цветки можно объединить в последний цвет. Функция восстановления справедлива (присутствует, дублируется) = (present1 ИЛИ present2, duplicate1 ИЛИ duplicate2 OR (present1 AND present2)). Этот шаг очень быстрый.
- Также безопасно распараллелить второй шаг, но его нужно немного изменить. Чтобы сделать это, вы берете дублирующий фильтр цветения с первого шага и используете его как фильтр на втором этапе, как и раньше. Однако вы не можете завершить окончательное сравнение так же легко. Вместо этого вы должны поместить потенциальные дубликаты в хэш-ведра. Затем, после того, как каждый осколок данных был записан в собственный список потенциальной повторяющейся хеш-таблицы, разделите данные вверх на хэш-ведро и на третьем шаге найдите дубликаты. Каждое хэш-ведро (с любого выхода на втором этапе) должно обрабатываться одним и тем же работником.
- В случаях, когда у вас есть большое количество структур, которые вы индексируете, вы можете повысить производительность за счет рекурсивного применения вышеуказанного алгоритма. Настройка состоит в том, что вы используете каждую подходящую категорию для выхода из вышеуказанного алгоритма в качестве вашего ввода в рекурсивный проход. Например, если вы индексируете только структуры, которые имеют до 5 элементов в первом запуске алгоритма, вы можете, когда вы рекурсируете, взять каждый набор дублированных подграфов и запустить алгоритм только на этих субграфах. Это было бы необходимо только с очень большими наборами данных, очевидно.
- Еще одна настройка, которую вы можете рассмотреть, если график очень большой, чтобы повысить эффективность ваших фильтров цветения, нужно перебирать алгоритм. Например, в первом проходе вы можете рассматривать только субграфы, которые имеют первую метку в качестве базы субграфа. Это уменьшит размер, необходимый вашим фильтрам цветка, и/или позволит вам отфильтровать больше субграфов на первом проходе.
Несколько примечаний для настройки выше:
- Учитывайте размеры кеша. Например, на чипе Intel Haswell каждое ядро имеет 32K в кэше L1 и 256K в кэше L2. Каждая строка кэша будет содержать 512 бит, поэтому, если вы пополните 1% фильтра цветения, будет затронута большая часть строк кэша. В зависимости от того, насколько быстро выполняются другие части алгоритма, и что другие вещи используют эти кеши, вы можете безопасно создать фильтр цветения, который содержит около 512 * 1024 записей (8 записей на бит на фильтр = 128k, на гиперпотоковых системах, что сколько L2 вы получаете) и по-прежнему поддерживают большую часть данных, установленных в кэше L2, и действительно активные элементы в L1. Для меньших наборов данных держите это число вниз, потому что нет смысла делать его большим. Если вы только отмечаете функции как потенциальные дубликаты, когда они составляют не менее 1% времени, это совершенно нормально.
- Параллелизация этого снова снова очень полезна в тех случаях, когда у вас есть тонны данных. Я предполагаю, что вы могли бы. Если вы распараллеливаете, вы должны рассмотреть геометрию. Использование этого алгоритма будет работать с частичными наборами данных на каждом компьютере. Затем вы можете запускать каждую итерацию (в варианте № 4) на каждом компьютере. В тех случаях, когда у вас есть огромные наборы данных, которые не позволят передавать все данные на все компьютеры.
В любом случае, чтобы подвести итог заявлению о сложности выполнения, я скажу, что это действительно зависит. Многие люди игнорируют тот факт, что увеличение рабочего набора данных приведет к тому, что доступ к памяти не будет одинаковым по стоимости. В сущности, вышеприведенный алгоритм, хотя и высокоэффективный, если он настроен соответствующим образом, будет работать очень быстро на небольшом наборе данных, но он действительно сияет с гораздо большими наборами данных, поскольку он позволяет эффективно использовать рабочий набор данных на любом уровне кеша (L1, L2, L3, ОЗУ, локальный диск, локальная сеть и т.д.). Сложность алгоритма будет зависеть от данных, но я не верю, что алгоритм намного быстрее может быть создан. Я не обращал внимания на то, как вы представляете подграфы, и там есть работа над достижением оптимального алгоритма, но, не зная большего, я не могу определить лучший способ сохранить эту информацию.
Причина, по которой алгоритм не может работать намного быстрее, чем тот, который я представил, заключается в том, что для первого прохода потребуется гораздо меньше работы, чем для второго, потому что он не требует разветвления и меньше работы побитовые операции. Поэтому мы можем сказать, что он мало добавляет к общей работе, которую мы делаем. Второй этап также настолько эффективен, насколько это возможно. Вы должны (запретить способ идеально описать каждую возможность с помощью конечного набора битов, который я объясню в секунде) фактически сравнить каждую функцию графика и записать информацию где-нибудь. Единственной переменной является то, сколько работы нужно проверить, нужно ли вам это делать. Проверка бит, в которой вы можете произвольно масштабировать частоту ошибок до 0%, так же хороша, как вы можете получить.
Для небольших наборов данных причина, по которой два прохода приносит вам пользу, заключается в том, что у вас может быть гораздо большее количество мощности цветения в меньшем объеме памяти. Но для действительно небольших наборов данных вы можете просто использовать второй шаг и игнорировать первый. Но, как минимум, вам нужно будет сохранить указатель для каждой хэш-цели. Это означает, что вам нужно будет записать в 32 или 64 раза больше данных для того же уровня фильтрации. Для небольших наборов данных это не имеет значения, потому что чтение является чтением, а запись - записью, но для более крупных наборов данных это может позволить вам выполнить тот же уровень фильтрации, оставаясь на заданном уровне кеширования. В тех случаях, когда вы должны работать на нескольких компьютерах или потоках, механизм, предусмотренный в этом алгоритме, будет более эффективным, поскольку данные могут быть объединены намного быстрее, и гораздо больше информации о возможных совпадениях можно обменять.
Теперь, наконец, как я уже говорил, вы можете быть немного лучше, если количество функций, которые вы проверяете на каждой итерации, будет уменьшено. Например, если вы проверяете только 32 возможных ярлыка и количество детей с определенной меткой в каждом проходе (и это ограничено 1024), вы можете представить это с 15 бит. Если вы ограничили число до 255, вы можете сохранить эту информацию с 32K. Чтобы это сделать в вашем случае, вам нужно будет использовать стратегии итерации, рекурсии и осколки, о которых я говорил выше, и вам нужно будет также отслеживать исходный граф и другую информацию. Я, честно говоря, сомневаюсь, что это будет хорошо работать, за исключением очень ограниченных ситуаций, но я включаю его для полноты.
Во всяком случае, это мой первый ответ на Stack Overflow, так что не слишком сильно на меня. Надеюсь, это было полезно!