Прочитать большой файл параллельно?

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

Сначала сделайте некоторые поддельные данные

paste <(seq 20000000) <(seq 2 20000001)  > largefile.txt

Теперь вот минимальная часть кода на Python, чтобы прочитать ее и сделать словарь.

import sys
from collections import defaultdict
fin = open(sys.argv[1])

dict = defaultdict(list)

for line in fin:
    parts = line.split()
    dict[parts[0]].append(parts[1])

Тайминги:

time ./read.py largefile.txt
real    0m55.746s

Однако можно прочитать весь файл гораздо быстрее, чем:

time cut -f1 largefile.txt > /dev/null    
real    0m1.702s

Мой процессор имеет 8 ядер, можно ли распараллелить эту программу в python, чтобы ускорить его?

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

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

perl -E 'say int rand 1e7, $", int rand 1e4 for 1 .. 1e7' > largefile.txt

(Связано с Читайте в большом файле и создайте словарь.)

Ответы

Ответ 1

Несколько лет назад на сайте Тима Брея была серия блога "Проект широкомасштабного поиска" [1]. Вы можете найти там решение [2] Fredrik Lundh из ElementTree [3] и PIL [4]. Я знаю, что ссылки на публикации обычно не приветствуются на этом сайте, но я думаю, что эти ссылки дают вам лучший ответ, чем копировать его код.

[1] http://www.tbray.org/ongoing/When/200x/2007/10/30/WF-Results
[2] http://effbot.org/zone/wide-finder.htm
[3] http://docs.python.org/3/library/xml.etree.elementtree.html
[4] http://www.pythonware.com/products/pil/

Ответ 2

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

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

Что вы можете сделать, выполняется относительно дорогостоящая операция string/dictionary/list параллельно с чтением.

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

(Если вы переходите на несколько потребительских потоков, как говорит Pappnese, создайте по одному словарю на поток, а затем присоедините их).


Советов:


Re. Баунти:

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

Использование strtok_r вместо str.split может быть быстрее, но помните, что вам также нужно будет управлять памятью для всех ваших строк вручную. О, и вам нужна логика для управления фрагментами строк. Честно говоря, C дает вам так много вариантов. Думаю, вам просто нужно просмотреть его и посмотреть.

Ответ 3

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

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

paste <(seq 20000000) <(seq 2 20000001) <(seq 3 20000002) |
  head -1000000 > largefile.txt

После профилирования исходного кода я обнаружил, что самая медленная часть процесса - это процедура разделения строк. (.split() занимает примерно 2x больше, чем .append() на моей машине.)

1000000    0.333    0.000    0.333    0.000 {method 'split' of 'str' objects}
1000000    0.154    0.000    0.154    0.000 {method 'append' of 'list' objects}

Итак, я рассмотрел разделение на другую функцию и использовал пул для распределения работы по разбиению полей:

import sys
import collections
import multiprocessing as mp

d = collections.defaultdict(list)

def split(l):
    return l.split()

pool = mp.Pool(processes=4)
for keys in pool.map(split, open(sys.argv[1])):
    d[keys[0]].append(keys[1:])

К сожалению, добавление пула замедлило работу более чем на 2 раза. Первоначальная версия выглядела так:

$ time python process.py smallfile.txt 
real    0m7.170s
user    0m6.884s
sys     0m0.260s

по сравнению с параллельной версией:

$ time python process-mp.py smallfile.txt 
real    0m16.655s
user    0m24.688s
sys     0m1.380s

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

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

Ответ 4

Одна вещь, которую вы могли бы попробовать, - получить счетчик строк из файла, затем создать 8 потоков, которые делают словарь от 1/8 файла каждого, затем присоединяться к словарям, когда все потоки закончены. Это, вероятно, ускорит его, если это добавление, которое требует времени, а не чтения строк.

Ответ 5

Больше кардинального решения для медленного добавления слова: замените словарь на массив пар строк. Заполните его, а затем выполните сортировку.

Ответ 6

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

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

С наилучшими пожеланиями

Барыш ЧУХАДАР.