C / Python производительность ввода-вывода для большого количества маленьких файлов - PullRequest
0 голосов
/ 07 мая 2018

С точки зрения ввода-вывода, я ожидаю, что Python и C будут иметь одинаковую производительность, но я вижу, что C в 1,5-2 раза быстрее, чем Python для аналогичной реализации.

Задача проста: объединить тысячи ~ 250 байтов текстовых файлов, каждый из которых содержит две строки:

Header1 \t Header2 \t ... HeaderN
float1  \t float2  \t ... floatN

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

Header1 \t Header2 \t ... HeaderN
float1  \t float2  \t ... floatN
float1  \t float2  \t ... floatN
float1  \t float2  \t ... floatN
... thousands of lines
float1  \t float2  \t ... floatN

Вот моя реализация в C:

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h> 
#include <time.h>

#define LINE_SIZE 300
#define BUFFER_SZ 5000*LINE_SIZE

void combine(char *fname) {
    DIR *d;
    FILE * fp;
    char line[LINE_SIZE];
    char buffer[BUFFER_SZ];
    short flagHeader = 1;
    buffer[0] = '\0';  // need to init buffer befroe strcat to it

    struct dirent *dir;
    chdir("runs");
    d = opendir(".");
    if (d) {
        while ((dir = readdir(d)) != NULL) {
            if ((strstr(dir->d_name, "Hs")) && (strstr(dir->d_name, ".txt")) ) {
                fp = fopen (dir->d_name, "r");
                fgets(line, LINE_SIZE, fp);  // read first line
                if (flagHeader) {  // append it to buffer only once
                    strcat(buffer, line);
                    flagHeader = 0;
                }
                fgets(line, LINE_SIZE, fp);  // read second line
                strcat(buffer, line);
                fclose(fp);
            }
        }
        closedir(d);
        chdir("..");
        fp = fopen(fname, "w");
        fprintf(fp, buffer);
        fclose(fp);
    }
}

int main() {

    clock_t tc;
    int msec;

    tc = clock(); 
    combine("results_c.txt");
    msec = (clock() - tc) * 1000 / CLOCKS_PER_SEC;
    printf("elapsed time: %d.%ds\n", msec/1000, msec%1000);
    return 0;
}

А в Python:

import glob
from time import time


def combine(wildcard, fname='results.txt'):
    """Concatenates all files matching a name pattern into one file.
    Assumes that the files have 2 lines, the first one being the header.
    """
    files = glob.glob(wildcard)
    buffer = ''
    flagHeader = True
    for file in files:
        with open(file, 'r') as pf:
            lines = pf.readlines()
        if not len(lines) == 2:
            print('Error reading file %s. Skipping.' % file)
            continue
        if flagHeader:
            buffer += lines[0]
            flagHeader = False
        buffer += lines[1]

    with open(fname, 'w') as pf:
        pf.write(buffer)


if __name__ == '__main__':
    et = time()
    combine('runs\\Hs*.txt')
    et = time() - et
    print("elapsed time: %.3fs" % et)

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

Run 1/10
C      elapsed time: 9.530s
Python elapsed time: 10.225s
===================
Run 2/10
C      elapsed time: 5.378s
Python elapsed time: 10.613s
===================
Run 3/10
C      elapsed time: 6.534s
Python elapsed time: 13.971s
===================
Run 4/10
C      elapsed time: 5.927s
Python elapsed time: 14.181s
===================
Run 5/10
C      elapsed time: 5.981s
Python elapsed time: 9.662s
===================
Run 6/10
C      elapsed time: 4.658s
Python elapsed time: 9.757s
===================
Run 7/10
C      elapsed time: 10.323s
Python elapsed time: 19.032s
===================
Run 8/10
C      elapsed time: 8.236s
Python elapsed time: 18.800s
===================
Run 9/10
C      elapsed time: 7.580s
Python elapsed time: 15.730s
===================
Run 10/10
C      elapsed time: 9.465s
Python elapsed time: 20.532s
===================

Кроме того, запуск профиля реализации Python действительно говорит о том, что 70% времени тратится на io.open, а остальное на readlines.

In [2]: prun bc.combine('runs\\Hs*.txt')
         64850 function calls (64847 primitive calls) in 12.205 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1899    8.391    0.004    8.417    0.004 {built-in method io.open}
     1898    3.322    0.002    3.341    0.002 {method 'readlines' of '_io._IOBase' objects}
        1    0.255    0.255    0.255    0.255 {built-in method nt.listdir}

Даже если readlines намного медленнее, чем fgets, время, потраченное Python только с io.open, больше, чем общее время выполнения в C. А также, в конечном итоге, readlines и fgets читать файл построчно, так что я ожидаю более сопоставимой производительности.

Итак, на мой вопрос: в данном конкретном случае, почему python намного медленнее, чем C для ввода / вывода?

1 Ответ

0 голосов
/ 07 мая 2018

Это сводится к нескольким вещам:

  1. Самое главное, версия Python использует текстовый режим (т.е. r и w), который подразумевает обработку str (UTF-8) объектов вместо bytes.

  2. Есть много маленьких файлов, и мы мало с ними работаем - собственные издержки Python (например, настройка файловых объектов в open) становятся важными.

  3. Python должен динамически распределять память для большинства вещей.

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

Теперь, если вы позаботитесь о текстовом режиме (то есть, используя rb и wb), а также сократите ассигнования (менее важные в этом случае, но также заметные), вы получите что-то вроде этого:

def combine():
    flagHeader = True
    with open('results-python-new.txt', 'wb') as fout:
        for filename in glob.glob('runs/Hs*.txt'):
            with open(filename, 'rb') as fin:
                header = fin.readline()
                values = fin.readline()
                if flagHeader:
                    flagHeader = False
                    fout.write(header)
                fout.write(values)

Тогда Python уже завершает задачи в два раза - фактически быстрее, чем версия C:

Old C:      0.234
Old Python: 0.389
New Python: 0.213

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

Однако, если вы также примените пару похожих модификаций к версии C, тогда вы получите намного лучшее время - треть времени Python:

New C:      0.068

Взгляните:

#define LINE_SIZE 300

void combine(void) {
    DIR *d;
    FILE *fin;
    FILE *fout;
    struct dirent *dir;
    char headers[LINE_SIZE];
    char values[LINE_SIZE];
    short flagHeader = 1;

    fout = fopen("results-c-new.txt", "wb");
    chdir("runs");
    d = opendir(".");
    if (d) {
        while ((dir = readdir(d)) != NULL) {
            if ((strstr(dir->d_name, "Hs")) && (strstr(dir->d_name, ".txt")) ) {
                fin = fopen(dir->d_name, "rb");
                fgets(headers, LINE_SIZE, fin);
                fgets(values, LINE_SIZE, fin);
                if (flagHeader) {
                    flagHeader = 0;
                    fputs(headers, fout);
                }
                fputs(values, fout);
                fclose(fin);
            }
        }
        closedir(d);
        fclose(fout);
    }
}
...