Улучшение времени выполнения кода Python с помощью индексации и конкатенации строк - PullRequest
0 голосов
/ 15 ноября 2018

Я очень старался улучшить время выполнения следующего фрагмента кода, который оказался узким местом ЦП в разрабатываемом пакете asyncio-client:

data = [''] * n
for i, ix in enumerate(indices):
    data[ix] = elements[i]
s = '\t'.join(data)
return s

То, что я делаю, в основном очень просто:

  • elements - это список str (каждый <= 7 символов), который я в конечном итоге записываю на определенных позициях в файл, разделенный табуляцией. </li>
  • indices представляет собой список int с указанием позиций каждого из elements в файле
  • Если в определенной позиции нет элемента, вставляется пустая строка

Наконец, я записываю строку в текстовый файл, используя aiofiles.

До сих пор я пытался использовать генератор для создания данных на лету, а также использовать numpy для более быстрой индексации, но безуспешно. Любая идея, как заставить этот код работать быстрее, была бы великолепна. Вот воспроизводимый пример с синхронизацией:

import numpy as np
import timeit

n = 1_000_000  # total number of items
k = 500_000  # number of elements to insert
elements = ['ade/gua'] * k  # elements to insert, <= 7 unicode characters
indices = list(range(0, n, 2))  # indices where to insert, sorted
assert len(elements) == len(indices)

# This is where I started
def baseline():
    data = [''] * n
    for i, ix in enumerate(indices):
        data[ix] = elements[i]
    s = '\t'.join(data)
    return s

# Generate values on the fly
def generator():
    def f():
        it = iter(enumerate(indices))
        i, ix = next(it)
        for j in range(n):
            if j == ix:
                yield elements[i]
                try:
                    i, ix = next(it)
                except:
                    pass
            else:
                yield ''
    s = '\t'.join(f())  # iterating though generation seem too costly
    return s

# Harness numpy
indices_np = np.array(indices)  # indices could also be numpy array
def numpy():
    data = np.full(n, '', dtype='<U7')
    data[indices_np] = elements  # this is faster with numpy
    s = '\t'.join(data)  # much slower. array2string or savetxt does not help
    return s

assert baseline() == generator() == numpy()

timeit.timeit(baseline, number=10)  # 0.8463204780127853
timeit.timeit(generator, number=10)  # 2.048296730965376 -> great job
timeit.timeit(numpy, number=10)  # 4.486689139157534 -> life sucks

Редактировать 1

Чтобы ответить на некоторые вопросы, поднятые в комментариях:

  • Я пишу строку aiofiles.open(filename, mode='w') as file и file.write()

  • Индексы, как правило, не могут быть выражены в виде диапазона

  • Предполагается, что индексы всегда сортируются без дополнительной оплаты.

  • Достаточно символов ASCII

Редактировать 2

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

def buffer_plumbing():
m = len(elements) # total number of data points to insert
k = 7  # each element is 7 bytes long, only ascii 
total_bytes = n - 1 + m * 7  # total number of bytes for the buffer

# find out the number of preceeding gaps for each element
gap = np.empty_like(indices_np)
gap[0] = indices_np[0]  # that many gaps a the beginning
np.subtract(indices_np[1:], indices_np[:-1], out=gap[1:])
gap[1:] -= 1  # subtract one to get the gaps (except for the first)

# pre-allocate a large enough byte buffer
s = np.full(total_bytes , '\t', dtype='S1')

# write element into the buffer
start = 0
for i, (g, e) in enumerate(zip(gap, elements)):
    start += g
    s[start: start + k].view(dtype=('S', k))[:] = e
    start += k + 1
return s.tostring().decode('utf-8')

timeit.timeit(buffer_plumbing, number=10)  # 26.82

1 Ответ

0 голосов
/ 16 ноября 2018

Вы можете предварительно отсортировать данные после преобразования их в пару пустых массивов.Это позволит вам манипулировать одним ранее существующим буфером, а не копировать строки снова и снова по мере их перераспределения.Разница между моим предложением и вашей попыткой состоит в том, что мы будем использовать ndarray.tobytes (или ndarray.tostring) при условии, что у вас есть только символы ASCII.Фактически, вы можете полностью обойти операцию копирования, связанную с преобразованием в bytes объект, используя ndarray.tofile напрямую.

Если у вас есть elements в руке, вы знаете,что общая длина вашей линии будет равна общей длине разделителей табуляции elements и n-1.Поэтому начало элемента в полной строке - это его индекс (количество предшествующих ему вкладок) плюс совокупная длина всех элементов, предшествующих ему.Вот простая реализация заполнения одного буфера с использованием в основном циклов Python:

lengths = np.array([len(e) for e in elements])
indices = np.asanyarray(indices)
elements = np.array(elements, dtype='S7')
order = np.argsort(indices)

elements = elements[order]
indices = indices[order]
lengths = lengths[order]

cumulative = np.empty_like(lengths)
cumulative[0] = 0
np.cumsum(lengths[:-1], out=cumulative[1:])
cumulative += lengths

s = np.full(cumulative[-1] + n - 1, '\t', dtype='S1')
for i, l, e in zip(cumulative, lengths, elements):
    s[i:i + l].view(dtype=('S', l))[:] = e

Здесь можно поиграть с множеством возможных оптимизаций, таких как возможность выделения s с использованием np.empty и заполняются только обязательные элементы вкладками.Это будет оставлено в качестве акциза для читателя.

Другая возможность состоит в том, чтобы полностью не преобразовывать elements в пустой массив (он, вероятно, просто тратит пространство и время).Затем вы можете переписать цикл for как

for i, l, o in zip(cumulative, lengths, order):
    s[i:i + l].view(dtype=('S', l))[:] = elements[o]

. Вы можете записать результат в объект bytes с помощью

s = s.tobytes()

ИЛИ

s = s.tostring()

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

s.tofile(f)

Это сэкономит вам немного памяти и время обработки.

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

indices = np.asanyarray(indices)
order = np.argsort(indices)

indices = indices[order]

tabs = np.empty_like(indices)
tabs[0] = indices[0]
np.subtract(indices[1:], indices[:-1], out=tabs[1:])
tabs[1:] -= 1

for t, o in zip(tabs, order):
    f.write('\t' * t)
    f.write(elements[o])
f.write('\t' * (n - indices[-1] - 1))

Этот второй подход имеет два основных преимущества помимоуменьшенная сумма расчета.Во-первых, он работает с символами Юникода, а не только с ASCII.Во-вторых, он не выделяет никаких буферов, кроме строк вкладок, которые должны быть очень быстрыми.

В обоих случаях сортировка elements и indices в порядке возрастания по индексу значительно ускорит процесс.,Первый случай уменьшается до

lengths = np.array([len(e) for e in elements])
indices = np.asanyarray(indices)

cumulative = np.empty_like(lengths)
cumulative[0] = 0
np.cumsum(lengths[:-1], out=cumulative[1:])
cumulative += lengths

s = np.full(cumulative[-1] + n - 1, '\t', dtype='S1')
for i, l, e in zip(cumulative, lengths, elements):
    s[i:i + l].view(dtype=('S', l))[:] = e

, а второй становится просто

indices = np.asanyarray(indices)

tabs = np.empty_like(indices)
tabs[0] = indices[0]
np.subtract(indices[1:], indices[:-1], out=tabs[1:])
tabs[1:] -= 1

for t, e in zip(tabs, elements):
    f.write('\t' * t)
    f.write(e)
f.write('\t' * (n - indices[-1] - 1))
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...