Подтвердить что ты не робот

Ускорение сопряжения строк в объекты в Python

Я пытаюсь найти эффективный способ объединить строки данных, содержащих целые точки, и сохранить их как объекты Python. Данные состоят из координатных точек X и Y, представленных в виде разделенных запятыми строк. Точки должны быть сопряжены, как в (x_1, y_1), (x_2, y_2), ... и т.д., А затем сохраняться как список объектов, где каждая точка является объектом. Функция ниже get_data генерирует данные этого примера:

def get_data(N=100000, M=10):
    import random
    data = []
    for n in range(N):
        pair = [[str(random.randint(1, 10)) for x in range(M)],
                [str(random.randint(1, 10)) for x in range(M)]]
        row = [",".join(pair[0]),
               ",".join(pair[1])]
        data.append(row)
    return data

Теперь у меня есть синтаксический анализ:

class Point:
    def __init__(self, a, b):
        self.a = a
        self.b = b

def test():
    import time
    data = get_data()
    all_point_sets = []
    time_start = time.time()
    for row in data:
        point_set = []
        first_points, second_points = row
        # Convert points from strings to integers
        first_points = map(int, first_points.split(","))
        second_points = map(int, second_points.split(","))
        paired_points = zip(first_points, second_points)
        curr_points = [Point(p[0], p[1]) \
                       for p in paired_points]
        all_point_sets.append(curr_points)
    time_end = time.time()
    print "total time: ", (time_end - time_start)

В настоящее время это занимает почти 7 секунд для 100 000 пунктов, что кажется очень неэффективным. Часть неэффективности, по-видимому, связана с вычислением first_points, second_points и paired_points - и преобразованием их в объекты.

Другая часть неэффективности - это создание all_point_sets. Вывод строки all_point_sets.append(...), по-видимому, приводит к переходу кода от ~ 7 секунд до 2 секунд!

Как это может ускориться? спасибо.

FOLLOWUP Спасибо за отличные предложения - все они были полезны. но даже со всеми улучшениями, он все еще около 3 секунд для обработки 100 000 записей. Я не уверен, почему в этом случае это не сразу, и есть ли альтернативное представление, которое сделает его мгновенным. Будет ли кодирование этого в Китоне меняться? Может ли кто-нибудь предложить пример? Еще раз спасибо.

4b9b3361

Ответ 1

Простое выполнение с pypy делает большую разницу

$ python pairing_strings.py 
total time:  2.09194397926
$ pypy pairing_strings.py 
total time:  0.764246940613

отключить gc не помогло для pypy

$ pypy pairing_strings.py 
total time:  0.763386964798

namedtuple for Point делает его хуже

$ pypy pairing_strings.py 
total time:  0.888827085495

используя itertools.imap, и itertools.izip

$ pypy pairing_strings.py 
total time:  0.615751981735

Использование memoized версии int и итератора, чтобы избежать zip

$ pypy pairing_strings.py 
total time:  0.423738002777 

Вот код, который я закончил с.

def test():
    import time
    def m_int(s, memo={}):
        if s in memo:
            return memo[s]
        else:
            retval = memo[s] = int(s)
            return retval
    data = get_data()
    all_point_sets = []
    time_start = time.time()
    for xs, ys in data:
        point_set = []
        # Convert points from strings to integers
        y_iter = iter(ys.split(","))
        curr_points = [Point(m_int(i), m_int(next(y_iter))) for i in xs.split(",")]
        all_point_sets.append(curr_points)
    time_end = time.time()
    print "total time: ", (time_end - time_start)

Ответ 2

Когда вы занимаетесь созданием большого количества объектов, часто самое большое повышение производительности, которое вы можете использовать, это отключить сборщик мусора. Каждое "поколение" объектов сборщик мусора пересекает все живые объекты в памяти, ищет объекты, которые являются частью циклов, но не заострены живыми объектами, поэтому они могут быть использованы для восстановления памяти. См. статью Doug Helmann PyMOTW GC для получения некоторой информации (более того, возможно, можно найти с Google и некоторым определением). Сборщик мусора запускается по умолчанию каждые 700 или около того объектов, созданных, но не восстановленных, причем последующие поколения работают несколько реже (я забываю точные детали).

Использование стандартного кортежа вместо класса Point может сэкономить вам некоторое время (с использованием namedtuple находится где-то посередине), а умная распаковка может сэкономить вам некоторое время, но наибольшее усиление может быть достигнуто, выключив gc перед вашим создание множества объектов, которые, как вам известно, не обязательно должны быть gc'd, а затем снова включить его.

Некоторые коды:

def orig_test_gc_off():
    import time
    data = get_data()
    all_point_sets = []
    import gc
    gc.disable()
    time_start = time.time()
    for row in data:
        point_set = []
        first_points, second_points = row
        # Convert points from strings to integers
        first_points = map(int, first_points.split(","))
        second_points = map(int, second_points.split(","))
        paired_points = zip(first_points, second_points)
        curr_points = [Point(p[0], p[1]) \
                       for p in paired_points]
        all_point_sets.append(curr_points)
    time_end = time.time()
    gc.enable()
    print "gc off total time: ", (time_end - time_start)

def test1():
    import time
    import gc
    data = get_data()
    all_point_sets = []
    time_start = time.time()
    gc.disable()
    for index, row in enumerate(data):
        first_points, second_points = row
        curr_points = map(
            Point,
            [int(i) for i in first_points.split(",")],
            [int(i) for i in second_points.split(",")])
        all_point_sets.append(curr_points)
    time_end = time.time()
    gc.enable()
    print "variant 1 total time: ", (time_end - time_start)

def test2():
    import time
    import gc
    data = get_data()
    all_point_sets = []
    gc.disable()
    time_start = time.time()
    for index, row in enumerate(data):
        first_points, second_points = row
        first_points = [int(i) for i in first_points.split(",")]
        second_points = [int(i) for i in second_points.split(",")]
        curr_points = [(x, y) for x, y in zip(first_points, second_points)]
        all_point_sets.append(curr_points)
    time_end = time.time()
    gc.enable()
    print "variant 2 total time: ", (time_end - time_start)

orig_test()
orig_test_gc_off()
test1()
test2()

Некоторые результаты:

>>> %run /tmp/flup.py
total time:  6.90738511086
gc off total time:  4.94075202942
variant 1 total time:  4.41632509232
variant 2 total time:  3.23905301094

Ответ 3

Я бы

  • используйте numpy массивы для этой проблемы (Cython будет вариантом, если это еще не достаточно быстро).
  • хранить точки как вектор не как отдельные экземпляры Point.
  • полагаться на существующие парсеры
  • (если возможно) проанализировать данные один раз и сохранить его в двоичном формате, таком как hdf5, для дальнейших вычислений, который будет самым быстрым вариантом (см. ниже).

У Numpy есть встроенные функции для чтения текстовых файлов, например loadtxt. Если у вас есть данные, хранящиеся в структурированном массиве, вам необязательно преобразовывать его в другой тип данных. Я буду использовать Pandas, который является сборкой библиотеки поверх numpy. Это немного удобнее для обработки и обработки структурированных данных. Pandas имеет свой собственный парсер файлов read_csv.

Чтобы это сделать, я написал данные в файл, как в исходной проблеме (он основан на вашем get_data):

import numpy as np
import pandas as pd

def create_example_file(n=100000, m=20):
    ex1 = pd.DataFrame(np.random.randint(1, 10, size=(10e4, m)),
                       columns=(['x_%d' % x for x in range(10)] +
                                ['y_%d' % y for y in range(10)]))
    ex1.to_csv('example.csv', index=False, header=False)
    return

Это код, который я использовал для чтения данных в pandas.DataFrame:

def with_read_csv(csv_file):
    df = pd.read_csv(csv_file, header=None,
                     names=(['x_%d' % x for x in range(10)] +
                            ['y_%d' % y for y in range(10)]))
    return df

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

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

In [18]: %timeit string_to_object.with_read_csv('example.csv')
1 loops, best of 3: 553 ms per loop

В ветке разработки есть новый C-парсер, который занимает 414 мс в моей системе. Ваш тест занимает 2,9 с в моей системе, но это не совсем сопоставимо, так как данные не считываются из файла, и вы создали экземпляры Point.

Если вы однажды прочитали данные, вы можете сохранить их в файле hdf5:

In [19]: store = pd.HDFStore('example.h5')

In [20]: store['data'] = df

In [21]: store.close()

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

In [1]: store = pd.HDFStore('example.h5')

In [2]: %timeit df = store['data']
100 loops, best of 3: 16.5 ms per loop

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

Использование массивов на основе numpy с большими наборами данных будет иметь преимущества при выполнении дальнейших вычислений. Cython не обязательно будет быстрее, если вы можете использовать векторизованные функции numpy и индексирование, это будет быстрее, если вам действительно нужна итерация (см. также этот ответ),

Ответ 4

Более быстрый метод, используя Numpy (ускорение около 7x):

import numpy as np
txt = ','.join(','.join(row) for row in data)
arr = np.fromstring(txt, dtype=int, sep=',')
return arr.reshape(100000, 2, 10).transpose((0,2,1))

Сравнение производительности:

def load_1(data):
    all_point_sets = []
    gc.disable()
    for xs, ys in data:
        all_point_sets.append(zip(map(int, xs.split(',')), map(int, ys.split(','))))
    gc.enable()
    return all_point_sets

def load_2(data):
    txt = ','.join(','.join(row) for row in data)
    arr = np.fromstring(txt, dtype=int, sep=',')
    return arr.reshape(100000, 2, 10).transpose((0,2,1))

load_1 работает через 1,52 секунды на моей машине; load_2 работает в 0,20 секунды, в 7 раз. Большой оговоркой здесь является то, что она требует, чтобы вы (1) знали длину всего заранее, и (2), что каждая строка содержит то же самое количество точек. Это верно для вывода get_data, но может быть неверным для вашего реального набора данных.

Ответ 5

Я получил 50% -ное улучшение, используя массивы, и объект-держатель, который лениво конструирует объекты Point при доступе. Я также "прорезал" объект Point для повышения эффективности хранения. Однако кортеж, вероятно, будет лучше.

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

from array import array

class Point(object):
    __slots__ = ["a", "b"]
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def __repr__(self):
        return "Point(%d, %d)" % (self.a, self.b)

class Points(object):
    def __init__(self, xs, ys):
        self.xs = xs
        self.ys = ys

    def __getitem__(self, i):
        return Point(self.xs[i], self.ys[i])

def test3():
    xs = array("i")
    ys = array("i")
    time_start = time.time()
    for row in data:
        xs.extend([int(val) for val in row[0].split(",")])
        ys.extend([int(val) for val in row[1].split(",")])
    print ("total time: ", (time.time() - time_start))
    return Points(xs, ys)

Но при работе с большими объемами данных я обычно использовал numpy N мерные массивы (ndarray). Если исходная структура данных может быть изменена, это, скорее всего, будет самым быстрым из всех. Если бы он мог быть структурирован для линейного чтения x, y пар, а затем изменить ndarray.

Ответ 6

  • make Point a namedtuple (~ 10% ускорение):

    from collections import namedtuple
    Point = namedtuple('Point', 'a b')
    
  • распаковывать во время итерации (~ 2-4% ускорения):

    for xs, ys in data:
    
  • используйте n -аргументную форму map, чтобы избежать zip (~ 10% ускорения):

    curr_points = map(Point,
        map(int, xs.split(',')),
        map(int, ys.split(',')),
    )
    

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

Ответ 7

cython способен ускорить процесс в 5,5 раза

$ python split.py
total time:  2.16252303123
total time:  0.393486022949

Вот код, который я использовал

split.py

import time
import pyximport; pyximport.install()
from split_ import test_


def get_data(N=100000, M=10):
    import random
    data = []
    for n in range(N):
        pair = [[str(random.randint(1, 100)) for x in range(M)],
                [str(random.randint(1, 100)) for x in range(M)]]
        row = [",".join(pair[0]),
               ",".join(pair[1])]
        data.append(row)
    return data

class Point:
    def __init__(self, a, b):
        self.a = a
        self.b = b

def test(data):
    all_point_sets = []
    for row in data:
        point_set = []
        first_points, second_points = row
        # Convert points from strings to integers
        first_points = map(int, first_points.split(","))
        second_points = map(int, second_points.split(","))
        paired_points = zip(first_points, second_points)
        curr_points = [Point(p[0], p[1]) \
                       for p in paired_points]
        all_point_sets.append(curr_points)
    return all_point_sets

data = get_data()
for func in test, test_:
    time_start = time.time()
    res = func(data)
    time_end = time.time()
    print "total time: ", (time_end - time_start)

split_.pyx

from libc.string cimport strsep
from libc.stdlib cimport atoi

cdef class Point:
    cdef public int a,b

    def __cinit__(self, a, b):
        self.a = a
        self.b = b

def test_(data):
    cdef char *xc, *yc, *xt, *yt
    cdef char **xcp, **ycp
    all_point_sets = []
    for xs, ys in data:
        xc = xs
        xcp = &xc
        yc = ys
        ycp = &yc
        point_set = []
        while True:
            xt = strsep(xcp, ',')
            if xt is NULL:
                break
            yt = strsep(ycp, ",")
            point_set.append(Point(atoi(xt), atoi(yt)))
        all_point_sets.append(point_set)
    return all_point_sets

poking вокруг дальше я могу приблизительно сломать некоторые из ресурсов процессора

         5% strsep()
         9% atoi()
        23% creating Point instances
        35% all_point_sets.append(point_set)

Я бы ожидал, что может быть улучшение, если бы cython смог прочитать файл csv (или любой другой) напрямую, вместо того, чтобы траться через объект Python.

Ответ 8

Вы можете сбрить несколько секунд:

class Point2(object):
    __slots__ = ['a','b']
    def __init__(self, a, b):
        self.a = a
        self.b = b

def test_new(data):
    all_point_sets = []
    for row in data:
        first_points, second_points = row
        r0 = map(int, first_points.split(","))
        r1 = map(int, second_points.split(","))
        cp = map(Point2, r0, r1)
        all_point_sets.append(cp)

который дал мне

In [24]: %timeit test(d)
1 loops, best of 3: 5.07 s per loop

In [25]: %timeit test_new(d)
1 loops, best of 3: 3.29 s per loop

Я с перерывами мог сбрить еще 0,3 с, предварительно выделив пространство в all_point_sets, но это может быть просто шум. И, конечно же, старомодный способ сделать вещи быстрее:

localhost-2:coding $ pypy pointexam.py
1.58351397514

Ответ 9

Данные представляют собой разделенный вкладкой файл, который состоит из списков запятой разделенные целые числа.

Используя образец get_data(), я сделал файл .csv следующим образом:

1,6,2,8,2,3,5,9,6,6     10,4,10,5,7,9,6,1,9,5
6,2,2,5,2,2,1,7,7,9     7,6,7,1,3,7,6,2,10,5
8,8,9,2,6,10,10,7,8,9   4,2,10,3,4,4,1,2,2,9
...

Затем я злоупотреблял C-оптимизированным анализом через JSON:

def test2():
    import json
    import time
    time_start = time.time()
    with open('data.csv', 'rb') as f:
        data = f.read()
    data = '[[[' + ']],[['.join(data.splitlines()).replace('\t', '],[') + ']]]'
    all_point_sets = [Point(*xy) for row in json.loads(data) for xy in zip(*row)]
    time_end = time.time()
    print "total time: ", (time_end - time_start)

Результаты моего бокса: ваш оригинальный test() ~ 8s, с отключенным gc ~ 6s, тогда как моя версия (включая I/O) дает ~ 6s и ~ 4s соответственно. То есть примерно на 50% ускоряется. Но, глядя на данные профилировщика, очевидно, что наибольшее узкое место в самой реализации объекта, поэтому ответ Matt Anderson обеспечит вам наибольшее усиление на CPython.

Ответ 10

Как вы привязаны к тому, чтобы ваши координаты были доступны как атрибуты .x и .y? К моему удивлению, мои тесты показывают, что самый большой одноразовый приемник - это не призывы к list.append(), а построение объектов Point. Они занимают в четыре раза больше, чем кортеж, и их много. Просто заменив Point(int(x), int(y)) кортежем (int(x), int(y)) в вашем коде, выбритом на 50% от общего времени выполнения (Python 2.6 на Win XP). Возможно, у вашего текущего кода все еще есть возможность оптимизировать это?

Если вы действительно настроены на доступ к координатам с помощью .x и .y, вы можете попробовать использовать collections.namedtuple. Это не так быстро, как простые кортежи, но, кажется, намного быстрее, чем класс Pair в вашем коде (я хеджирую, потому что отдельный контрольный момент времени дал мне странные результаты).

Pair = namedtuple("Pair", "x y")  # instead of the Point class
...
curr_points = [ Pair(x, y) for x, y in paired_points ]

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

PS Я вижу, что @MattAnderson давно упомянул проблему с объектным кортежем. Но это важный эффект (по крайней мере, на моем ящике), даже до отключения сбора мусора.

               Original code: total time:  15.79
      tuple instead of Point: total time:  7.328
 namedtuple instead of Point: total time:  9.140

Ответ 11

Я не знаю, можно ли многое сделать.

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

first_points  = (int(p) for p in first_points .split(","))
second_points = (int(p) for p in second_points.split(","))
paired_points = itertools.izip(first_points, second_points)
curr_points   = [Point(x, y) for x,y in paired_points]

Даже сведение всего цикла в одно массовое понимание списка не делает много.

all_point_sets = [
    [Point(int(x), int(y)) for x, y in itertools.izip(xs.split(','), ys.split(','))]
    for xs, ys in data
]

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

all_point_sets = (
    [Point(int(x), int(y)) for x, y in itertools.izip(xs.split(','), ys.split(','))]
    for xs, ys in data
)

Ответ 12

Поскольку время для встроенных функций, таких как zip(a,b) или map(int, string.split(",")) для массивов длиной 2000000, является незначительным, я должен предположить, что самая трудоемкая операция добавляется.

Таким образом, правильный способ решения проблемы состоит в том, чтобы рекурсивно объединить строки:
10 строк из 10 элементов в большую строку
10 строк из 100 элементов
10 строк из 1000 элементов

и, наконец, до zip(map(int,huge_string_a.split(",")),map(int,huge_string_b.split(",")));

Затем просто тонкая настройка, чтобы найти оптимальную базу N для метода append и conquer.

Ответ 13

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

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