Думаю, что тема в полной мере описывает вопрос. Мне нужно подсчитать повторения элементов в массиве, который я не могу полностью загрузить в память компьютера. Массив может иметь размер 50 ГБ и даже больше. В моей конкретной задаче элементами этого массива являются строки длиной не более 256 символов в кодировке UTF-8 (всего 512 байт). Количество струн ~ 100 миллионов. Например, если у меня на входе есть следующий массив (строки для краткости сокращены):

VERY NICE ELEMENT_1
VERY NICE ELEMENT_1
VERY NICE ELEMENT_2
VERY NICE ELEMENT_2
NOT SO GOOD ELEMENT
NOT SO GOOD ELEMENT
BAD ELEMENT
BAD ELEMENT
BAD ELEMENT
PERFECT FIFTH ELEMENT

Алгоритм должен вывести следующее (возможно, не совсем в таком порядке):

VERY NICE ELEMENT_1 2
VERY NICE ELEMENT_2 2
NOT SO GOOD ELEMENT 2
BAD ELEMENT 3
PERFECT FIFTH ELEMENT 1

Другими словами, мне нужно делать то, что делает SELECT COUNT(*) GROUP BY.

Я предполагаю, что алгоритм должен выполнять подсчет в несколько этапов, разделяя элементы на некоторые группы, которые обычно помещаются в ОЗУ. Затем следует уменьшить эти группы в одну группу. Но как это сделать? Как можно эффективно объединить эти группы?

Жесткий диск имеет неограниченную емкость. Язык программирования значения не имеет. Мне нужно знать лишь абстрактный алгоритм. У меня был опыт решения аналогичной задачи, когда мне нужно было отсортировать такой массив. Там я проделал то же самое, разделив все элементы на разделы, а затем объединив их в один файл. Но в этом случае я не знаю, как делать «часть слияния».

Заранее спасибо за помощь.

3
Dobby007 7 Сен 2016 в 21:28

4 ответа

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

Т.е.

А 3,45,23 ... б 112,34,546 ...

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

По крайней мере, это была бы моя первая идея.

Очевидно, что это лучшие слова, в основном случайные, и не все начинается с одной и той же буквы, или, в худшем случае, все слова одинаковы.

0
breakline 7 Сен 2016 в 19:17

Я бы просто разделил файл на несколько файлов в соответствии с хеш-кодом каждой строки. Сделайте файл размером 1000x 50 МБ из файла 1x 50 ГБ. Затем обработайте каждый файл отдельно, он без проблем уместится в памяти.

protected static string[] Partition(string inputFileName, string outPath, int partitions)
{
    string[] fileNames = Enumerable.Range(0, partitions)
        .Select(i => Path.Combine(outPath, "part" + i))
        .ToArray();

    StreamWriter[] writers = fileNames
        .Select(fn => new StreamWriter(fn))
        .ToArray();

    StreamReader file = new StreamReader(inputFileName);
    string line;
    while ((line = file.ReadLine()) != null)
    {
        int partition = Math.Abs(line.GetHashCode() % partitions);
        writers[partition].WriteLine(line);
    }
    file.Close();

    writers.AsParallel().ForAll(c => c.Close());

    return fileNames;
}

protected static void CountFile(string inputFileName, StreamWriter writer)
{
    Dictionary<string, int> dict = new Dictionary<string, int>();

    StreamReader file = new StreamReader(inputFileName);
    string line;
    while ((line = file.ReadLine()) != null)
    {
        int count;
        if (dict.TryGetValue(line, out count))
        {
            dict[line] = count + 1;
        }
        else
        {
            dict.Add(line, 1);
        }
    }
    file.Close();

    foreach (var kv in dict)
    {
        writer.WriteLine(kv.Key + ": " + kv.Value);
    }
}

protected static void CountFiles(string[] fileNames, string outFile)
{
    StreamWriter writer = new StreamWriter(outFile);
    foreach (var fileName in fileNames)
    {
        CountFile(fileName, writer);
    }
    writer.Close();
}

static void Main(string[] args)
{
    var fileNames = Partition("./data/random2g.txt", "./data/out", 211);           
    CountFiles(fileNames, "./data/random2g.out");
}

Контрольный показатель

Я решил попробовать сравнить подход к сортировке (Леон) и хеширование. Сортировка - это довольно большая работа, если она Вам не нужна. Я сделал файл с 2 миллиардами номеров. Распределение (long)Math.Exp(rnd.NextDouble() * 30) дает все длины чисел (до 14) с одинаковой вероятностью. Это распределение производит много уникальных значений, но в то же время также и значений, которые повторяются много раз. Даже вероятность персонажей меняется. Для искусственных данных это не так уж и плохо.

File size: 16,8GiB
Number of lines: 2G (=2000000000)
Number of distinct lines: 576M
Line occurences: 1..46M, average: 3,5
Line length: 1..14, average: 7
Used characters: '0', '1',...,'9'
Character frequency: 8,8%..13%, average: 10%
Disc: SSD

Сортировка результатов

10M lines in partition
10M distinct lines in partition
114 partitions
Partition size: 131MiB
Sum of partitions size: 14,6GiB
Partitioning time: 105min
Merging time: 180min
Total time: 285min (=4hod 45min)

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

Результаты хеширования

7M..54M lines in partition, average: 9,5M
2723766..2732318 distinct lines in partition, average: 2,73M
211 partitions
Partition size 73MiB..207MiB, average: 81MiB
Sum of partitions size: 16,8GiB
Partitioning time: 6min
Merging time: 15min
Total time: 21min

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

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

1
Antonín Lejsek 10 Сен 2016 в 22:56

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

1
Cecilio Pardo 7 Сен 2016 в 19:06

Используйте фреймворк hadoop и выполните сокращение карты для ваших входных строк.

0
displayName 20 Сен 2017 в 05:28