Внешняя сортировка строк, ограниченная памятью, с объединенными и подсчитанными дубликатами на критическом сервере (миллиарды имен файлов)

Наш сервер создает файлы типа {c521c143-2a23-42ef-89d1-557915e2323a}-sign.xml в папке журнала. Первая часть - GUID; вторая часть - это шаблон имени.

Я хочу подсчитать количество файлов с одинаковым шаблоном имен. Например, мы имеем

{c521c143-2a23-42ef-89d1-557915e2323a}-sign.xml
{aa3718d1-98e2-4559-bab0-1c69f04eb7ec}-hero.xml
{0c7a50dc-972e-4062-a60c-062a51c7b32c}-sign.xml

Результат должен быть

sign.xml,2
hero.xml,1

Общие типы возможных шаблонов имен неизвестны, возможно, превышает int.MaxValue.

Общее количество файлов на сервере неизвестно, возможно, превышает int.MaxValue.

Требования

Конечный результат должен быть отсортирован по шаблону имени.

Сервер, на котором запускается этот инструмент, является суперкритическим. Мы должны иметь возможность указать использование памяти (МБ) и количество созданных временных файлов, если они есть, перед запуском инструмента и без знания каких-либо признаков папки журнала.

Мы используем язык С#.

Моя идея:

  • Для первых 5000 файлов, подсчитайте вхождения, напишите результат на Group1.txt.
  • Для вторых 5000 файлов, подсчитайте вхождения, напишите результат на Group2.txt.
  • Повторяйте до тех пор, пока все файлы не будут обработаны. Теперь у нас есть группа групповых файлов.

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

   Group1.txt     Group2.txt   Group3.txt     Group4.txt   
       \            /            \                /
       Group1-2.txt                Group3-4.txt
                  \                 /
                    Group1-4.txt

Group1-4.txt - конечный результат.

Разногласия между мной и моим другом - это то, как мы посчитаем события.

Я предлагаю использовать словарь. Шаблон имени файла является ключевым. Пусть m - размер раздела. (В этом примере это 5000.) Тогда временная сложность O (m), пространственная сложность O (m).

Мой друг предлагает отсортировать шаблон имени, а затем подсчитать вхождение за один проход, так как все одинаковые шаблоны имен теперь объединены. временная сложность O (m log m), пространственная сложность O (m).

Мы не можем убеждать друг друга. Вы, ребята, видите какие-либо проблемы двух методов?

Ответ 1

IDK, если была изучена внешняя сортировка с совмещением дубликатов. Я нашел статью 1983 года (см. Ниже). Обычно алгоритмы сортировки разрабатываются и изучаются с учетом сортировки объектов по ключам, поэтому дублирующиеся ключи имеют разные объекты. На этом может быть какая-то существующая литература, но это очень интересная проблема. Вероятно, это просто считается применением компактных словарей в сочетании с внешней сортировкой слияния.

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


TL: DR резюме полезных идей, так как я слишком подробно рассказывал о многих вещах в основной части этого ответа:

  • Пакетные границы, когда размер словаря достигает порога, а не после фиксированного количества входных файлов. Если в группе из 5000 строк было много дубликатов, вы все равно не будете использовать очень много памяти. Вы можете найти путь к дублированию в первом проходе таким образом.

  • Отсортированные партии делают слияние намного быстрее. Вы можете и должны объединять много- > один вместо бинарного слияния. Используйте PriorityQueue, чтобы выяснить, какой входной файл имеет следующую строку.

  • Чтобы избежать использования памяти при сортировке ключей в хеш-таблице, используйте словарь, который может выполнять обход ключей в порядке. (т.е. сортировать на лету.) Там SortedDictionary<TKey, TValue> (двоичный древовидный). Это также чередует использование процессора для сортировки с ожиданием ввода-вывода для получения входных строк.

  • Radix-сортировать каждую партию в выходы по первому символу (a-z, не-алфавит, который сортируется до A и не-алфавит, который сортируется после z). Или какой-нибудь другой выбор, который хорошо распределяет ваши ключи. Используйте отдельные словари для каждого ведра счисления и пустые только самые большие в пакет, когда вы нажмете на потолок памяти. (лучшая эвристика выселения, чем "самая большая", может стоить того.)

  • дроссельный ввод-вывод (особенно при слиянии), а также проверка загрузки ЦП и давления памяти. Адаптируйте поведение соответственно, чтобы убедиться, что вы не нанесете ущерба, когда сервер занят.

  • Для меньших временных файлов за счет процессорного времени используйте кодировку с общим префиксом или, возможно, lz4.

  • Космический эффективный словарь позволит увеличить размер партии (и, следовательно, большее окно поиска дубликатов) для одной и той же верхней границы памяти. A Trie (или лучше, Radix Trie) может быть идеальным, поскольку он хранит символы в узлах дерева, причем общие префиксы сохраняются только один раз. Направленные ациклические графы Word еще более компактны (поиск избыточности между обычными подстроками, которые не являются префиксами). Использование одного словаря является сложным, но возможно возможным (см. Ниже).

  • Воспользуйтесь тем фактом, что вам не нужно удалять любые узлы или строки дерева, пока вы не опустошите весь словарь. Используйте растущий массив узлов и еще один растущий массив char, который собирает строки в хвост. (Полезно для узлов Radix Trie (multi-char), но не является регулярным Trie, где каждый node является единственным char.)

  • В зависимости от того, как распределяются дубликаты, вы можете или не сможете найти очень много на первом проходе. Это имеет некоторые последствия, но на самом деле не влияет на то, как вы заканчиваете слияние.


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

Сбросьте как можно больше ненужных символов (например, потеряйте .xml, если они все .xml).


Может быть полезно сделать интенсивную работу CPU/памяти на отдельном компьютере, в зависимости от того, какое другое оборудование у вас есть с быстрым сетевым подключением к вашему критическому серверу.

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


И теперь, поскольку ни один из других ответов действительно не сослал все части, вот мой фактический ответ:

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

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

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

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


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

Не сортировка ваших партий делает временную сложность первого прохода O (N), но либо вам придется сортировать в какой-то момент позже, либо ваши более поздние этапы имеют худший случай, который значительно хуже. Вы хотите, чтобы ваш результат сортировался по всему миру, поэтому подход, отличный от решения RadixSort, не позволяет избежать O (N log N) где-то.

При ограниченном размере партии ожидаются шаги слияния O (log N), поэтому ваш первоначальный анализ пропустил сложность O (N log N) вашего подхода, игнорируя, что должно произойти после того, как будут записаны партии Phase1.


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

Если в любом случае вы не можете делать много дублирования в каждой партии, тогда вам нужно оптимизировать для объединения возможных совпадающих ключей для следующего этапа. Ваш первый этап может группировать строки ввода по первому байту, до 252 или около того выходных файлов (не все 256 значений являются юридическими именами имен файлов) или в 27 или около того выходных файлов (алфавит + разное) или 26 + 26 + 1 для верхнего/нижнего регистра + без алфавита. Файлы Temp могут игнорировать общий префикс из каждой строки.

Тогда большинство из этих партий первой стадии должны иметь гораздо более высокую плотность дубликатов. Фактически, это распределение Radix входов в выходные ковши полезно в любом случае, см. Ниже.

Вы все равно должны сортировать свои выходы первой ступени в кусках, чтобы дать следующий проход гораздо более широкое окно поиска дубликатов для той же ОЗУ.


Я собираюсь потратить больше времени на домен, где вы можете найти полезное количество дубликатов в исходном потоке, прежде чем использовать до 100 Мбайт ОЗУ или что бы вы ни выбрали в качестве верхнего предела.

Очевидно, мы добавляем строки к некоторому словарю для поиска и подсчета дубликатов "на лету", при этом требуя достаточного количества хранилища для набора уникальных строк. Простое хранение строк, а затем их сортировка были бы значительно менее эффективными, потому что мы бы быстрее ударили по пределу RAM без обнаружения дубликатов на лету.

Чтобы свести к минимуму работу фазы 2, фаза 1 должна найти и подсчитать как можно больше дубликатов, уменьшая общий размер данных p2. Уменьшение количества слияния для фазы 2 тоже хорошо. Большие партии помогают с обоими факторами, поэтому очень полезно приблизиться к потолку памяти, насколько это безопасно в фазе 1. Вместо того, чтобы писать партию после постоянного количества строк ввода, сделайте это, когда потребление памяти приблизится к выбранному потолку. Дубликаты подсчитываются и отбрасываются, и не требуют дополнительного хранения.

Альтернативой точному учету памяти является отслеживание уникальных строк в вашем словаре, что легко (и сделано для вас реализацией библиотеки). Накопление длины добавленных строк может дать вам хорошую оценку памяти, используемой для хранения строк. Или просто сделайте предположение о распределении длины строки. Сначала сделайте хеш-таблицу правильным размером, чтобы она не увеличивалась при добавлении элементов, поэтому вы останавливаетесь, когда она заполняется на 60% (коэффициент загрузки) или что-то в этом роде.


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

Мы знаем, что мы хотим, чтобы наша партия отсортировалась, как только мы увидели достаточно уникальных ключей, поэтому имеет смысл использовать словарь, который может быть пройден в отсортированном порядке. Сортировка на лету имеет смысл, потому что ключи будут медленно, ограниченные дисковым IO, так как мы читаем из метаданных файловой системы. Один недостаток заключается в том, что большинство ключей, которые мы видим, являются дублирующими, тогда мы делаем много запросов O (log batchsize), а не много поисков O (1). И более вероятно, что ключ будет дублировать, когда словарь большой, поэтому большинство этих запросов O (log batchsize) будут иметь размер партии около max, а не равномерно распределены между 0 и макс. Дерево оплачивает служебные данные O (log n) для сортировки для каждого поиска, независимо от того, был ли ключ уникальным или нет. Хэш-таблица оплачивает стоимость сортировки только после удаления дубликатов. Таким образом, для дерева это O (total_keys * log unique_keys), хэш-таблица - O (unique_keys * log unique_keys) для сортировки партии.

Хэш-таблица с максимальным коэффициентом загрузки, установленным в 0.75, или что-то может быть довольно плотным, но для сортировки KeyValuePair перед тем, как выписать партию, возможно, помещается на использование стандартного словаря. Вам не нужны копии строк, но вы, вероятно, в конечном итоге скопируете все указатели (refs), чтобы поцарапать место для не-на месте, и, возможно, также, когда выходите из хэш-таблицы перед сортировкой. (Или вместо указателей KeyValuePair, чтобы избежать необходимости возвращаться и искать каждую строку в хеш-таблице). Если короткие всплески большого потребления памяти являются допустимыми и не приводят к тому, что вы меняете/на диск, вы можете быть в порядке. Этого можно избежать, если вы можете сделать сортировку на месте в буфере, используемом хеш-таблицей, но я сомневаюсь, что это может произойти с контейнерами стандартной библиотеки.

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

Стандартная библиотека .NET имеет SortedDictionary<TKey, TValue>, которые, по словам разработчиков, реализованы с помощью двоичного дерева. Я не проверял, имеет ли он функцию перебалансировки или использует красно-черное дерево, чтобы гарантировать наихудшую производительность O (log n). Я не уверен, сколько издержек памяти у него было бы. Если это одноразовое задание, я бы настоятельно рекомендовал использовать его для его быстрого и легкого выполнения. А также для первой версии более оптимизированного дизайна для многократного использования. Вероятно, вы найдете его достаточно хорошим, если не найдете хорошую реализацию библиотеки Tries.


Структуры данных для отсортированных по памяти словарей

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

Вторичным воздействием выбора структуры данных является объем трафика памяти, который мы генерируем во время работы на критическом сервере. Сортированный массив (с временем поиска O (log n) (двоичный поиск) и временем вставки O (n) (элементы тасования, чтобы освободить место)) будет компактным. Тем не менее, это было бы не просто медленным, это бы насыщало пропускную способность памяти с помощью memmove. Использование 100% -ного использования ЦП окажет большее влияние на производительность сервера, чем на 100% использование ЦП в бинарном дереве. Он не знает, где загрузить следующий node, пока он не загрузит текущий node, поэтому он не может запросить запросы к конвейеру. Отражающие ошибочные предсказания сравнений в поиске дерева также помогают умеренному потреблению пропускной способности памяти, которая разделяется всеми ядрами. (Правильно, некоторые программы на 100% -ПЦП хуже других!)

Хорошо, если опустошение нашего словаря не оставляет память фрагментированной, когда мы ее опорожняем. Узлы деревьев будут постоянными, однако, поэтому куча рассеянных дыр будет использоваться для будущих древовидных распределений node. Однако, если у нас есть отдельные словари для нескольких кодов счисления (см. Ниже), ключевые строки, связанные с другими словарями, могут быть смешаны с узлами дерева. Это может привести к тому, что malloc с трудом переустанавливает всю освобожденную память, что потенциально увеличивает фактическое использование памяти операционной системы с помощью небольшого фактора. (Если сборка мусора С# не делает уплотнение, в этом случае обходится фрагментация.)

Поскольку вам никогда не нужно удалять узлы, пока вы не захотите очистить словарь и удалить их все, вы можете сохранить свои узлы дерева в растущем массиве. Таким образом, управление памятью должно отслеживать только одно большое распределение, уменьшая накладные расходы на бухгалтерию по сравнению с malloc каждого node отдельно. Вместо реальных указателей левые/правые дочерние указатели могут быть индексами массива. Это позволяет использовать для них только 16 или 24 бит. (A Heap - это еще один вид двоичного дерева, хранящегося в массиве, но он не может быть эффективно использован в качестве словаря. дерево, но не дерево поиска).

Хранение строковых ключей для словаря обычно выполняется с каждой строкой в ​​качестве объекта, выделенного отдельно, с указателями на них в массиве. Снова вам никогда не нужно удалять, увеличивать или даже модифицировать, пока вы не будете готовы их удалить, вы можете упаковать их в хвост в массиве char с завершающим нулевым байтом в конце каждого один. Это снова экономит много бухгалтерского учета, а также позволяет легко отслеживать, сколько памяти используется для ключевых строк, позволяя вам безопасно приблизиться к вашей верхней границе памяти.

Trie/DAWG для еще более компактного хранения

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

A Trie хранит строки в древовидной структуре, что дает вам общее префиксное сжатие. Его можно перемещать в отсортированном порядке, поэтому он сортируется "на лету". Каждый node имеет столько же детей, сколько в этом списке есть разные следующие символы, поэтому это не двоичное дерево. Частичная реализация С# Trie (удаление не написана) можно найти в этом SO-ответе на вопрос, подобный этому, но не требующий пакетной/внешней сортировки.

Узлы Trie должны хранить потенциально много указателей для детей, поэтому каждый node может быть большим. Или каждый node может быть переменным размером, удерживая список nextchar: ref пары внутри node, если С# делает это возможным. Или, как говорится в статье в Википедии, node может фактически быть связанным списком или бинарным деревом поиска, чтобы избежать потери пространства в узлах с небольшим количеством детей. (У более низких уровней дерева будет много этого.) Маркеры/узлы в конце слова необходимы, чтобы различать подстроки, которые не являются отдельными записями словаря, и те, которые есть. Наше поле подсчета может служить этой цели. Count = 0 означает, что окончание подстроки здесь не находится в словаре. count >= 0 означает, что это.

Более компактным Trie является дерево Radix или дерево PATRICIA, в котором хранится несколько символов на node.

Другим продолжением этой идеи является детерминированный ациклический автомат конечного состояния (DAFSA), иногда называемый прямым ациклическим графиком слов (DAWG) но обратите внимание, что статья DAWG wikipediaэто другое дело с тем же именем. Я не уверен, что DAWG может быть пройден в отсортированном порядке, чтобы получить все ключи в конце, и, как указывает Википедия, хранение связанных данных (например, дублирующее количество) требует модификации. Я также не уверен, что они могут быть построены постепенно, но я думаю, что вы можете выполнять поиск без сжатия. Новые добавленные записи будут храниться как Trie, до этапа уплотнения каждые 128 новых ключей объединяют их в DAWG. (Или запустите сжатие реже для больших DAWG, поэтому вы не делаете этого слишком много, например, удваиваете размер хеш-таблицы, когда она должна расти, вместо того, чтобы расти линейно, чтобы амортизировать дорогостоящий op.)

Вы можете сделать DAWG более компактным, сохранив несколько символов в одном node, когда нет ветвления/слияния. Эта страница также упоминает подход Huffman-кодирования для компактных DAWG и содержит некоторые другие ссылки и статьи.

Эффект DAWG от JohnPaul Adamovsky (в C) выглядит хорошо и описывает некоторые оптимизации, которые он использует. Я не смотрел внимательно, чтобы увидеть, может ли он сопоставить строки для подсчета. Он оптимизирован для хранения всех узлов в массиве.

Этот ответ для слов с дублированием слов в текстовом вопросе 1 ТБ предлагает DAWG и имеет пару ссылок, но я не уверен, насколько он полезен.


Написание партий: Radix на первом символе

Вы можете включить свой RadixSort и хранить отдельные словари для каждого стартового символа (или для a-z, не-алфавита, который сортируется перед a, не-алфавитом, который сортируется после z). Каждый словарь записывает в другой файл temp. Если у вас есть несколько вычислительных узлов, доступных для подхода MapReduce, это будет способ распространения работы слияния на вычислительные узлы.

Это позволяет сделать интересную модификацию: вместо того, чтобы записывать все конверты радиуса сразу, записывать только самый большой словарь в виде пакета. Это предотвращает крошечные партии, идущие в несколько ведер каждый раз, когда вы. Это уменьшит ширину слияния в каждом ковше, ускоряя фазу2.

С двоичным деревом это уменьшает глубину каждого дерева примерно на log2 (num_buckets), ускоряя поиск. С Trie это избыточно (каждый node использует следующий символ в качестве основы для упорядочения дочерних деревьев). С DAWG это на самом деле вредит вашей космической эффективности, потому что вы теряете нахождение избыточности между строками с разными пусками, но позже разделяемыми частями.

Это может вести себя плохо, если есть несколько нередко тронутых ведер, которые продолжают расти, но обычно не являются самыми большими. Они могут использовать большую часть вашей общей памяти, делая для небольших партий из обычно используемых ковшей. Вы могли бы реализовать более умный алгоритм выселения, который записывает, когда последний ведро (словарь) был опустошен. Оценка NeedsEmptying для ведра будет чем-то вроде продукта размера и возраста. Или, может быть, какая-то функция возраста, например, sqrt (возраст). Некоторый способ записать, сколько дубликатов, найденных каждым ковшом с момента последнего опорожнения, было бы полезно. Если вы находитесь в месте вашего входного потока, где есть много повторов для одного из ведер, последнее, что вы хотите сделать, это часто его пустая. Может быть, каждый раз, когда вы находите дубликат в ведре, увеличивайте счетчик. Посмотрите на соотношение между возрастом и дублями. Локализаторы с низким потреблением, сидящие там, беря RAM вдали от других ведер, будут легко найти этот путь, когда их размер начнет подкрадываться. Действительно ценные ведра могут храниться даже тогда, когда они являются самыми большими, если они находят много дубликатов.

Если ваши структуры данных для отслеживания возраста и совпадений найдены структурными массивами, разделение (last_emptied[bucket] - current_pos) / (float)dups_found[bucket] может быть эффективно выполнено с векторной плавающей точкой. Одно целочисленное деление медленнее, чем одно FP-деление. Одно разделение FP имеет ту же скорость, что и 4 подразделения FP, и компиляторы могут, мы надеемся, авто-векторизация, если вы упростите их таким образом.

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

выбор способа ввода

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

Существует множество способов для объединения строк в ведра. Некоторые из них гарантируют, что каждый элемент в ковше сравнивается меньше, чем каждый элемент в каждом более позднем ковше, поэтому создание полностью отсортированного вывода легко. Некоторые не будут, но имеют другие преимущества. Будут компромиссы между выборами для балансировки, все из которых зависят от данных:

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

Я уверен, что умные люди думали о хороших способах переносить строки передо мной, так что это, вероятно, стоит поискать, если очевидный подход первого персонажа не идеален. Этот специальный прецедент (сортировка при исключении/подсчете дубликатов) не является типичным. Я думаю, что большая часть работы по сортировке учитывает только сортировки, которые сохраняют дубликаты. Таким образом, вы, возможно, не найдете многого, что поможет выбрать хороший алгоритм bucketing для дублирования внешнего сортировки. В любом случае он будет зависящим от данных.

Некоторые конкретные варианты для bucketing: Radix = первые два байта вместе (все еще сочетая верхний/нижний регистр и объединение неалфавитных символов). Или Radix = первый байт хэш-кода. (Требуется глобальное слияние для создания отсортированного вывода.) Или Radix = (str[0]>>2) << 6 + str[1]>>2. то есть игнорировать низкие 2 бита первых двух символов, объединить вместе [abcd][abcd].*, [abcd][efgh].* и т.д. Это также потребует некоторого объединения отсортированных результатов между некоторыми наборами ковшей. например daxxx будет в первом ковше, но aexxx будет во втором. Но для создания отсортированного окончательного вывода необходимо объединить только ведра с теми же самыми первыми char высокими битами.

Идея для обработки выбора bucketing, которая дает большое дублирование, но требует сортировки слияния между ведрами: при записи вывода фазы2 ведите его с первым символом в качестве основы для создания порядка сортировки, который вы хотите. Каждый ведро фаз1 рассеивает выход в ведра фазы 2 как часть глобальной сортировки. После того, как все партии Phase1, которые могут включать строки, начинающиеся с A, были обработаны, выполните слияние A фаз2-ведро в конечный вывод и удалите эти временные файлы.

Radix = первые 2 байта (объединение неалфавитных) будет делать для 28 2= 784 ведер. При 200 Мбайт ОЗУ этот средний размер выходного файла составляет всего ~ 256 тыс. Опорожнение всего лишь одного ведра за раз сделало бы это минимум, и вы обычно получали большие партии, чтобы это могло сработать. (Ваш алгоритм выселения может поразить патологический случай, который заставил его хранить много больших ведер, и написать серию небольших партий для новых ведер. Есть опасения для умной эвристики, если вы не испытываете тщательно).

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


Слияние:

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

MergeSort обычно объединяет пары, но при внешней сортировке (то есть диск → диск), гораздо более широкий вход является общим, чтобы избежать чтения и повторной записи вывода много раз. Наличие 25 входных файлов, открытых для объединения в один выходной файл, должно быть прекрасным. Используйте библиотечную реализацию PriorityQueue (обычно реализуемую как кучу), чтобы выбрать следующий элемент ввода из множества отсортированных списков. Возможно, добавьте строки ввода со строкой в ​​качестве приоритета, а число счетчиков и входных файлов - как полезную нагрузку.

Если вы использовали radix для передачи по первому символу в первом проходе, затем объедините все партии A в окончательный выходной файл (даже если этот процесс занимает несколько этапов слияния), то все пакеты b и т.д. Вам не нужно проверять какие-либо партии из пула start-with- A на партии из любого другого ведра, поэтому это экономит много слияния, особенно. если ваши ключи хорошо распределены первым символом.


Сведение к минимуму воздействия на производственный сервер:

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

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

Также проверьте использование системной памяти и уменьшите порог партии, если память на рабочем сервере плотна. (Или, если действительно плотно, промойте частичную партию и спящий режим, пока давление в памяти не уменьшится.)

Если размер temp файла является проблемой, вы можете сделать сжатие с общим префиксом, например, frcode из updatedb/locate, чтобы значительно уменьшить файл размер для отсортированных списков строк. Возможно, используйте сортировку, чувствительную к регистру, в пакетной, но нечувствительной к регистру системе. Поэтому каждая партия в ведре A будет иметь все A s, затем все A s. Или даже LZ4 сжимать/распаковывать их на лету. Используйте hex для подсчета, а не десятичного. Он короче и быстрее кодируется/декодируется.

Используйте разделитель, который не является символом юридического имени файла, например /, между ключом и счетчиком. Анализ строк может занять много времени процессора на этапе слияния, поэтому стоит подумать. Если вы можете оставить строки в буфере ввода для каждого файла и просто указать на них PQueue, это может быть хорошо. (И скажите, с какого входного файла вышла строка, без сохранения этого отдельно.)


настройка производительности:

Если исходные несортированные строки были доступны очень быстро, хэш-таблица с небольшими партиями, которые соответствуют словарю в кэше ЦП L3, может быть победой, если большее окно не может включать в себя гораздо большую долю ключей и найти больше Dups. Это зависит от того, сколько повторов типично для 100 тыс. Файлов. Постройте небольшие сортированные партии в RAM при чтении, а затем объедините их в пакет дисков. Это может быть более эффективным, чем выполнение большой оперативной памяти в оперативной памяти, поскольку у вас нет случайного доступа к вводу до тех пор, пока вы его не прочитали.

Поскольку I/O, вероятно, будет предел, большие партии, которые не вписываются в кеш-память ЦП, вероятно, являются победой, чтобы найти больше дубликатов и (значительно?) уменьшить объем выполняемой слияния.

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


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

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

Ответ 2

Как вы "объединяете файлы групп" в своем подходе? В худшем случае каждая строка имела другой шаблон имен, поэтому в каждом групповом файле было 5000 строк, и каждое слияние удваивает количество строк до переполнения памяти.

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

Ответ 3

Хорошая хорошая проблема.

Учитывая, что вы намерены обрабатывать результаты в партиях 5000, я не считаю, что оптимизация памяти будет иметь особое значение, поэтому мы могли бы, вероятно, игнорировать этот аспект, как плохой фильм Адама Сэндлера и двигаться на более интересные вещи. Кроме того, только потому, что в некоторых вычислениях используется больше ОЗУ, это не обязательно означает плохой алгоритм. Никто не жаловался на справочные таблицы.

Однако, я согласен вычислительно, что подход словарь лучше, потому что он быстрее. Что касается альтернативы, зачем выполнять ненужную сортировку, даже если ее быстро? Последнее с его "O (m log m)" в конечном счете медленнее, чем "O (m)".

Реальная проблема?

С ОЗУ из уравнения проблема состоит, по сути, в вычислении. Любая "проблема производительности" в алгоритме, возможно, будет незначительна до времени, необходимого для прохождения файловой системы в первую очередь.

Возможно, там, где будет реальная проблема. Возможно, проблема в другое время?

EDIT: displayName делает хороший вывод об использовании Hadoop - идеально подходит для одновременных заданий и вычислений

Удачи!

Ответ 4

Ваша проблема - очень хороший кандидат для Map-Reduce. Отличные новости: вам не нужно переходить с С# на Java (Hadoop), поскольку Map-Reduce возможно в .NET framework!

Через LINQs у вас есть основные элементы выполнения, которые уже выполняются для выполнения Map Reduce в С#. Это может быть одним преимуществом перед выбором External Sort, хотя нет никаких сомнений в наблюдении за External Sort. Эта ссылка имеет "Hello World!" Map-Reduce уже реализован в С# с использованием LINQs и должен начать работу.


Если вы перейдете на Java, одно из наиболее подробных руководств по этому вопросу - здесь. Google о Hadoop и Map-Reduce, и вы получите много информации и множество хороших онлайн-видеороликов.

Кроме того, если вы хотите перейти на Java, ваши требования:

  • Отсортированные результаты
  • использование критической памяти

несомненно, будут выполнены, поскольку они являются встроенными выполнениями, которые вы получаете из задания Map-Reduce в Hadoop.