2014-10-01 3 views
0

Отмеченный это как ответ начал более простую тему вокруг, где вопрос скорости действительно кажутсяPython многопроцессорной обработки текста проблема производительности экстракция против Perl эквивалент

Python slow read performance issue

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

У меня есть около 40M XML-файлов, распространяемых (не равномерно) через ок. 60K подкаталоги, структура основана на 10 значное число разделить так:

12/34/56/78/90/files.xml

У меня есть сценарий PERL, который работает против файлов тянущих значение одно поле и распечатает значение и имя файла. Скрипт Perl обернут в сценарий bash, который запускает максимум 12 параллельных экземпляров в списке всех каталогов на глубине 2, а затем идет по каждому из них и обрабатывает файлы на нижнем уровне по мере их обнаружения.

Принимая кэширование диска из нескольких прогонов время Unix процесса возвращается приблизительно:

real 37m47.993s 
user 49m50.143s 
sys  54m57.570s 

Я хотел перенести это питон скрипт (в качестве учебного упражнения и теста), поэтому создается следующее (после много читал на методы питона для различных вещей):

import glob, os, re 
    from multiprocessing import Pool 

    regex = re.compile(r'<field name="FIELDNAME">([^<]+)<', re.S) 

    def extractField(root, dataFile): 
      line = '' 
      filesGlob = root + '/*.xml' 
      global regex 
      for file in glob.glob(filesGlob): 
        with open(file) as x: 
          f = x.read() 
        match = regex.search(f) 
        line += file + '\t' + match.group(1) + '\n' 

      dataFile.write(line) 

    def processDir(top): 
      topName = top.replace("/", "") 
      dataFile = open('data/' + topName + '.data', 'w') 
      extractField(top, dataFile) 
      dataFile.close() 

    filesDepth5 = glob.glob('??/??/??/??/??') 
    dirsDepth5 = filter(lambda f: os.path.isdir(f), filesDepth5) 
    processPool = Pool(12) 
    processPool.map(processDir, dirsDepth5) 
    processPool.close() 
    processPool.join() 

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

real 131m48.731s 
user 35m37.102s 
sys  48m11.797s 

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

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

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

Оцените любые указатели и предоставит больше информации по мере необходимости.

Благодаря

Si

Для справки скрипт Perl находится ниже:

use File::Find; 

my $cwd = `pwd`; 
chomp $cwd; 
find(\&hasxml, shift); 

sub hasxml { 
    if (-d) { 
     my @files = <$_/*.xml>; 
     if (scalar(@files) > 0) { 
      process("$cwd/${File::Find::dir}/$_"); 
     } 
    } 
} 

sub process { 
    my $dir = shift; 

    my @files = <$dir/*.xml>; 

    foreach my $file (@files) { 
     my $fh; 
     open($fh, "< $file") or die "Could not read file <$file>"; 
     my $contents = do { local $/; <$fh> }; 
     close($fh); 
     my ($id) = $contents =~ /<field name="FIELDNAME">([^<]+)<\/field>/s; 
     print "$file\t<$id>\n"; 
    } 
} 
+0

может быть, это поможет вам: http://stackoverflow.com/questions/14863224/efficient-reading-of-800-gb-xml-file-in-python-2-7 Я думаю, вы должны профилировать свое приложение, чтобы узнать, где он проводит больше всего времени, в противном случае это немного угадывает ... –

+0

Я посмотрю спасибо, а должен добавить файлы XML, которые я читаю, не более 4K каждый в но я попробую буферизацию, упомянутую для просмотра. Я пробовал профилирование, но когда я запускаю - python -m cProfile script.py это barfs, я подозреваю, что он не знает, как обрабатывать элемент многопроцессорности, но, признаюсь, я не смотрел на ошибку подробно. – Simon

+0

user/sys ниже (подсказка python выполняется быстрее), но реальная намного выше (намекая, что мы не получаем распараллеливание) ... Протокол parent/child имеет большое значение. Попробуйте 'processPool.map (processDir, dirsDepth5, chunksize = 16)', который отправляет задания более крупными партиями, чтобы узнать, имеет ли это значение. – tdelaney

ответ

1

EDIT

Забыл добавить благодарность авторов на эту тему:

Python slow read performance issue

, который помог мне решить эту проблему.

EDIT

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

По умолчанию Perl сортирует лексикографически (т.е. 1,11,2,22) по умолчанию, Python сортирует по порядку каталога (ls -U), и файлы создаются в естественном порядке (1,2,3,4), поэтому Я взял оригинальный Python чавкать и создал slurpNatural после некоторого поиска Stackoverflow для простого естественного вида:

import glob, sys, re 

def natural_sort_key(s, _nsre=re.compile('([0-9]+)')): 
    return [int(text) if text.isdigit() else text.lower() 
      for text in re.split(_nsre, s)] 

for file in sorted(glob.iglob(sys.argv[1] + '/*.xml'), key=natural_sort_key): 
    with open(file) as x: 
     f = x.read() 

Я тогда бежал все 3 против 50К документы и получил:

$ sync; sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches' 
$ /usr/bin/time perl slurp.pl 1 
1.21user 2.17system 0:12.70elapsed 26%CPU (0avgtext+0avgdata 9140maxresident)k 
1234192inputs+0outputs (22major+2466minor)pagefaults 0swaps 

$ sync; sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches' 
$ /usr/bin/time python slurp.py 1 
2.88user 6.13system 4:48.00elapsed 3%CPU (0avgtext+0avgdata 8020maxresident)k 
1236304inputs+0outputs (35major+52252minor)pagefaults 0swaps 

$ sync; sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches' 
$ /usr/bin/time python slurpNatural.py 1 
1.25user 2.82system 0:10.70elapsed 38%CPU (0avgtext+0avgdata 22408maxresident)k 
1237360inputs+0outputs (35major+56531minor)pagefaults 0swaps 

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

Спасибо за всю помощь, я честно не думал, что порядок чтения файлов будет иметь такое большое значение!

Si

+0

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

4

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

Вот сравнение двух методов:

У меня есть текстовый файл с именем «tmp_large.txt», который имеет 1000000 строк в ней. Каждая строка имеет алфавит в нижнем регистре. Всего за одну линию примерно на полпути через файл, я заменил букву «м» с «х», и я ищу для этой строки:

import re 
import mmap 

from timeit import timeit 
from datetime import timedelta 

c_regex = re.compile('defghijklxnopqrstuvwx') 

def read_file(): 
    with open('tmp_large.txt', 'r') as fi: 
     f = fi.read() 
     match = c_regex.search(f) 

def mmap_file(): 
    with open('tmp_large.txt', 'r+b') as fi: # must open as binary for mmap 
     mm = mmap.mmap(fi.fileno(), 0) 
     match = c_regex.search(mm) 
     mm.close() 

t1 = timedelta(seconds=timeit(read_file, setup='gc.enable()', number=1)) 
t2 = timedelta(seconds=timeit(mmap_file, setup='gc.enable()', number=1)) 

print(t1) 
print(t2) 

Этот сценарий производит этот выход:

0: 00: 00,036021
0: 00: 00,028974

Мы видим экономию времени выполнения чуть меньше, чем на треть. Однако, если поместить строку я ищу в верхней входного файла, мы видим, этот результат:

0: 00: 00,009327
0: 00: 00,000338

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

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

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

Как было предложено в комментариях, использование итератора iglob и замена карты на что-то вроде apply_async может помочь ускорить работу слишком, потому что они оба помогают уменьшить объем памяти:

processPool = Pool(12) 

for dir_or_file in glob.iglob('??/??/??/??/??'): 
    if os.path.isdir(dir_or_file): 
     processPool.apply_async(processDir, (dir_or_file,)) 

processPool.close() 
processPool.join() 

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

Несколько другой код заметки для вас:

  1. Вам не нужно флаг re.S на своем регулярном выражении, так как вы на самом деле не имеют какой-либо «» в шаблоне регулярного выражения.
  2. Если у вас нет какой-то веской причины, вы должны открыть свой выходной файл с помощью конструкции with open() так же, как вы открываете свои входные файлы, чтобы предотвратить появление открытых файловых дескрипторов, если у вас есть исключение.
  3. Когда вы вычисляете dataFile и filesGlob, рассмотрите возможность использования os.path.join() вместо добавления вручную в разделители путей. В долгосрочной перспективе он будет менее подвержен ошибкам.
  4. Вам не нужна ваша линия global regex. Вы всегда можете читать и вызывать методы глобальных объектов без него, как и в моем примере. Вам это нужно только тогда, когда вы собираетесь изменить глобальный.
  5. На всякий случай, вы не знаете об этом, mutliprocessing pools будет по умолчанию запускать столько рабочих, сколько у вас есть ядра ЦП. Если вы уже это знали, пожалуйста, проигнорируйте этот комментарий. Указание 12 процессов для вашего пула просто показалось мне немного странным.
+0

Это отличная информация, спасибо, я сделал то, что вы упомянули, и несколько улучшилось, но, похоже, скорость чтения на диске по-прежнему остается проблемой, даже несмотря на то, что версия Perl значительно превосходит Python, на множестве около 250 тыс. файлов всего около 1 ГБ на python заняло 35 и Perl 17. Я начал новый вопрос о сокращении https://stackoverflow.com/questions/26178038/python-slow-read-performance-issue. Спасибо за ваши предложения, которые мне многому научили :-) – Simon

+0

@Simon: вы можете использовать 'iglob()' с 'для результата в pool.imap_unordered (process_dir, (d для d в iglob (...) if isdir (d)), chunksize = 1000): ... ', чтобы избежать очередей всех файлов одновременно. Убедитесь, что вы улавливаете и регистрируете (или возвращаете) все исключения в 'process_dir()'. – jfs

+0

@Simon: насколько я знаю 'mmap()' не повышает производительность в Linux, но может быть и в Windows. Но 'mmap()' удобно, если вам нужно обработать потенциально большой файл в виде байта, как показано в @SLawson с регулярным выражением. Если это xml-файл, вы можете использовать 'etree.iterparse()' для [избегать использования регулярных выражений для разбора xml/html] (http://stackoverflow.com/a/1732454/4279) :) – jfs

Смежные вопросы