Очень плохая производительность weakref в Python / SQL Alchemy

я провел день, пытаясь отладить проблему с памятью в моем скрипте Python. Я использую SQL Alchemy в качестве своего ORM. Здесь есть несколько запутанных вопросов, и я надеюсь, что если я перечислю их все, кто-нибудь сможет указать мне правильное направление.

для достижения производительности, которую я ищу, я читаю во всех записях в таблице (~400k), затем просматриваю электронную таблицу, сопоставляю записи, которые я ранее читал, а затем создаю новые записи (~800k) в другая таблица. Вот примерно как выглядит код:

dimensionMap = {}
for d in connection.session.query(Dimension):
   dimensionMap[d.businessKey] = d.primarySyntheticKey

# len(dimensionMap) == ~400k, sys.getsizeof(dimensionMap) == ~4MB

allfacts = []
sheet = open_spreadsheet(path)
for row in sheet.allrows():
    dimensionId = dimensionMap[row[0]]
    metric = row[1]

    fact = Fact(dimensionId, metric)
    connection.session.add(fact)
    allfacts.append(fact)

    if row.number % 20000 == 0:
        connection.session.flush()

# len(allfacts) == ~800k, sys.getsizeof(allfacts) == ~50MB

connection.session.commit()

sys.stdout.write('All Done')

400k и 800k не кажутся мне особенно большими числами, но я, тем не менее, сталкиваюсь с проблемами памяти машины с 4 ГБ памяти. Это действительно странно для меня, так как я запускал sys.getsizeof на моих двух самых больших коллекциях, и они оба были хорошо под любым размером, который вызвал бы проблемы.

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

Profiler output

во-первых, 87% времени программы тратится на фиксацию, в частности на эту строку кода:

self.transaction._new[state] = True

это можно найти в session.py:1367. self.transaction._new пример weakref.WeakKeyDictionary(). Почему это weakref:261:__setitem__ занимает так много времени?

во-вторых, даже когда программа сделана ("все сделано" было напечатано в stdout), сценарий продолжается, казалось бы, навсегда, с использованием 2,2 ГБ памяти.

Я сделал некоторые поиски на weakrefs, но не видел, чтобы кто-нибудь упоминал проблемы с производительностью, с которыми я сталкиваюсь. В конечном счете, я мало что могу с этим поделать, учитывая, что он похоронен глубоко в алхимии SQL, но я все равно буду признателен за любые идеи.

Выводы

как упоминалось @zzzeek, для поддержания постоянных объектов требуется много накладных расходов. Вот небольшой график, чтобы показать рост.

Total memory used vs number of persistent instances

линия тренда предполагает, что каждый постоянный экземпляр занимает около 2 кб памяти, даже если сам экземпляр составляет всего 30 байт. Это на самом деле приносит мне еще одну вещь, я узнал, что забрать sys.getsizeof С огромной солью.

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

последнее, что я узнал, это то, что, когда Python находится на грани истощения памяти и бьется как сумасшедший, происходят странные вещи, которые не следует принимать как часть проблемы. В моем случае, массовое замедление, профиль, указывающий на создание weakref, и отбой после завершения программы, все последствия проблема с памятью. Как только я перестал создавать и хранить постоянные экземпляры, а вместо этого просто держал вокруг свойств объектов, которые мне нужны, все остальные проблемы исчезли.

1 ответов


объекты 800k ORM очень большие. Это объекты Python, каждый из которых имеет __dict__ и _sa_instance_state атрибут, который сам является объектом, который затем имеет weakrefs и другие вещи внутри него, а затем Session имеет более одного weakref для вашего объекта-объект ORM отслеживается идентичность, функция что обеспечивает высокую степень автоматизации в настойчивости но ценой серий больше памяти и накладные расходы вызова функции.

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

ваш пример кода может быть изменен, чтобы не использовать объекты ORM identity-mapped следующим образом. Для получения более подробной информации о массовых вставках см. почему SQLAlchemy insert с sqlite в 25 раз медленнее, чем с помощью sqlite3 напрямую?.

# 1. only load individual columns - loading simple tuples instead 
# of full ORM objects with identity tracking.  these tuples can be
# used directly in a dict comprehension
dimensionMap = dict(
    connection.session.query(Dimension.businessKey, Dimension.primarySyntheticKey)
)

# 2. For bulk inserts, use Table.insert() call with
# multiparams in chunks
buf = []
for row in sheet.allrows():
    dimensionId = dimensionMap[row[0]]
    metric = row[1]

    buf.append({"dimensionId": dimensionId, "metric": metric})

    if len(buf == 20000):
        connection.session.execute(Fact.__table__.insert(), params=buf)
        buf[:] = []

connection.session.execute(Fact.__table__.insert(), params=buf)
sys.stdout.write('All Done')