python

Профилирование и оптимизация символьных вычислений для будущего сервера

  • вторник, 9 мая 2017 г. в 03:14:11
https://habrahabr.ru/post/328170/
  • Программирование
  • Высокая производительность
  • Python
  • NoSQL


Привет, Хабр! Сегодня хочу поделиться своим небольшим опытом выбора инструментов для организации расчетов на будущем сервере. Отмечу сразу, что в этой публикации речь пойдет не о самом сервере, а скорее об оптимизации символьных вычислений на нем.

Задача


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

Типовая формула. Использовалась для профилирования
8.348841409877572e-11*x1_*x2_*x3_*x4_*x5_*x6_*x7_ — 3.480284409621004e-9*x1_*x2_*x3_*x4_*x5_*x6_ — 1.44049340858321e-9*x1_*x2_*x3_*x4_*x5_*x7_ + 6.004816835089577e-8*x1_*x2_*x3_*x4_*x5_ — 2.674192940005371e-9*x1_*x2_*x3_*x4_*x6_*x7_ + 1.1147596343241695e-7*x1_*x2_*x3_*x4_*x6_ + 4.614001865646533e-8*x1_*x2_*x3_*x4_*x7_ — 1.92338517189701e-6*x1_*x2_*x3_*x4_ — 3.980463071998064e-9*x1_*x2_*x3_*x5_*x6_*x7_ + 1.6592892295580475e-7*x1_*x2_*x3_*x5_*x6_ + 6.867815593308846e-8*x1_*x2_*x3_*x5_*x7_ — 2.862906227803913e-6*x1_*x2_*x3_*x5_ + 1.2749703798969891e-7*x1_*x2_*x3_*x6_*x7_ — 5.314820397426395e-6*x1_*x2_*x3_*x6_ — 2.199809760060692e-6*x1_*x2_*x3_*x7_ + 9.1700905110223e-5*x1_*x2_*x3_ — 1.846888532733293e-9*x1_*x2_*x4_*x5_*x6_*x7_ + 7.69890890657543e-8*x1_*x2_*x4_*x5_*x6_ + 3.1865865064706345e-8*x1_*x2_*x4_*x5_*x7_ — 1.3283551698311385e-6*x1_*x2_*x4_*x5_ + 5.915714175810938e-8*x1_*x2_*x4_*x6_*x7_ — 2.4660148079756056e-6*x1_*x2_*x4_*x6_ — 1.0206861262244266e-6*x1_*x2_*x4_*x7_ + 4.254815271209286e-5*x1_*x2_*x4_ + 8.80537876744858e-8*x1_*x2_*x5_*x6_*x7_ — 3.6705956013363683e-6*x1_*x2_*x5_*x6_ — 1.5192633852443432e-6*x1_*x2_*x5_*x7_ + 6.333176170880347e-5*x1_*x2_*x5_ — 2.820424906208041e-6*x1_*x2_*x6_*x7_ + 0.0001175717652455964*x1_*x2_*x6_ + 4.866307746134377e-5*x1_*x2_*x7_ — 0.002028560982722154*x1_*x2_ — 4.643965319933718e-9*x1_*x3_*x4_*x5_*x6_*x7_ + 1.9358756900289542e-7*x1_*x3_*x4_*x5_*x6_ + 8.012609870218512e-8*x1_*x3_*x4_*x5_*x7_ — 3.3401232720775553e-6*x1_*x3_*x4_*x5_ + 1.4874948386055242e-7*x1_*x3_*x4_*x6_*x7_ — 6.200746333919621e-6*x1_*x3_*x4_*x6_ — 2.5664954382120103e-6*x1_*x3_*x4_*x7_ + 0.00010698650352362546*x1_*x3_*x4_ + 2.2140953873789337e-7*x1_*x3_*x5_*x6_*x7_ — 9.229641340558273e-6*x1_*x3_*x5_*x6_ — 3.8201582714825905e-6*x1_*x3_*x5_*x7_ + 0.00015924648463737888*x1_*x3_*x5_ — 7.091903641665703e-6*x1_*x3_*x6_*x7_ + 0.00029563191995286495*x1_*x3_*x6_ + 0.00012236236302703984*x1_*x3_*x7_ — 0.005100777187540484*x1_*x3_ + 1.0273144909755949e-7*x1_*x4_*x5_*x6_*x7_ — 4.282446163036621e-6*x1_*x4_*x5_*x6_ — 1.7725089771387925e-6*x1_*x4_*x5_*x7_ + 7.388851548491282e-5*x1_*x4_*x5_ — 3.290560750768279e-6*x1_*x4_*x6_*x7_ + 0.0001371697701523112*x1_*x4_*x6_ + 5.6774712332795935e-5*x1_*x4_*x7_ — 0.0023667012497318313*x1_*x4_ — 4.897909687533869e-6*x1_*x5_*x6_*x7_ + 0.0002041734515648569*x1_*x5_*x6_ + 8.45076066374878e-5*x1_*x5_*x7_ — 0.0035227700858871253*x1_*x5_ + 0.00015688350080537115*x1_*x6_*x7_ — 0.006539819616205367*x1_*x6_ — 0.0027068382268636906*x1_*x7_ + 0.11283680975413288*x1_ — 1.4404933842970813e-9*x2_*x3_*x4_*x5_*x6_*x7_ + 6.004816833354854e-8*x2_*x3_*x4_*x5_*x6_ + 2.4854000114926666e-8*x2_*x3_*x4_*x5_*x7_ — 1.0360597302149638e-6*x2_*x3_*x4_*x5_ + 4.614001870156814e-8*x2_*x3_*x4_*x6_*x7_ — 1.923385171910888e-6*x2_*x3_*x4_*x6_ — 7.960911484056199e-7*x2_*x3_*x4_*x7_ + 3.3185723683902546e-5*x2_*x3_*x4_ + 6.867815595043569e-8*x2_*x3_*x5_*x6_*x7_ — 2.8629062278143214e-6*x2_*x3_*x5_*x6_ — 1.1849599028824348e-6*x2_*x3_*x5_*x7_ + 4.9396042143235244e-5*x2_*x3_*x5_ — 2.1998097600572225e-6*x2_*x3_*x6_*x7_ + 9.170090511020218e-5*x2_*x3_*x6_ + 3.795510120421959e-5*x2_*x3_*x7_ — 0.0015821900589679597*x2_*x3_ + 3.1865865045624386e-8*x2_*x4_*x5_*x6_*x7_ — 1.3283551698172608e-6*x2_*x4_*x5_*x6_ — 5.498076038248229e-7*x2_*x4_*x5_*x7_ + 2.2919188659665732e-5*x2_*x4_*x5_ — 1.0206861262122835e-6*x2_*x4_*x6_*x7_ + 4.254815271210674e-5*x2_*x4_*x6_ + 1.7610725219094348e-5*x2_*x4_*x7_ — 0.0007341177730757296*x2_*x4_ — 1.5192633852512821e-6*x2_*x5_*x6_*x7_ + 6.333176170880174e-5*x2_*x5_*x6_ + 2.6213082872067472e-5*x2_*x5_*x7_ — 0.0010927142286346146*x2_*x5_ + 4.8663077461354176e-5*x2_*x6_*x7_ — 0.002028560982722149*x2_*x6_ — 0.0008396235272224249*x2_*x7_ + 0.03500040721534296*x2_ + 8.012609870391985e-8*x3_*x4_*x5_*x6_*x7_ — 3.340123272067147e-6*x3_*x4_*x5_*x6_ — 1.38248054011407e-6*x3_*x4_*x5_*x7_ + 5.762985469397186e-5*x3_*x4_*x5_ — 2.566495438213745e-6*x3_*x4_*x6_*x7_ + 0.00010698650352363066*x3_*x4_*x6_ + 4.428182648625982e-5*x3_*x4_*x7_ — 0.00184592488062541*x3_*x4_ — 3.820158271480856e-6*x3_*x5_*x6_*x7_ + 0.00015924648463738755*x3_*x5_*x6_ + 6.591228770929172e-5*x3_*x5_*x7_ — 0.0027476087026188038*x3_*x5_ + 0.00012236236302704678*x3_*x6_*x7_ — 0.005100777187540465*x3_*x6_ — 0.0021112170500446024*x3_*x7_ + 0.08800784408220161*x3_ — 1.7725089771387925e-6*x4_*x5_*x6_*x7_ + 7.388851548491629e-5*x4_*x5_*x6_ + 3.058253437834488e-5*x4_*x5_*x7_ — 0.0012748584600295945*x4_*x5_ + 5.677471233278379e-5*x4_*x6_*x7_ — 0.002366701249731833*x4_*x6_ — 0.0009795801398659112*x4_*x7_ + 0.040834615376717426*x4_ + 8.450760663750168e-5*x5_*x6_*x7_ — 0.003522770085887094*x5_*x6_ — 0.0014580782487184623*x5_*x7_ + 0.060781208246755536*x5_ — 0.0027068382268636976*x6_*x7_ + 0.11283680975413288*x6_ + 0.04670327439658878*x7_ + 0.5527559695044361

Формула поступает в виде строки и подлежит сохранению на сервере и вызову по запросам пользователей. Предполагается, что в запросах пользователей передаются параметры x1_, x2_,… в виде простого списка значений. Требуется определить способ организации подобных вычислений с уклоном на минимизацию времени выполнения.

Особенность 1


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

Особенность 2


Предполагается, что основной объем запросов будет носить групповой характер, т.е. в одном запросе могут передаваться несколько наборов значений x1_, x2_,… для расчета по одной и той же формуле.

Инструменты


Язык программирования — Python 3x. В качестве СУБД — Redis (NoSQL).

Пару слов про Redis. На мой взгляд данная задача — прекрасный пример для его использования: пользователь формирует формулу; формула обрабатывается и отправляется в хранилище; далее она извлекается из хранилища и обрабатывается в случае, если кто-то захотел ей воспользоваться; переданные по запросу значения подставляются в формулу и выдается результат. Всё. Единственное, что необходимо знать пользователю, который хочет что-то рассчитать — количество уникальных переменных в формуле. В Redis есть встроенный механизм хэшей, так почему бы им и не воспользоваться?

Пример использования Python + Redis
import redis
r = redis.StrictRedis(host='localhost', port=6379, db=0) #подключение к серверу redis

r.hset('expr:1', 'expr', expr) #запись самой формулы в хэш 'expr:1'
r.hset('expr:1', 'params', num) #запись числа параметров в хэш 'expr:1'

r.hget('expr:1', 'expr') #извлечение формулы из хэша 'expr:1'
r.hget('expr:1', 'params') #извлечение числа параметров из хэша 'expr:1'


Для работы с самими формулами воспользуемся замечательной библиотекой Sympy, которая умеет переводить формулу-строку в символьное выражение и производить необходимые вычисления (а вообще библиотека открывает огромный математический функционал для работы с символьными выражениями).

Профилирование и оптимизация


Для измерения времени выполнения участков кода воспользуемся следующим классом (где-то позаимствованным в просторах интернета):

class Profiler(object): #профилировщик времени
    def __init__(self,info=''):
        self.info = info
    def __enter__(self):
        self._startTime = time()
    def __exit__(self, type, value, traceback):
        print(self.info, "Elapsed time: {:.3f} sec".format(time() - self._startTime))

Поехали… Для чистоты эксперимента введем num_iter = 1000 — число испытаний.

Протестируем профилировщик на чтении формулы-строки из файла:

with Profiler('read (' + str(num_iter) + '): cycle'):
    for i in range(num_iter):
        f = open('expr.txt')
        expr_txt = f.read()
        f.close()
>>read (1000): cycle Elapsed time: 0.014 sec

Формула-строка загружена. Теперь определим сколько же в ней переменных и какие они (должны же мы знать в какую переменную именно подставлять значения):

with Profiler('find unique sorted symbols (' + str(num_iter) + '): cycle'):
    for i in range(num_iter):
        symbols_set = set()
        result = re.findall(r"x\d_", expr_txt)
        for match in result:
            symbols_set.add(match)
        symbols_set = sorted(symbols_set)
        symbols_list = symbols(symbols_set)
>>find unique sorted symbols (1000): cycle Elapsed time: 0.156 sec

Полученное время вполне устраивает. Теперь переведем формулу-строку в символьное выражение:

with Profiler('sympify'):
    expr = sympify(expr_txt)
>>sympify Elapsed time: 0.426 sec

В этом виде ее уже можно использовать для вычислений. Попробуем:

with Profiler('subs cycle (' + str(num_iter) + '): cycle'):
    for i in range(num_iter):
        expr_copy = copy.copy(expr)
        for x in symbols_list:
            expr_copy = expr_copy.subs(x,1)
>>subs cycle (1000): cycle Elapsed time: 0.245 sec

Здесь есть особенность: sympy не умеет (?) подставлять сразу все значения в переменные символьного выражения. Приходится пользоваться циклом. В результате выполнения в expr_copy получаем вещественное число.

В sympy есть возможность преобразовать символьное выражение в лямбда-функцию с использованием модуля numpy, что теоретически должно ускорить расчеты. Осуществим перевод:

with Profiler('lambdify'):
    func = lambdify(tuple(symbols_list), expr, 'numpy') # returns a numpy-ready function
>>lambdify Elapsed time: 0.114 sec

Не слишком долго получилось, что радует. Теперь проверим как быстро будут осуществляться вычисления:

with Profiler('subs cycle (' + str(num_iter) + '): lambdify'):
    for i in range(num_iter):
        func(*[1 for i in range(len(symbols_set))])
>>subs cycle (1000): lambdify Elapsed time: 0.026 sec

Вот это уровень! Быстрее почти на порядок. Особенно вкусно, если учесть необходимость в групповых запросах (особенность 2). Проверим на всякий случай совпадение значений:

print('exp1 == exp2:', round(expr_copy,12) == round(func(*[1 for i in range(len(symbols_set))]),12))
>>exp1 == exp2: True

Вывод 1


Хранить строку-формулу нецелесообразно — велико время ее преобразования для вычислений. Имеет смысл хранить либо символьное выражение, либо лямбда-функцию.

Попробуем разобраться с хранением. Символьное выражение — класс sympy, лямбда функция — также класс (в особенности не вникал). Будем пробовать сериализовать с помощью встроенного pickle, cloudpickle, dill:

with Profiler('pickle_dumps cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        pickle_dump = pickle.dumps(expr)
with Profiler('pickle_loads cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        pickle.loads(pickle_dump)
print()
with Profiler('cloudpickle_dumps cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        cloudpickle_dump = cloudpickle.dumps(expr)
with Profiler('cloudpickle_loads cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        cloudpickle.loads(cloudpickle_dump)
print()
with Profiler('dill_dumps cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        dill_dump = dill.dumps(expr)
with Profiler('dill_loads cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        dill.loads(dill_dump)

>>pickle_dumps cycle (1000): sympifyed expr Elapsed time: 0.430 sec
>>pickle_loads cycle (1000): sympifyed expr Elapsed time: 2.320 sec
>>
>>cloudpickle_dumps cycle (1000): sympifyed expr Elapsed time: 7.584 sec
>>cloudpickle_loads cycle (1000): sympifyed expr Elapsed time: 2.314 sec
>>
>>dill_dumps cycle (1000): sympifyed expr Elapsed time: 8.259 sec
>>dill_loads cycle (1000): sympifyed expr Elapsed time: 2.806 sec

Отметим, что pickle супер быстро сериализует символьные выражения, если сравнивать с «коллегами». Время десериализации отличается, но уже не так существенно. Теперь попробует протестировать сериализацию/десериализацию в связке с хранением/загрузкой Redis. Следует отметить тот факт, что pickle не сумел сериализовать/десериализовать лямбда-функцию.

with Profiler('redis_set cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        r.set('expr', pickle_dump)
with Profiler('redis_get cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        r.get('expr')
print()
with Profiler('pickle_dumps + redis_set cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        r.set('expr', pickle.dumps(expr))
with Profiler('redis_get + pickle_loads cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        pickle.loads(r.get('expr'))
print()
with Profiler('cloudpickle_dumps + redis_set cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        r.set('expr', cloudpickle.dumps(expr))
with Profiler('redis_get + cloudpickle_loads cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        cloudpickle.loads(r.get('expr'))
print()
with Profiler('dill_dumps + redis_set cycle (' + str(num_iter) + '): lambdifyed expr'):
    for i in range(num_iter):
        r.set('expr', dill.dumps(expr))
with Profiler('redis_get + dill_loads cycle (' + str(num_iter) + '): lambdifyed expr'):
    for i in range(num_iter):
        dill.loads(r.get('expr'))
>>redis_set cycle (1000): sympifyed expr Elapsed time: 0.066 sec
>>redis_get cycle (1000): sympifyed expr Elapsed time: 0.051 sec
>>
>>pickle_dumps + redis_set cycle (1000): sympifyed expr Elapsed time: 0.524 sec
>>redis_get + pickle_loads cycle (1000): sympifyed expr Elapsed time: 2.437 sec
>>
>>cloudpickle_dumps + redis_set cycle (1000): sympifyed expr Elapsed time: 7.659 sec
>>redis_get + cloudpickle_loads cycle (1000): sympifyed expr Elapsed time: 2.492 sec
>>
>>dill_dumps + redis_set cycle (1000): lambdifyed expr Elapsed time: 8.333 sec
>>redis_get + dill_loads cycle (1000): lambdifyed expr Elapsed time: 2.932 sec

cloudpickle и dill с сериализацией/десериализацией лямбда-функции справились (в примере выше, правда, cloudpickle работал с символьным выражением).

Вывод 2


Redis показывает хороший результат чтение/запись 1000 значений в одном потоке. Чтобы сделать выбор в дальнейшем требуется профилировать полные цепочки действий от поступления формулы-строки до выдачи пользователю рассчитанного по ней значения:

print('\nFINAL performance test:')
with Profiler('sympify + pickle_dumps_sympifyed_expr + redis_set cycle (' + str(num_iter) + '): '):
    for i in range(num_iter):
        expr = sympify(expr_txt)
        r.set('expr', pickle.dumps(expr))
with Profiler('redis_get + pickle_loads_sympifyed_expr + subs cycle (' + str(num_iter) + '): '):
    for i in range(num_iter):
        loaded_expr = pickle.loads(r.get('expr'))
        expr_copy = copy.copy(loaded_expr)
        for x in symbols_list:
            expr_copy = expr_copy.subs(x,1)
with Profiler('sympify + lambdify + dill_dumps_lambdifyed_expr + redis_set cycle (' + str(num_iter) + '): '):
    for i in range(num_iter):
        expr = sympify(expr_txt)
        func = lambdify(tuple(symbols_list), expr, 'numpy')
        r.set('expr', dill.dumps(expr))
with Profiler('redis_get + dill_loads_lambdifyed_expr + subs cycle (' + str(num_iter) + '): '):
    for i in range(num_iter):
        loaded_expr = dill.loads(r.get('expr'))
        func(*[1 for i in range(len(symbols_set))])
with Profiler('sympify + cloudpickle_dumps_sympifyed_expr + redis_set cycle (' + str(num_iter) + '): '):
    for i in range(num_iter):
        expr = sympify(expr_txt)
        r.set('expr', cloudpickle.dumps(expr))
with Profiler('redis_get + cloudpickle_loads_sympifyed_expr + subs cycle (' + str(num_iter) + '): '):
    for i in range(num_iter):
        loaded_expr = cloudpickle.loads(r.get('expr'))
        expr_copy = copy.copy(loaded_expr)
        for x in symbols_list:
            expr_copy = expr_copy.subs(x,1)
with Profiler('sympify + lambdify + cloudpickle_dumps_lambdifyed_expr + redis_set cycle (' + str(num_iter) + '): '):
    for i in range(num_iter):
        expr = sympify(expr_txt)
        func = lambdify(tuple(symbols_list), expr, 'numpy')
        r.set('expr', cloudpickle.dumps(expr))
with Profiler('redis_get + cloudpickle_loads_lambdifyed_expr + subs cycle (' + str(num_iter) + '): '):
    for i in range(num_iter):
        loaded_expr = cloudpickle.loads(r.get('expr'))
        func(*[1 for i in range(len(symbols_set))])

>>FINAL performance test:
>>sympify + pickle_dumps_sympifyed_expr + redis_set cycle (1000):  Elapsed time: 15.075 sec
>>redis_get + pickle_loads_sympifyed_expr + subs cycle (1000):  Elapsed time: 2.929 sec
>>sympify + lambdify + dill_dumps_lambdifyed_expr + redis_set cycle (1000):  Elapsed time: 87.707 sec
>>redis_get + dill_loads_lambdifyed_expr + subs cycle (1000):  Elapsed time: 2.356 sec
>>sympify + cloudpickle_dumps_sympifyed_expr + redis_set cycle (1000):  Elapsed time: 23.633 sec
>>redis_get + cloudpickle_loads_sympifyed_expr + subs cycle (1000):  Elapsed time: 3.059 sec
>>sympify + lambdify + cloudpickle_dumps_lambdifyed_expr + redis_set cycle (1000):  Elapsed time: 86.739 sec
>>redis_get + cloudpickle_loads_lambdifyed_expr + subs cycle (1000):  Elapsed time: 1.721 sec

Вывод 3


Создание лямбда-функции и ее сериализация с помощью cloudpickle, конечно, оказались самыми долгими, НО, если вспомнить (особенность 1) некритичность времени обработки и хранения, то… Cloudpickle молодец! Удалось в рамках одного потока вытащить из базы, десериализовать и рассчитать 1000 раз за 1,7 сек. Что, в целом, хорошо, учитывая сложность исходной формулы-строки.

Попробуем оценить производительность для групповых запросов. Будем менять число групп параметров порядками с надеждой на улучшение результата:

print('\nTEST performance for complex requests:')

for x in [1,10,100,1000]:
    with Profiler('redis_get + cloudpickle_loads_lambdifyed_expr + ' + str(x) + '*subs cycle (' + str(round(num_iter/x)) + '): '):
        for i in range(round(num_iter/x)):
            loaded_expr = cloudpickle.loads(r.get('expr'))
            for j in range(x):
                func(*[1 for i in range(len(symbols_set))])

>>TEST performance for complex requests:
>>redis_get + cloudpickle_loads_lambdifyed_expr + 1*subs cycle (1000):  Elapsed time: 1.768 sec
>>redis_get + cloudpickle_loads_lambdifyed_expr + 10*subs cycle (100):  Elapsed time: 0.204 sec
>>redis_get + cloudpickle_loads_lambdifyed_expr + 100*subs cycle (10):  Elapsed time: 0.046 sec
>>redis_get + cloudpickle_loads_lambdifyed_expr + 1000*subs cycle (1):  Elapsed time: 0.028 sec

Результат выглядит вполне жизнеспособным. Расчеты проводились на виртуальной машине со следующими характеристиками: ОС Ubuntu 16.04.2 LTS, Процессор Intel® Core(TM) i7-4720HQ CPU @ 2.60GHz (выделено 1 ядро), DDR3-1600 (выделено 1Gb).

Заключение


Спасибо за просмотр! Буду рад конструктивной критике и интересным замечаниям.

В вопросе профилирования и оптимизации требуемых вычислений были использованы идеи и подходы, изложенные здесь (слишком «слабая» формула в примере, но хороший набор тестов) и здесь (информация о сериализации лямбда-функций).

Полный текст проведенных тестов, включая импорты библиотек
import redis

import pickle
import dill
import cloudpickle

import re
import copy
from time import time
from sympy.utilities.lambdify import lambdify
from sympy import sympify, symbols

class Profiler(object): #профилировщик времени
    def __init__(self,info=''):
        self.info = info
    def __enter__(self):
        self._startTime = time()
    def __exit__(self, type, value, traceback):
        print(self.info, "Elapsed time: {:.3f} sec".format(time() - self._startTime))

num_iter = 1000

dill.settings['recurse'] = True

r = redis.StrictRedis(host='localhost', port=6379, db=0)

with Profiler('read (' + str(num_iter) + '): cycle'):
    for i in range(num_iter):
        f = open('expr.txt')
        expr_txt = f.read()
        f.close()

with Profiler('find unique sorted symbols (' + str(num_iter) + '): cycle'):
    for i in range(num_iter):
        symbols_set = set()
        result = re.findall(r"x\d_", expr_txt)
        for match in result:
            symbols_set.add(match)
        symbols_set = sorted(symbols_set)
        symbols_list = symbols(symbols_set)

print()

with Profiler('sympify'):
    expr = sympify(expr_txt)

with Profiler('lambdify'):
    func = lambdify(tuple(symbols_list), expr, 'numpy') # returns a numpy-ready function

print()

with Profiler('subs cycle (' + str(num_iter) + '): cycle'):
    for i in range(num_iter):
        expr_copy = copy.copy(expr)
        for x in symbols_list:
            expr_copy = expr_copy.subs(x,1)

with Profiler('subs cycle (' + str(num_iter) + '): lambdify'):
    for i in range(num_iter):
        func(*[1 for i in range(len(symbols_set))])

print()

print('exp1 == exp2:', round(expr_copy,12) == round(func(*[1 for i in range(len(symbols_set))]),12))

print()

with Profiler('pickle_dumps cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        pickle_dump = pickle.dumps(expr)

with Profiler('pickle_loads cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        pickle.loads(pickle_dump)

print()

with Profiler('cloudpickle_dumps cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        cloudpickle_dump = cloudpickle.dumps(expr)

with Profiler('cloudpickle_loads cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        cloudpickle.loads(cloudpickle_dump)

print()

with Profiler('dill_dumps cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        dill_dump = dill.dumps(expr)

with Profiler('dill_loads cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        dill.loads(dill_dump)

print()

#убедились, что все правильно считает (до 12 знака), сравнили производительность, попробуем побаловаться с redis

with Profiler('redis_set cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        r.set('expr', pickle_dump)

with Profiler('redis_get cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        r.get('expr')

print()

with Profiler('pickle_dumps + redis_set cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        r.set('expr', pickle.dumps(expr))

with Profiler('redis_get + pickle_loads cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        pickle.loads(r.get('expr'))

print()

with Profiler('cloudpickle_dumps + redis_set cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        r.set('expr', cloudpickle.dumps(expr))

with Profiler('redis_get + cloudpickle_loads cycle (' + str(num_iter) + '): sympifyed expr'):
    for i in range(num_iter):
        cloudpickle.loads(r.get('expr'))

print()

with Profiler('dill_dumps + redis_set cycle (' + str(num_iter) + '): lambdifyed expr'):
    for i in range(num_iter):
        r.set('expr', dill.dumps(expr))

with Profiler('redis_get + dill_loads cycle (' + str(num_iter) + '): lambdifyed expr'):
    for i in range(num_iter):
        dill.loads(r.get('expr'))

print('\nFINAL performance test:')

with Profiler('sympify + pickle_dumps_sympifyed_expr + redis_set cycle (' + str(num_iter) + '): '):
    for i in range(num_iter):
        expr = sympify(expr_txt)
        r.set('expr', pickle.dumps(expr))

with Profiler('redis_get + pickle_loads_sympifyed_expr + subs cycle (' + str(num_iter) + '): '):
    for i in range(num_iter):
        loaded_expr = pickle.loads(r.get('expr'))
        expr_copy = copy.copy(loaded_expr)
        for x in symbols_list:
            expr_copy = expr_copy.subs(x,1)

with Profiler('sympify + lambdify + dill_dumps_lambdifyed_expr + redis_set cycle (' + str(num_iter) + '): '):
    for i in range(num_iter):
        expr = sympify(expr_txt)
        func = lambdify(tuple(symbols_list), expr, 'numpy')
        r.set('expr', dill.dumps(expr))

with Profiler('redis_get + dill_loads_lambdifyed_expr + subs cycle (' + str(num_iter) + '): '):
    for i in range(num_iter):
        loaded_expr = dill.loads(r.get('expr'))
        func(*[1 for i in range(len(symbols_set))])

with Profiler('sympify + cloudpickle_dumps_sympifyed_expr + redis_set cycle (' + str(num_iter) + '): '):
    for i in range(num_iter):
        expr = sympify(expr_txt)
        r.set('expr', cloudpickle.dumps(expr))

with Profiler('redis_get + cloudpickle_loads_sympifyed_expr + subs cycle (' + str(num_iter) + '): '):
    for i in range(num_iter):
        loaded_expr = cloudpickle.loads(r.get('expr'))
        expr_copy = copy.copy(loaded_expr)
        for x in symbols_list:
            expr_copy = expr_copy.subs(x,1)

with Profiler('sympify + lambdify + cloudpickle_dumps_lambdifyed_expr + redis_set cycle (' + str(num_iter) + '): '):
    for i in range(num_iter):
        expr = sympify(expr_txt)
        func = lambdify(tuple(symbols_list), expr, 'numpy')
        r.set('expr', cloudpickle.dumps(expr))

with Profiler('redis_get + cloudpickle_loads_lambdifyed_expr + subs cycle (' + str(num_iter) + '): '):
    for i in range(num_iter):
        loaded_expr = cloudpickle.loads(r.get('expr'))
        func(*[1 for i in range(len(symbols_set))])

print('\nTEST performance for complex requests:')

for x in [1,10,100,1000]:
    with Profiler('redis_get + cloudpickle_loads_lambdifyed_expr + ' + str(x) + '*subs cycle (' + str(round(num_iter/x)) + '): '):
        for i in range(round(num_iter/x)):
            loaded_expr = cloudpickle.loads(r.get('expr'))
            for j in range(x):
                func(*[1 for i in range(len(symbols_set))])


#r.set('expr', func)

>>read (1000): cycle Elapsed time: 0.014 sec
>>find unique sorted symbols (1000): cycle Elapsed time: 0.156 sec
>>
>>sympify Elapsed time: 0.426 sec
>>lambdify Elapsed time: 0.114 sec
>>
>>subs cycle (1000): cycle Elapsed time: 0.245 sec
>>subs cycle (1000): lambdify Elapsed time: 0.026 sec
>>
>>exp1 == exp2: True
>>
>>pickle_dumps cycle (1000): sympifyed expr Elapsed time: 0.430 sec
>>pickle_loads cycle (1000): sympifyed expr Elapsed time: 2.320 sec
>>
>>cloudpickle_dumps cycle (1000): sympifyed expr Elapsed time: 7.584 sec
>>cloudpickle_loads cycle (1000): sympifyed expr Elapsed time: 2.314 sec
>>
>>dill_dumps cycle (1000): sympifyed expr Elapsed time: 8.259 sec
>>dill_loads cycle (1000): sympifyed expr Elapsed time: 2.806 sec
>>
>>redis_set cycle (1000): sympifyed expr Elapsed time: 0.066 sec
>>redis_get cycle (1000): sympifyed expr Elapsed time: 0.051 sec
>>
>>pickle_dumps + redis_set cycle (1000): sympifyed expr Elapsed time: 0.524 sec
>>redis_get + pickle_loads cycle (1000): sympifyed expr Elapsed time: 2.437 sec
>>
>>cloudpickle_dumps + redis_set cycle (1000): sympifyed expr Elapsed time: 7.659 sec
>>redis_get + cloudpickle_loads cycle (1000): sympifyed expr Elapsed time: 2.492 sec
>>
>>dill_dumps + redis_set cycle (1000): lambdifyed expr Elapsed time: 8.333 sec
>>redis_get + dill_loads cycle (1000): lambdifyed expr Elapsed time: 2.932 sec
>>
>>FINAL performance test:
>>sympify + pickle_dumps_sympifyed_expr + redis_set cycle (1000):  Elapsed time: 15.075 sec
>>redis_get + pickle_loads_sympifyed_expr + subs cycle (1000):  Elapsed time: 2.929 sec
>>sympify + lambdify + dill_dumps_lambdifyed_expr + redis_set cycle (1000):  Elapsed time: 87.707 sec
>>redis_get + dill_loads_lambdifyed_expr + subs cycle (1000):  Elapsed time: 2.356 sec
>>sympify + cloudpickle_dumps_sympifyed_expr + redis_set cycle (1000):  Elapsed time: 23.633 sec
>>redis_get + cloudpickle_loads_sympifyed_expr + subs cycle (1000):  Elapsed time: 3.059 sec
>>sympify + lambdify + cloudpickle_dumps_lambdifyed_expr + redis_set cycle (1000):  Elapsed time: 86.739 sec
>>redis_get + cloudpickle_loads_lambdifyed_expr + subs cycle (1000):  Elapsed time: 1.721 sec
>>
>>TEST performance for complex requests:
>>redis_get + cloudpickle_loads_lambdifyed_expr + 1*subs cycle (1000):  Elapsed time: 1.768 sec
>>redis_get + cloudpickle_loads_lambdifyed_expr + 10*subs cycle (100):  Elapsed time: 0.204 sec
>>redis_get + cloudpickle_loads_lambdifyed_expr + 100*subs cycle (10):  Elapsed time: 0.046 sec
>>redis_get + cloudpickle_loads_lambdifyed_expr + 1000*subs cycle (1):  Elapsed time: 0.028 sec


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

  • Создать файл expr.txt рядом с python-скриптом и поместить в него формулу-строку соответствующего вида
  • Установить библиотеки redis, dill, cloudpickle, sympy, numpy