Python read() из stdout намного медленнее, чем чтение строки за строкой (slurping?)

У меня есть вызов подпроцесса python, который запускает исполняемый файл и выводит вывод на мой основной файл subprocess.

В случаях, когда данные stdout относительно малы (~ 2k строк), производительность между чтением строки за строкой и чтением как фрагмента (stdout.read()) сопоставима... с stdout.read() немного быстрее.

Как только данные станут больше (скажем, 30k + строк), производительность для чтения по строкам значительно лучше.

Это мое сравнение script:

proc=subprocess.Popen(executable,stdout=subprocess.PIPE)
tic=time.clock()
for line in (iter(proc.stdout.readline,b'')):
    tmp.append(line)
print("line by line = %.2f"%(time.clock()-tic))

proc=subprocess.Popen(executable,stdout=subprocess.PIPE)
tic=time.clock()
fullFile=proc.stdout.read()
print("slurped = %.2f"%(time.clock()-tic))

И это результаты для чтения ~ 96k строк (или 50 Мб на диске):

line by line = 5.48
slurped = 153.03

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

Может ли кто-нибудь дать мне представление о затратах на чтение()?

Ответ 1

Это не просто Python, чтение символами без буферизации всегда медленнее, чем строки для чтения или большие куски.

Рассмотрим эти две простые программы на C:

[readchars.c]

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>

int main(void) {
        FILE* fh = fopen("largefile.txt", "r");
        if (fh == NULL) {
                perror("Failed to open file largefile.txt");
                exit(1);
        }

        int c;
        c = fgetc(fh);
        while (c != EOF) {
                c = fgetc(fh);
        }

        return 0;
}

[readlines.c]

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>

int main(void) {
        FILE* fh = fopen("largefile.txt", "r");
        if (fh == NULL) {
                perror("Failed to open file largefile.txt");
                exit(1);
        }

        char* s = (char*) malloc(120);
        s = fgets(s, 120, fh);
        while ((s != NULL) && !feof(fh)) {
                s = fgets(s, 120, fh);
        }

        free(s);

        return 0;
}

И их результаты (YMMW, largefile.txt был ~ 200 Мбайт текстового файла):

$ gcc readchars.c -o readchars
$ time ./readchars            
./readchars  1.32s user 0.03s system 99% cpu 1.350 total
$ gcc readlines.c -o readlines
$ time ./readlines            
./readlines  0.27s user 0.03s system 99% cpu 0.300 total

Ответ 2

Попробуйте добавить параметр bufsize к вашему вызову Popen и посмотреть, не имеет ли он значения:

proc=subprocess.Popen(executable, bufsize=-1, stdout=subprocess.PIPE)

В Popen есть опция для установки размера буфера для чтения ввода. bufsize по умолчанию равно 0, что означает небуферизованный ввод. Любое другое значение означает буфер примерно такого размера. Отрицательное значение означает использование системы по умолчанию, что означает полностью буферизованный ввод.

Документы Python включают эту заметку:

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

Ответ 3

Я вообще не понимаю этого поведения.

import subprocess
import time


executable = ["cat", "data"]

proc=subprocess.Popen(executable,stdout=subprocess.PIPE)
tic=time.clock()
tmp = []
for line in (iter(proc.stdout.readline,b'')):
    tmp.append(line)
print("line by line = %.2f"%(time.clock()-tic))

proc=subprocess.Popen(executable,stdout=subprocess.PIPE)
tic=time.clock()
fullFile=proc.stdout.read()
print("slurped = %.2f"%(time.clock()-tic))

Данные - это текст.

pts/0$ ll data
-rw-r--r-- 1 javier users 18M feb 21 20:53 data

pts/0$ wc -l data
169866 data

Результат:

pts/0$ python3 a.py 
line by line = 0.08
slurped = 0.01

Python 2 намного медленнее, чем Python 3!

pts/0$ python2 a.py 
line by line = 4.45
slurped = 0.01

Возможно, это зависит от подпроцесса?

Ответ 4

У меня были пятнистые результаты с bufsize, я запускаю непрерывный ping script, который регистрирует ответы, и мне нужно, чтобы он выполнялся без остановок, это будет зависать каждые несколько дней, и моим решением было написать отдельный script, чтобы просмотреть список задач и убить любую задачу ping, которая занимает более 10 секунд. См. Ниже

import subprocess
import time

CREATE_NO_WINDOW = 0x08000000
previous_id = ''

while 0!=1:
    command = subprocess.Popen(['tasklist'], stdout=subprocess.PIPE, 
              shell=False, creationflags = CREATE_NO_WINDOW)
    reply = str(command.communicate()[0]).split('Ko')
    for item in reply:
        if 'PING.EXE' in item:
            print(item.split(' ')[0][4:]+' '+item.split(' ')[22])
        if item.split(' ')[22] != previous_id:
            previous_id = item.split(' ')[22]
            print('New ping detected, system is healthy')
        else:
            print('Same ping active for 10 seconds, killing')
            command = subprocess.Popen(['taskkill','/f','/im','PING.EXE'], stdout=subprocess.PIPE, shell=False, creationflags = CREATE_NO_WINDOW)
            err_log=open('errors.txt','w')
    time.sleep(10)

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