Увеличение скорости кода python
У меня есть код python, который имеет много классов. Я использовал cProfile
, чтобы узнать, что общее время запуска программы составляет 68 секунд. Я обнаружил, что следующая функция в классе под названием Buyers
занимает около 60 секунд этих 68 секунд. Мне нужно запустить программу примерно 100 раз, поэтому любое увеличение скорости поможет. Можете ли вы предложить способы увеличить скорость, изменив код? Если вам нужна дополнительная информация, которая поможет, сообщите мне.
def qtyDemanded(self, timePd, priceVector):
'''Returns quantity demanded in period timePd. In addition,
also updates the list of customers and non-customers.
Inputs: timePd and priceVector
Output: count of people for whom priceVector[-1] < utility
'''
## Initialize count of customers to zero
## Set self.customers and self.nonCustomers to empty lists
price = priceVector[-1]
count = 0
self.customers = []
self.nonCustomers = []
for person in self.people:
if person.utility >= price:
person.customer = 1
self.customers.append(person)
else:
person.customer = 0
self.nonCustomers.append(person)
return len(self.customers)
self.people
- это список объектов person
. Каждый person
имеет customer
и utility
в качестве своих атрибутов.
EDIT - добавлен ответ
-------------------------------------
Большое спасибо за предложения. Здесь
ответ на некоторые вопросы и предложения, которые люди любезно
сделал. Я не пробовал их всех, но попробую других и напишу позже.
(1) @amber - доступ к функции осуществляется 80 000 раз.
(2) @gnibbler и другие - self.people - это список объектов Person в памяти. Не подключен к базе данных.
(3) @Hugh Bothwell
cumtime, взятый исходной функцией - 60,8 с (доступ к 80000 раз)
cumtime, взятый новой функцией с локальными псевдонимами функций, как было предложено - 56,4 с (доступ к 80000 раз)
(4) @rotoglup и @Martin Thomas
Я еще не пробовал ваши решения. Мне нужно проверить остальную часть кода, чтобы увидеть места, где я использую self.customers, прежде чем я смогу внести изменения, не добавляя клиентов в список self.customers. Но я попробую это и напишу.
(5) @TryPyPy - спасибо за ваше любезное предложение проверить код.
Позвольте мне вначале прочитать немного о тех предложениях, которые вы сделали, чтобы узнать, будут ли эти возможности использоваться.
РЕДАКТИРОВАТЬ 2
Некоторые предположили, что, поскольку я помещаю клиентов и неклиентов в self.people
, я должен попробовать, не создавая отдельные списки self.customers
и self.noncustomers
, используя append. Вместо этого я должен перебрать self.people
, чтобы найти количество клиентов. Я попробовал следующий код и приурочил обе функции ниже f_w_append
и f_wo_append
. Я обнаружил, что последнее занимает меньше времени, но это еще 96% времени, затраченного первым. То есть, это очень небольшое увеличение скорости.
@TryPyPy - Следующий фрагмент кода достаточно велик, чтобы проверить функцию узкого места, если ваше предложение все еще существует, чтобы проверить его с другими компиляторами.
Спасибо всем, кто ответил.
import numpy
class person(object):
def __init__(self, util):
self.utility = util
self.customer = 0
class population(object):
def __init__(self, numpeople):
self.people = []
self.cus = []
self.noncus = []
numpy.random.seed(1)
utils = numpy.random.uniform(0, 300, numpeople)
for u in utils:
per = person(u)
self.people.append(per)
popn = population(300)
def f_w_append():
'''Function with append'''
P = 75
cus = []
noncus = []
for per in popn.people:
if per.utility >= P:
per.customer = 1
cus.append(per)
else:
per.customer = 0
noncus.append(per)
return len(cus)
def f_wo_append():
'''Function without append'''
P = 75
for per in popn.people:
if per.utility >= P:
per.customer = 1
else:
per.customer = 0
numcustomers = 0
for per in popn.people:
if per.customer == 1:
numcustomers += 1
return numcustomers
РЕДАКТИРОВАТЬ 3: Кажется, проблема с numpy
Это в ответ на то, что сказал Джон Махин ниже. Ниже вы видите два способа определения класса Population
. Я запускал программу ниже дважды, один раз с каждым способом создания класса Population
. Один использует numpy и один не использует numpy. Один без numpy принимает аналогичное время, которое Джон нашел в своих прогонах. Один с numpy занимает гораздо больше времени. Мне не ясно, что экземпляр popn
создается до начала записи времени (по крайней мере, это то, что оно появляется из кода). Затем, почему версия numpy занимает больше времени. И, я думал, что numpy должно быть более эффективным. Во всяком случае, проблема, похоже, связана с numpy и не с добавлением, хотя она немного замедляет работу. Может кто-нибудь, пожалуйста, подтвердите с помощью кода ниже? Спасибо.
import random # instead of numpy
import numpy
import time
timer_func = time.time # using Mac OS X 10.5.8
class Person(object):
def __init__(self, util):
self.utility = util
self.customer = 0
class Population(object):
def __init__(self, numpeople):
random.seed(1)
self.people = [Person(random.uniform(0, 300)) for i in xrange(numpeople)]
self.cus = []
self.noncus = []
# Numpy based
# class Population(object):
# def __init__(self, numpeople):
# numpy.random.seed(1)
# utils = numpy.random.uniform(0, 300, numpeople)
# self.people = [Person(u) for u in utils]
# self.cus = []
# self.noncus = []
def f_wo_append(popn):
'''Function without append'''
P = 75
for per in popn.people:
if per.utility >= P:
per.customer = 1
else:
per.customer = 0
numcustomers = 0
for per in popn.people:
if per.customer == 1:
numcustomers += 1
return numcustomers
t0 = timer_func()
for i in xrange(20000):
x = f_wo_append(popn)
t1 = timer_func()
print t1-t0
Изменить 4: см. ответы Джона Мачина и TryPyPy
Поскольку здесь было так много исправлений и обновлений, те, кто впервые здесь оказался, могут немного запутаться. См. Ответы Джона Мачина и TryPyPy. Оба они могут помочь в улучшении скорости кода. Я благодарен им и другим, которые предупредили меня о медлительности append
. Поскольку в этом случае я собираюсь использовать решение Джона Мачина и не использовать numpy для создания утилит, я принимаю его ответ в качестве ответа. Тем не менее, я очень ценю указания, упомянутые TryPyPy.
Ответы
Ответ 1
Пожалуйста, подумайте об уменьшении вашей функции f_wo_append
:
def f_wo_append():
'''Function without append'''
P = 75
numcustomers = 0
for person in popn.people:
person.customer = iscust = person.utility >= P
numcustomers += iscust
return numcustomers
Изменить в ответ на комментарий OP "" Это значительно ухудшилось! Урезанная версия занимает в 4 раза больше времени, чем версия, которую я опубликовал выше. "" "
Нет никакого способа, чтобы это могло произойти "в 4 раза больше" (5 раз?)... вот мой код, который демонстрирует значительное сокращение случая "без append", как я предложил, а также вводит значительное улучшение в случае "с добавлением".
import random # instead of numpy
import time
timer_func = time.clock # better on Windows, use time.time on *x platform
class Person(object):
def __init__(self, util):
self.utility = util
self.customer = 0
class Population(object):
def __init__(self, numpeople):
random.seed(1)
self.people = [Person(random.uniform(0, 300)) for i in xrange(numpeople)]
self.cus = []
self.noncus = []
def f_w_append(popn):
'''Function with append'''
P = 75
cus = []
noncus = []
for per in popn.people:
if per.utility >= P:
per.customer = 1
cus.append(per)
else:
per.customer = 0
noncus.append(per)
popn.cus = cus # omitted from OP code
popn.noncus = noncus # omitted from OP code
return len(cus)
def f_w_append2(popn):
'''Function with append'''
P = 75
popn.cus = []
popn.noncus = []
cusapp = popn.cus.append
noncusapp = popn.noncus.append
for per in popn.people:
if per.utility >= P:
per.customer = 1
cusapp(per)
else:
per.customer = 0
noncusapp(per)
return len(popn.cus)
def f_wo_append(popn):
'''Function without append'''
P = 75
for per in popn.people:
if per.utility >= P:
per.customer = 1
else:
per.customer = 0
numcustomers = 0
for per in popn.people:
if per.customer == 1:
numcustomers += 1
return numcustomers
def f_wo_append2(popn):
'''Function without append'''
P = 75
numcustomers = 0
for person in popn.people:
person.customer = iscust = person.utility >= P
numcustomers += iscust
return numcustomers
if __name__ == "__main__":
import sys
popsize, which, niter = map(int, sys.argv[1:4])
pop = Population(popsize)
func = (f_w_append, f_w_append2, f_wo_append, f_wo_append2)[which]
t0 = timer_func()
for _unused in xrange(niter):
nc = func(pop)
t1 = timer_func()
print "popsize=%d func=%s niter=%d nc=%d seconds=%.2f" % (
popsize, func.__name__, niter, nc, t1 - t0)
и вот результаты его запуска (Python 2.7.1, Windows 7 Pro, "Intel Core i3 CPU 540 @3.07 ГГц" ):
C:\junk>\python27\python ncust.py 300 0 80000
popsize=300 func=f_w_append niter=80000 nc=218 seconds=5.48
C:\junk>\python27\python ncust.py 300 1 80000
popsize=300 func=f_w_append2 niter=80000 nc=218 seconds=4.62
C:\junk>\python27\python ncust.py 300 2 80000
popsize=300 func=f_wo_append niter=80000 nc=218 seconds=5.55
C:\junk>\python27\python ncust.py 300 3 80000
popsize=300 func=f_wo_append2 niter=80000 nc=218 seconds=4.29
Изменить 3 Почему numpy занимает больше времени:
>>> import numpy
>>> utils = numpy.random.uniform(0, 300, 10)
>>> print repr(utils[0])
42.777972538362874
>>> type(utils[0])
<type 'numpy.float64'>
и вот почему моя функция f_wo_append2 выполнялась в 4 раза дольше:
>>> x = utils[0]
>>> type(x)
<type 'numpy.float64'>
>>> type(x >= 75)
<type 'numpy.bool_'> # iscust refers to a numpy.bool_
>>> type(0 + (x >= 75))
<type 'numpy.int32'> # numcustomers ends up referring to a numpy.int32
>>>
Эмпирическое доказательство состоит в том, что эти пользовательские типы не так быстр при использовании в качестве скаляров... возможно, потому что им нужно reset аппаратное обеспечение с плавающей запятой каждый раз, когда они используются. OK для больших массивов, а не для скаляров.
Используете ли вы какие-либо другие функции numpy? Если нет, просто используйте модуль random
. Если у вас есть другое использование для numpy, вы можете принудить numpy.float64
к float
во время настройки популяции.
Ответ 2
Есть много вещей, которые вы можете попробовать после оптимизации кода Python для скорости. Если этой программе не нужны расширения C, вы можете запустить ее под PyPy, чтобы воспользоваться ее компилятором JIT. Вы можете попробовать сделать расширение C для возможного огромных ускорений. Shed Skin позволит вам конвертировать вашу программу Python в автономный двоичный файл С++.
Я готов отправить вашу программу в соответствии с этими различными сценариями оптимизации, если вы можете предоставить достаточно кода для бенчмаркинга,
Изменить. Прежде всего, я должен согласиться со всеми остальными: вы уверены, что правильно измеряете время? Пример кода работает 100 раз в течение менее 0,1 секунд здесь, так что есть хороший шанс, либо время не так, либо у вас есть узкое место (IO?), Которое отсутствует в образце кода.
Тем не менее, я сделал это 300000 человек, поэтому времена были последовательными. Здесь адаптированный код, совместно используемый CPython (2.5), PyPy и Shed Skin:
from time import time
import random
import sys
class person(object):
def __init__(self, util):
self.utility = util
self.customer = 0
class population(object):
def __init__(self, numpeople, util):
self.people = []
self.cus = []
self.noncus = []
for u in util:
per = person(u)
self.people.append(per)
def f_w_append(popn):
'''Function with append'''
P = 75
cus = []
noncus = []
# Help CPython a bit
# cus_append, noncus_append = cus.append, noncus.append
for per in popn.people:
if per.utility >= P:
per.customer = 1
cus.append(per)
else:
per.customer = 0
noncus.append(per)
return len(cus)
def f_wo_append(popn):
'''Function without append'''
P = 75
for per in popn.people:
if per.utility >= P:
per.customer = 1
else:
per.customer = 0
numcustomers = 0
for per in popn.people:
if per.customer == 1:
numcustomers += 1
return numcustomers
def main():
try:
numpeople = int(sys.argv[1])
except:
numpeople = 300000
print "Running for %s people, 100 times." % numpeople
begin = time()
random.seed(1)
# Help CPython a bit
uniform = random.uniform
util = [uniform(0.0, 300.0) for _ in xrange(numpeople)]
# util = [random.uniform(0.0, 300.0) for _ in xrange(numpeople)]
popn1 = population(numpeople, util)
start = time()
for _ in xrange(100):
r = f_wo_append(popn1)
print r
print "Without append: %s" % (time() - start)
popn2 = population(numpeople, util)
start = time()
for _ in xrange(100):
r = f_w_append(popn2)
print r
print "With append: %s" % (time() - start)
print "\n\nTotal time: %s" % (time() - begin)
if __name__ == "__main__":
main()
Запуск с PyPy так же просто, как запуск с CPython, вы просто набираете "pypy" вместо "python". Для Shed Skin вы должны преобразовать в С++, скомпилировать и запустить:
shedskin -e makefaster.py && make
# Check that you're using the makefaster.so file and run test
python -c "import makefaster; print makefaster.__file__; makefaster.main()"
И вот Cython -ized код:
from time import time
import random
import sys
cdef class person:
cdef readonly int utility
cdef public int customer
def __init__(self, util):
self.utility = util
self.customer = 0
class population(object):
def __init__(self, numpeople, util):
self.people = []
self.cus = []
self.noncus = []
for u in util:
per = person(u)
self.people.append(per)
cdef int f_w_append(popn):
'''Function with append'''
cdef int P = 75
cdef person per
cus = []
noncus = []
# Help CPython a bit
# cus_append, noncus_append = cus.append, noncus.append
for per in popn.people:
if per.utility >= P:
per.customer = 1
cus.append(per)
else:
per.customer = 0
noncus.append(per)
cdef int lcus = len(cus)
return lcus
cdef int f_wo_append(popn):
'''Function without append'''
cdef int P = 75
cdef person per
for per in popn.people:
if per.utility >= P:
per.customer = 1
else:
per.customer = 0
cdef int numcustomers = 0
for per in popn.people:
if per.customer == 1:
numcustomers += 1
return numcustomers
def main():
cdef int i, r, numpeople
cdef double _0, _300
_0 = 0.0
_300 = 300.0
try:
numpeople = int(sys.argv[1])
except:
numpeople = 300000
print "Running for %s people, 100 times." % numpeople
begin = time()
random.seed(1)
# Help CPython a bit
uniform = random.uniform
util = [uniform(_0, _300) for i in xrange(numpeople)]
# util = [random.uniform(0.0, 300.0) for _ in xrange(numpeople)]
popn1 = population(numpeople, util)
start = time()
for i in xrange(100):
r = f_wo_append(popn1)
print r
print "Without append: %s" % (time() - start)
popn2 = population(numpeople, util)
start = time()
for i in xrange(100):
r = f_w_append(popn2)
print r
print "With append: %s" % (time() - start)
print "\n\nTotal time: %s" % (time() - begin)
if __name__ == "__main__":
main()
Для его создания неплохо иметь setup.py, как этот:
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
ext_modules = [Extension("cymakefaster", ["makefaster.pyx"])]
setup(
name = 'Python code to speed up',
cmdclass = {'build_ext': build_ext},
ext_modules = ext_modules
)
Вы строите его с помощью: python setupfaster.py build_ext --inplace
Затем тест: python -c "import cymakefaster, print cymakefaster. файл; cymakefaster.main()"
Сроки выполнялись пять раз для каждой версии, причем Cython был самым быстрым и простым в использовании генераторами кода (Shed Skin стремится быть более простым, но критические сообщения об ошибках и неявное статическое типирование сделали это сложнее здесь). Что касается лучшего значения, PyPy дает впечатляющее ускорение в счетной версии без изменений кода.
#Results (time in seconds for 30000 people, 100 calls for each function):
Mean Min Times
CPython 2.5.2
Without append: 35.037 34.518 35.124, 36.363, 34.518, 34.620, 34.559
With append: 29.251 29.126 29.339, 29.257, 29.259, 29.126, 29.272
Total time: 69.288 68.739 69.519, 70.614, 68.746, 68.739, 68.823
PyPy 1.4.1
Without append: 2.672 2.655 2.655, 2.670, 2.676, 2.690, 2.668
With append: 13.030 12.672 12.680, 12.725, 14.319, 12.755, 12.672
Total time: 16.551 16.194 16.196, 16.229, 17.840, 16.295, 16.194
Shed Skin 0.7 (gcc -O2)
Without append: 1.601 1.599 1.599, 1.605, 1.600, 1.602, 1.599
With append: 3.811 3.786 3.839, 3.795, 3.798, 3.786, 3.839
Total time: 5.704 5.677 5.715, 5.705, 5.699, 5.677, 5.726
Cython 0.14 (gcc -O2)
Without append: 1.692 1.673 1.673, 1.710, 1.678, 1.688, 1.711
With append: 3.087 3.067 3.079, 3.080, 3.119, 3.090, 3.067
Total time: 5.565 5.561 5.562, 5.561, 5.567, 5.562, 5.572
Изменить: Aaaand более значимые тайминги, для 80000 звонков по 300 человек каждый:
Results (time in seconds for 300 people, 80000 calls for each function):
Mean Min Times
CPython 2.5.2
Without append: 27.790 25.827 25.827, 27.315, 27.985, 28.211, 29.612
With append: 26.449 24.721 24.721, 27.017, 27.653, 25.576, 27.277
Total time: 54.243 50.550 50.550, 54.334, 55.652, 53.789, 56.892
Cython 0.14 (gcc -O2)
Without append: 1.819 1.760 1.760, 1.794, 1.843, 1.827, 1.871
With append: 2.089 2.063 2.100, 2.063, 2.098, 2.104, 2.078
Total time: 3.910 3.859 3.865, 3.859, 3.944, 3.934, 3.951
PyPy 1.4.1
Without append: 0.889 0.887 0.894, 0.888, 0.890, 0.888, 0.887
With append: 1.671 1.665 1.665, 1.666, 1.671, 1.673, 1.681
Total time: 2.561 2.555 2.560, 2.555, 2.561, 2.561, 2.569
Shed Skin 0.7 (g++ -O2)
Without append: 0.310 0.301 0.301, 0.308, 0.317, 0.320, 0.303
With append: 1.712 1.690 1.733, 1.700, 1.735, 1.690, 1.702
Total time: 2.027 2.008 2.035, 2.008, 2.052, 2.011, 2.029
Shed Skin становится быстрее, PyPy превосходит Cython. Все три скорости ускоряются по сравнению с CPython.
Ответ 3
Вы можете устранить некоторые запросы, используя локальные псевдонимы функций:
def qtyDemanded(self, timePd, priceVector):
'''Returns quantity demanded in period timePd. In addition,
also updates the list of customers and non-customers.
Inputs: timePd and priceVector
Output: count of people for whom priceVector[-1] < utility
'''
price = priceVector[-1]
self.customers = []
self.nonCustomers = []
# local function aliases
addCust = self.customers.append
addNonCust = self.nonCustomers.append
for person in self.people:
if person.utility >= price:
person.customer = 1
addCust(person)
else:
person.customer = 0
addNonCust(person)
return len(self.customers)
Ответ 4
В зависимости от того, как часто вы добавляете новые элементы в self.people
или меняете person.utility
, вы можете рассмотреть сортировку self.people
по полю utility
.
Затем вы можете использовать функцию bisect
, чтобы найти нижний индекс i_pivot
, где выполняется условие person[i_pivot].utility >= price
. Это будет иметь более низкую сложность (O (log N)), чем ваш полный цикл (O (N))
С помощью этой информации вы можете в случае необходимости обновить список people
:
Вам действительно нужно обновлять поле utility
каждый раз? В отсортированном случае вы можете легко вывести это значение во время итерации: например, учитывая, что ваш список отсортирован в порядке incresing, utility = (index >= i_pivot)
Тот же вопрос с списками customers
и nonCustomers
. Зачем они нужны? Они могут быть заменены фрагментами исходного отсортированного списка: например, customers = self.people[0:i_pivot]
Все это позволит вам уменьшить сложность вашего алгоритма и использовать более встроенные (быстрые) функции Python, это может ускорить вашу реализацию.
Ответ 5
В этом комментарии звучат звонки:
'''Returns quantity demanded in period timePd. In addition,
also updates the list of customers and non-customers.
Помимо того, что timePd
не используется в функции, если вы действительно хотите просто вернуть количество, сделайте это только в функции. Делайте "дополнительно" в отдельной функции.
Затем снова профиль и посмотрите, какую из этих двух функций вы проводите большую часть своего времени.
Мне нравится применять SRP к методам, а также к классам: это упрощает тестирование.
Ответ 6
Некоторые любопытные вещи, которые я заметил:
timePd передается как параметр, но никогда не используется
цена - это массив, но вы используете только последнюю запись - почему бы не передать значение там вместо передачи списка?
подсчет инициализируется и никогда не используется
self.people содержит несколько объектов человека, которые затем копируются либо в self.customers, либо в self.noncustomers, а также с установленным флагом клиента. Почему бы не пропустить операцию копирования и, вернувшись, просто перебирать список, глядя на флаг клиента? Это сэкономит дорогостоящее приложение.
В качестве альтернативы попробуйте использовать psyco, который может ускорить чистый Python, иногда значительно.
Ответ 7
Удивительно, что показанная функция является таким узким местом, потому что она настолько проста. По этой причине я дважды проверю процедуру и результаты профилирования. Однако, если они верны, самая затратная часть вашей функции должна быть в цикле for
, который она содержит, конечно, поэтому имеет смысл сосредоточиться на ускорении этого. Один из способов сделать это - заменить if/else
на линейный код. Вы также можете немного уменьшить поиск атрибутов для метода списка append
. Здесь, как обе эти вещи могут быть достигнуты:
def qtyDemanded(self, timePd, priceVector):
'''Returns quantity demanded in period timePd. In addition,
also updates the list of customers and non-customers.
Inputs: timePd and priceVector
Output: count of people for whom priceVector[-1] < utility
'''
price = priceVector[-1] # last price
kinds = [[], []] # initialize sublists of noncustomers and customers
kindsAppend = [kinds[b].append for b in (False, True)] # append methods
for person in self.people:
person.customer = person.utility >= price # customer test
kindsAppend[person.customer](person) # add to proper list
self.nonCustomers = kinds[False]
self.customers = kinds[True]
return len(self.customers)
Тем не менее, я должен добавить, что кажется немного избыточным иметь как флаг customer
в каждом объекте человека, так и каждый из них помещать в отдельный список в зависимости от этого атрибута. Разумеется, создание этих двух списков не ускорит цикл.
Ответ 8
Вы просите угадать, и в основном вы догадываетесь.
Не нужно гадать. Вот пример.