Как улучшить чтение строки файла Python C Extensions? - PullRequest
0 голосов
/ 22 мая 2019

Первоначально задавался вопрос на Существуют ли альтернативные и переносимые реализации алгоритма для чтения строк из файла в Windows (Visual Studio Compiler) и Linux? , но закрыт как за границей, поэтому я пытаюсь уменьшить егообласть применения с более кратким использованием кейсов.

Моя цель - реализовать собственный модуль чтения файлов для Python с расширениями Python C с политикой кэширования строк.Чисто реализация алгоритма Python без какой-либо политики кэширования строк:

# This takes 1 second to parse 100MB of log data
with open('myfile', 'r', errors='replace') as myfile:
    for line in myfile:
        if 'word' in line: 
            pass

Возобновление реализации расширений Python C: ( см. Здесь полный код с политикой кэширования строк )

// other code to open the file on the std::ifstream object and create the iterator
...

static PyObject * PyFastFile_iternext(PyFastFile* self, PyObject* args)
{
    std::string newline;

    if( std::getline( self->fileifstream, newline ) ) {
        return PyUnicode_DecodeUTF8( newline.c_str(), newline.size(), "replace" );
    }

    PyErr_SetNone( PyExc_StopIteration );
    return NULL;
}

static PyTypeObject PyFastFileType =
{
    PyVarObject_HEAD_INIT( NULL, 0 )
    "fastfilepackage.FastFile" /* tp_name */
};

// create the module
PyMODINIT_FUNC PyInit_fastfilepackage(void)
{
    PyFastFileType.tp_iternext = (iternextfunc) PyFastFile_iternext;
    Py_INCREF( &PyFastFileType );

    PyObject* thismodule;
    // other module code creating the iterator and context manager
    ...

    PyModule_AddObject( thismodule, "FastFile", (PyObject *) &PyFastFileType );
    return thismodule;
}

И это код Python, который использует код Python C Extensions, чтобы открыть файл и прочитать его строки одну за другой:

from fastfilepackage import FastFile

# This takes 3 seconds to parse 100MB of log data
iterable = fastfilepackage.FastFile( 'myfile' )
for item in iterable:
    if 'word' in iterable():
        pass

Прямо сейчас код Python C Extensions fastfilepackage.FastFile сC ++ 11 std::ifstream занимает 3 секунды для анализа 100 МБ данных журнала, в то время как представленная реализация Python занимает 1 секунду.

Содержимое файла myfile - это всего лишь log lines с примерно 100 ~ 300 символами в каждой строке.Символы - это просто ASCII (модуль% 256), но из-за ошибок в механизме ведения журнала он может содержать недопустимые символы ASCII или Unicode.Следовательно, именно поэтому я использовал политику errors='replace' при открытии файла.

Мне просто интересно, смогу ли я заменить или улучшить эту реализацию расширения Python C, сократив время запуска программы Python на 3 секунды.

Я использовал это, чтобы сделать тест:

import time
import datetime
import fastfilepackage

# usually a file with 100MB
testfile = './myfile.log'

timenow = time.time()
with open( testfile, 'r', errors='replace' ) as myfile:
    for item in myfile:
        if None:
            var = item

python_time = time.time() - timenow
timedifference = datetime.timedelta( seconds=python_time )
print( 'Python   timedifference', timedifference, flush=True )
# prints about 3 seconds

timenow = time.time()
iterable = fastfilepackage.FastFile( testfile )
for item in iterable:
    if None:
        var = iterable()

fastfile_time = time.time() - timenow
timedifference = datetime.timedelta( seconds=fastfile_time )
print( 'FastFile timedifference', timedifference, flush=True )
# prints about 1 second

print( 'fastfile_time %.2f%%, python_time %.2f%%' % ( 
        fastfile_time/python_time, python_time/fastfile_time ), flush=True )

Связанные вопросы:

  1. Чтение файла построчно в C
  2. Улучшение чтения файла C ++ построчно?

Ответы [ 2 ]

1 голос
/ 22 мая 2019

Чтение построчно вызовет здесь неизбежное замедление. Встроенные в Python текстовые объекты, доступные только для чтения, на самом деле представляют собой три слоя:

  1. io.FileIO - необработанный небуферизованный доступ к файлу
  2. io.BufferedReader - Буферы базового FileIO
  3. io.TextIOWrapper - переносит BufferedReader для реализации буферизованного декодирования в str

Хотя iostream выполняет буферизацию, она выполняет только работу io.BufferedReader, а не io.TextIOWrapper. io.TextIOWrapper добавляет дополнительный уровень буферизации, считывая 8 КБ чанков из BufferedReader и декодируя их в массе до str (когда чанк заканчивается неполным символом, он сохраняет оставшиеся байты, предшествующие следующему фрагменту), затем выдающие отдельные строки из декодированного фрагмента по запросу, пока он не будет исчерпан (когда декодированный фрагмент заканчивается частичной строкой, остаток добавляется к следующему декодированному фрагменту).

В отличие от этого, вы потребляете строку за один раз с std::getline, затем декодируете строку за один раз с PyUnicode_DecodeUTF8, затем возвращаетесь к вызывающей стороне; к тому времени, когда вызывающая сторона запрашивает следующую строку, шансы, что по крайней мере часть кода, связанного с вашей реализацией tp_iternext, покинули кэш ЦП (или, по крайней мере, оставили самые быстрые части кеша). Точный цикл декодирования 8 КБ текста в UTF-8 будет работать очень быстро; многократный выход из цикла и только декодирование 100-300 байт за раз будет медленнее.

Решение состоит в том, чтобы сделать примерно то, что делает io.TextIOWrapper: читать фрагменты, а не строки, и декодировать их навалом (сохраняя неполные символы в кодировке UTF-8 для следующего фрагмента), а затем искать новые строки для поиска подстрок из декодированный буфер, пока он не исчерпан (не обрезайте буфер каждый раз, просто отслеживайте индексы). Когда в декодированном буфере не останется больше полных строк, обрежьте материал, который вы уже получили, и прочитайте, декодируйте и добавьте новый фрагмент.

Есть некоторая возможность для улучшения базовой реализации Python io.TextIOWrapper.readline (например, им приходится создавать уровень Python int каждый раз, когда они читают чанк и вызывают косвенно, поскольку не могут гарантировать они обертывают BufferedReader), но это прочная основа для реализации вашей собственной схемы.

Обновление: При проверке полного кода (который сильно отличается от того, что вы опубликовали) у вас возникают другие проблемы. Ваш tp_iternext просто несколько раз возвращает None, требуя от вас вызова вашего объекта для извлечения строки. Это прискорбно. Это больше, чем удвоение накладных расходов интерпретатора Python на элемент (tp_iternext дешево вызывать, будучи довольно специализированным; tp_call не так уж и дешев, проходя через запутанные пути кода общего назначения, требуя, чтобы интерпретатор пропустил пустой tuple из аргументов, которые вы никогда не используете, и т. д .; дополнительное примечание: PyFastFile_tp_call должен принимать третий аргумент для kwds, который вы игнорируете, но все же должны быть приняты; приведение к ternaryfunc приводит к глушению ошибки, но это сломается на некоторых платформах).

Последнее замечание (на самом деле не относится к производительности для всех, кроме самых маленьких файлов): контракт для tp_iternext не требует, чтобы вы устанавливали исключение, когда итератор исчерпан, просто вы return NULL;. Вы можете удалить свой звонок на PyErr_SetNone( PyExc_StopIteration );; до тех пор, пока не установлено другое исключение, только return NULL; указывает на конец итерации, поэтому вы можете сохранить некоторую работу, не устанавливая ее вообще.

0 голосов
/ 24 мая 2019

Эти результаты относятся только к компилятору Linux или Cygwin. Если вы используете Visual Studio Compiler, результаты для std::getline и std::ifstream.getline будут 100% или более медленными, чем встроенный итератор Python for line in file.

Вы увидите, что linecache.push_back( emtpycacheobject ) используется в коде, потому что таким образом я только сравниваю время, используемое для чтения строк, исключая время, которое Python тратит на преобразование входной строки в объект Unicode Python. Поэтому я закомментировал все строки, которые вызывают PyUnicode_DecodeUTF8.

Это глобальные определения, используемые в примерах:

const char* filepath = "./myfile.log";
size_t linecachesize = 131072;

PyObject* emtpycacheobject;
emtpycacheobject = PyUnicode_DecodeUTF8( "", 0, "replace" );

Мне удалось оптимизировать использование Posix C getline (кэшируя общий размер буфера вместо того, чтобы всегда передавать 0), и теперь Posix C getline превосходит встроенную Python for line in file на 5%. Я предполагаю, что если я удаляю весь код Python и C ++ вокруг Posix C getline, это должно повысить производительность:

char* readline = (char*) malloc( linecachesize );
FILE* cfilestream = fopen( filepath, "r" );

if( cfilestream == NULL ) {
    std::cerr << "ERROR: Failed to open the file '" << filepath << "'!" << std::endl;
}

if( readline == NULL ) {
    std::cerr << "ERROR: Failed to alocate internal line buffer!" << std::endl;
}

bool getline() {
    ssize_t charsread;
    if( ( charsread = getline( &readline, &linecachesize, cfilestream ) ) != -1 ) {
        fileobj.getline( readline, linecachesize );
        // PyObject* pythonobject = PyUnicode_DecodeUTF8( readline, charsread, "replace" );
        // linecache.push_back( pythonobject );
        // return true;

        Py_XINCREF( emtpycacheobject );
        linecache.push_back( emtpycacheobject );
        return true;
    }
    return false;
}

if( readline ) {
    free( readline );
    readline = NULL;
}

if( cfilestream != NULL) {
    fclose( cfilestream );
    cfilestream = NULL;
}

Мне также удалось улучшить производительность C ++ до 20% медленнее, чем встроенный Python C for line in file, с помощью std::ifstream.getline():

char* readline = (char*) malloc( linecachesize );
std::ifstream fileobj;
fileobj.open( filepath );

if( fileobj.fail() ) {
    std::cerr << "ERROR: Failed to open the file '" << filepath << "'!" << std::endl;
}

if( readline == NULL ) {
    std::cerr << "ERROR: Failed to alocate internal line buffer!" << std::endl;
}

bool getline() {

    if( !fileobj.eof() ) {
        fileobj.getline( readline, linecachesize );
        // PyObject* pyobj = PyUnicode_DecodeUTF8( readline, fileobj.gcount(), "replace" );
        // linecache.push_back( pyobj );
        // return true;

        Py_XINCREF( emtpycacheobject );
        linecache.push_back( emtpycacheobject );
        return true;
    }
    return false;
}

if( readline ) {
    free( readline );
    readline = NULL;
}

if( fileobj.is_open() ) {
    fileobj.close();
}

Наконец, мне также удалось получить только на 10% меньшую производительность, чем встроенный Python C for line in file с std::getline, кэшировав std::string, который он использует в качестве ввода:

std::string line;
std::ifstream fileobj;
fileobj.open( filepath );

if( fileobj.fail() ) {
    std::cerr << "ERROR: Failed to open the file '" << filepath << "'!" << std::endl;
}

try {
    line.reserve( linecachesize );
}
catch( std::exception error ) {
    std::cerr << "ERROR: Failed to alocate internal line buffer!" << std::endl;
}

bool getline() {

    if( std::getline( fileobj, line ) ) {
        // PyObject* pyobj = PyUnicode_DecodeUTF8( line.c_str(), line.size(), "replace" );
        // linecache.push_back( pyobj );
        // return true;

        Py_XINCREF( emtpycacheobject );
        linecache.push_back( emtpycacheobject );
        return true;
    }
    return false;
}

if( fileobj.is_open() ) {
    fileobj.close();
}

После удаления всех шаблонов из C ++ производительность для Posix C getline была на 10% ниже встроенной в Python for line in file:

const char* filepath = "./myfile.log";
size_t linecachesize = 131072;

PyObject* emtpycacheobject = PyUnicode_DecodeUTF8( "", 0, "replace" );
char* readline = (char*) malloc( linecachesize );
FILE* cfilestream = fopen( filepath, "r" );

static PyObject* PyFastFile_tp_call(PyFastFile* self, PyObject* args, PyObject *kwargs) {
    Py_XINCREF( emtpycacheobject );
    return emtpycacheobject;
}

static PyObject* PyFastFile_iternext(PyFastFile* self, PyObject* args) {
    ssize_t charsread;
    if( ( charsread = getline( &readline, &linecachesize, cfilestream ) ) == -1 ) {
        return NULL;
    }
    Py_XINCREF( emtpycacheobject );
    return emtpycacheobject;
}

static PyObject* PyFastFile_getlines(PyFastFile* self, PyObject* args) {
    Py_XINCREF( emtpycacheobject );
    return emtpycacheobject;
}

static PyObject* PyFastFile_resetlines(PyFastFile* self, PyObject* args) {
    Py_INCREF( Py_None );
    return Py_None;
}

static PyObject* PyFastFile_close(PyFastFile* self, PyObject* args) {
    Py_INCREF( Py_None );
    return Py_None;
}

Значения последнего запуска, в котором Posix C getline уступал Python на 10%:

$ /bin/python3.6 fastfileperformance.py fastfile_time 1.15%, python_time 0.87%
Python   timedifference 0:00:00.695292
FastFile timedifference 0:00:00.796305

$ /bin/python3.6 fastfileperformance.py fastfile_time 1.13%, python_time 0.88%
Python   timedifference 0:00:00.708298
FastFile timedifference 0:00:00.803594

$ /bin/python3.6 fastfileperformance.py fastfile_time 1.14%, python_time 0.88%
Python   timedifference 0:00:00.699614
FastFile timedifference 0:00:00.795259

$ /bin/python3.6 fastfileperformance.py fastfile_time 1.15%, python_time 0.87%
Python   timedifference 0:00:00.699585
FastFile timedifference 0:00:00.802173

$ /bin/python3.6 fastfileperformance.py fastfile_time 1.15%, python_time 0.87%
Python   timedifference 0:00:00.703085
FastFile timedifference 0:00:00.807528

$ /bin/python3.6 fastfileperformance.py fastfile_time 1.17%, python_time 0.85%
Python   timedifference 0:00:00.677507
FastFile timedifference 0:00:00.794591

$ /bin/python3.6 fastfileperformance.py fastfile_time 1.20%, python_time 0.83%
Python   timedifference 0:00:00.670492
FastFile timedifference 0:00:00.804689
...