OOP und MemoryError

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
ts7343
User
Beiträge: 69
Registriert: Mittwoch 5. Mai 2010, 13:48

Hallo,

ich habe ein Problem mit einem recht komplexen Programm, welches mit Hilfe von OOP immer auf ein Hauptobjekt
zugreift und das mit Hilfe verschiedener Funktionen von aussen schrittweise mit Daten füllt.
Bei einer bestimmten Datenmenge steigt das ganze mit MemoryError aus und ich hab das Gefühl, dass die Strategie,
dieses Hauptobjekt immer in der Argumentliste mitzuführen vielleicht der Grund dafür ist, dass das ganze mit einem
MemoryError endet.
(Evtl. macht Python dann daraus einen Call-by-Value und dupliziert die Daten?)
(Oder ist das ganze zu unsauber, wenn Daten in das Hauptobjekt geschrieben werden?)

Ich hab das ganze mal versucht auf ein kleines Beispiel herunterzubrechen, welches ungefähr den Datenfluss simuliert.
Das Hauptobjekt AllData sammelt alle Daten und Unterprogramme lesen und schreiben fleissig auf diesen Daten.

Könntet ihr mal schauen, ob diese Art des Datanhandlings evtl der Grund für Speicherprobleme sind?
Wie könnte man das ganze anders aufsetzen ohne das gesamte Memory Model umzuwerfen?
Gibt es Möglichkeiten zwischendurch den Speicher freizuschaufeln, der unnütz herumliegt? (del ? aber wo ansetzen?)

Danke

Code: Alles auswählen

class AllData:
   def __init__(self):
      self.data_a = DataA()
      self.data_b = DataB()
      self.data_c = DataC()
# -------------------------------------------------------------------------
class DataA:
   def __init__(self):                        self.data_dict = {}
   def add_data_to_dict(self, id, data_obj):  self.data_dict[id] = data_obj
   def get_len_data_a(self):                  return len(self.data_dict)
class ObjA:
   def __init__(self, name):                  self.name = name
# -------------------------------------------------------------------------      
class DataB:
   def __init__(self):                        self.data_dict = {}
   def add_data_to_dict(self, id, data_obj):  self.data_dict[id] = data_obj
   def get_len_data_b(self):                  return len(self.data_dict)
class ObjB:
   def __init__(self, name):                   self.name = name
# -------------------------------------------------------------------------
class DataC:
   def __init__(self):                        self.data_dict = {}
   def add_data_to_dict(self, id, data_obj):  self.data_dict[id] = data_obj
   def get_len_data_c(self):                  return len(self.data_dict)
class ObjC:
   def __init__(self, name):                  self.name = name
# -------------------------------------------------------------------------
def ctrl_a(all_data):                         create_a_objects(all_data)
def ctrl_b(all_data):                         create_b_objects(all_data)
def ctrl_c(all_data):                         create_c_objects(all_data)
# -------------------------------------------------------------------------
def create_a_objects(all_data):
   inst_a = ObjA("name_01")
   all_data.data_a.add_data_to_dict(1,inst_a)
   
def create_b_objects(all_data):
   inst_b = ObjB("name_02")
   all_data.data_b.add_data_to_dict(2,inst_b)
   
def create_c_objects(all_data):
   if all_data.data_a.get_len_data_a() > 0:
      inst_c = ObjC("name_03")   
      all_data.data_c.add_data_to_dict(100,inst_c)
      all_data.data_a.add_data_to_dict(100,inst_c)
   
# --- main ----------------------------------------------------------------
all_data = AllData()
ctrl_a(all_data)
ctrl_b(all_data)
ctrl_c(all_data)
print all_data.data_a.get_len_data_a()
# -------------------------------------------------------------------------
Zuletzt geändert von Anonymous am Dienstag 14. März 2017, 11:50, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Python übergibt Referenzen, die intern (in C) als Zeiger realisiert sind. Das sollte also erstmal kein Speicherproblem sein, außer bei extrem vielen Referenzen. Natürlich können aber ab einem Punkt eine sehr hohe Zahl von unterschiedlichen Objekten zum Problem werden. Schau dir hierzu vielleicht mal yield an. Ein grobes Schema zur Erzeugung deiner Objekte wäre dann:

Code: Alles auswählen

def create_objects(source):
    for line in source:
        yield get_object(line)
Natürlich mit aussagekräftigeren Namen...

Die Idee ist halt, dass man nicht alle Objekte auf einmal erzeugt und diese nach ihrer Erstellung gesammelt zurückgibt, sondern dass man pro Iterationsschritt einer Schleife immer nur das in diesem Schritt erzeugte Objekt ausliefert. Der Aufrufer kann dann seinerseits mit einer Schleife die Objekte durchgehen und etwas damit tun. Das nächste Objekt wird dabei also immer erst auf Anforderung erzeugt Also sozusagen in der Art get_next_object(), nur dass dieser Schritt aufgrund der Sprachmittel von Python etwas versteckt wird.
BlackJack

@ts7343: Es egal ob Du das `AllData`-Objekt als Argument übergibst oder nicht. Bei Aufrufen und Zuweisungen an Namen kopiert Python nicht die Daten sondern es handelt sich da immer um das selbe Objekt das übergeben oder zugewiesen wird. Das kann also nicht zu einem MemoryError führen.

Es ist wohl nicht die Frage ob es unsauber ist die Daten in dieses Hauptobjekt zu schreiben, sondern ob es nicht einfach zu viele Daten sind um sie im Adressraum eines Prozesses zu halten.

Der Speicher wird automatisch freigeschaufelt. Wenn da welcher unnütz belegt ist, dann nur wenn man Daten in Datenstrukturen behält die man eigentlich nicht mehr braucht. Ob das der Fall ist, kann man so allgemein nicht entscheiden, das musst Du schon selbst wissen was Du noch brauchst und was nicht. ``del`` ist da wahrscheinlich nicht so unbedingt das Mittel, denn das sorgt ja höchstens indirekt für das freigeben von Speicher. Einzig um einzelne Schlüssel/Wert-Paare aus Wörterbüchern zu entfernen würde ich das verwenden. Wenn man ganze Datenstrukturen zum Abschuss freigeben möchte, oder zumindest klarstellen möchte das von einem bestimmten Punkt im Programm kein Zugriff mehr erfolgt, würde ich `None` zuweisen und damit den Zugriff auf die Datenstruktur verhindern. Namen oder Attribute würde ich nicht löschen.

Wenn man keine Ahnung hat wo der Speicher verbraucht wird, sollte man am besten mal mit einem entsprechenden Werkzeug schauen wo und von was der Speicher verbraucht wird. So kann man eventuell auch Programmstellen auf die Spur kommen, von denen man das nicht erwartet hat, oder Objekten von denen man weiss das sie eigentlich nicht mehr gebraucht werden, die aber aus irgendwelchen Gründen immer noch erreichbar sind.

Ansonsten müsste man wenn der Hauptspeicher nicht reicht, auf externe Speicher zugreifen, also irgendeine Form von Datenbank. Das würde zwar das „Memory Model“ umwerfen, aber am Code müsste man ja grundsätzlich erst einmal nur die `Data*`-Klassen ändern, das die nicht auf Wörterbüchern im Speicher operieren, sondern die Daten in einer Datenbank halten. Was das für eine sein könnte, hängt von den Daten und Operationen ab. Wenn die Daten nicht persistent sein müssen, die Schlüssel auf Zeichenketten beschränkbar sind, und die Werte ge`pickle`d werden können, könnte man beispielsweise das `shelve`-Modul verwenden. Oder man bastelt sich mit SQLite einen einfachen Key/Value-Store. Vielleicht würde das Programm aber auch insgesamt von einer relationalen oder dokumentorientierten Datenbank profitieren können. Oder es könnten Daten sein die sinnvoll in HDF5 dargestellt werden können. Oder…
Benutzeravatar
ts7343
User
Beiträge: 69
Registriert: Mittwoch 5. Mai 2010, 13:48

Vielen Dank für die Rückmeldungen,

also yield hilft wahrscheinlich nicht viel, weil die Objekte sowieso einzeln in einem Loop in die Dictionaries geschrieben werden,
als key immer eine unique ID und der value dann das Objekt,

da stellt sich mir auch die Frage, ob vielleicht das der Grund für das Speicherproblem sein könnte,
dass man ein leeres Dictionary initialisiert und anschliessend der Loop das ganze vergrössert so das Python
immer wieder neue Speicherbereiche belegt um das ganze abzulegen und die freien kleinen Bereiche nicht neu belegt,
aber das ist wahrscheinlich zu einfach gedacht,

Ich musste mich erst einmal etwas bzgl Pickle und Shelve belesen,
das hört sich recht gut an, wobei ich noch nicht ganz verstanden hab, wie komplex die abzuspeichernden Objekte sein dürfen,
also welche Tiefe sie haben dürfen (Objekt im Objekt im Objekt z.B.).

Wie ist das persistent gemeint ?
Dadurch das man auf Platte schreibt sind die Daten doch statisch, oder?
BlackJack

@ts7343: Mit persistent ist gemeint ob die Daten dauerhaft gespeichert werden müssen. In dem Fall würde ich pickle/shelve nicht empfehlen. Wenn es dagegen nur im das auslagern während eines Programmlaufs oder einen Cache geht der auch gelöscht werden kann, dann ist pickle brauchbar.

Grenzen für die Komplexität gibt's nicht wirklich, ausser natürlich dem Wertebereich von `size_t` und RAM. Aber es kann nicht alles ge`pickle`d werden. So Sachen wie Dateiobjekte beispielsweise nicht, weil da ja nicht nur das was sich im Speicher unter der Kontrolle von Python befindet eine Rolle spielt, sondern auch was das Betriebssystem für ein Dateiobjekt speichert, wo man aber nicht dran kommt.
Benutzeravatar
ts7343
User
Beiträge: 69
Registriert: Mittwoch 5. Mai 2010, 13:48

Hallo,

sorry für den langen Code, so richtig viel kleiner hab ich ihn nicht bekommen, ich hoffe, das ist noch zumutbar,

ich hab das ganze mal so verändert, dass die Daten mit shelve gespeichert werden,
und nur gelesen werden, wenn eine Methode des Objekts darauf zugreift,
dann bleiben sie im Memory, bis es ein clean_up_memory gibt, wo sie wieder mit shelve gespeichert werden.

Nun hab ich leider das Problem, dass ich in jeder Methode abfragen muss, ob das Objekt im Memory ist, oder nicht.
z.b.
add_data_to_data_a
get_len_data_a
bei vielen Methoden wird das recht nervig, das jedesmal abzufragen, habt ihr da eine andere Idee,
wie man das aufsetzen kann, weil irgendwo muss ich ja fragen.


Ist das memory clean up so anständig aufgesetzt, oder sollte man das anders machen?
Und ist somit der Speicher wirklich wieder leer?

Code: Alles auswählen

#!/usr/bin/python

import shelve, os

class AllData:
   def __init__(self):
      self.data_a = DataA()
      self.data_b = DataB()
      try:    os.remove("temp_file.slv")
      except: pass
      sh = shelve.open("temp_file.slv")
      sh["data_a"] = self.data_a
      sh["data_b"] = self.data_b
      sh.close()
      self.data_a.set_in_memory(False)
      self.data_b.set_in_memory(False)
   def clean_up_memory(self):
      sh = shelve.open("temp_file.slv")
      sh["data_a"] = self.data_a
      sh["data_b"] = self.data_b
      sh.close()      
      self.data_a.set_in_memory(False)
      self.data_b.set_in_memory(False)
      self.data_a = DataA()
      self.data_b = DataB()
   def debug_printout(self):
      return self.data_a.get_len_data_a("CURRENT"),self.data_b.get_len_data_b("CURRENT")
# -------------------------------------------------------------------------
class DataA:
   def __init__(self):
      self.data_dict = {}
      self.data_list = []  
      self.in_memory = False 
   def get_data_into_memory(self):
      sh = shelve.open("temp_file.slv")
      self.data_dict = sh["data_a"].data_dict
      self.data_list = sh["data_a"].data_list
      sh.close()
      self.set_in_memory(True)
   def add_data_to_data_a(self, id, data_obj):
      if self.in_memory == False:
         self.get_data_into_memory()
      self.data_dict[id] = data_obj 
      self.data_list.append(data_obj)
   def set_in_memory(self, value):
      self.in_memory = value
   def get_len_data_a(self, option):
      return_value = 0
      if option == "STORED":
         if self.in_memory == False:
            self.get_data_into_memory()
         return_value = len(self.data_dict)
      if option == "CURRENT":
         return_value = len(self.data_dict)
      return return_value
   
class ObjA:
   def __init__(self, name):                  
      self.name = name
      self.renum = {}
   def set_renum(self, key, value):
      self.renum[key] = value
# -------------------------------------------------------------------------      
class DataB:
   def __init__(self):
      self.data_dict = {}
      self.data_list = []                 
      self.in_memory = False 
   def get_data_into_memory(self):
      sh = shelve.open("temp_file.slv")
      self.data_dict = sh["data_b"].data_dict
      self.data_list = sh["data_b"].data_list
      sh.close()
      self.set_in_memory(True)
   def add_data_to_data_b(self, id, data_obj):  
      if self.in_memory == False:
         self.get_data_into_memory()
      self.data_dict[id] = data_obj 
      self.data_list.append(data_obj)
   def set_in_memory(self, value):
      self.in_memory = value
   def get_len_data_b(self, option):
      return_value = 0
      if option == "STORED":
         if self.in_memory == False:
            self.get_data_into_memory()
         return_value = len(self.data_dict)
      if option == "CURRENT":
         return_value = len(self.data_dict)
      return return_value

      
class ObjB:
   def __init__(self, any_id):      self.any_id = any_id
   def get_id(self):                return self.any_id
# -------------------------------------------------------------------------
def ctrl_a(all_data):  
   create_a_objects(all_data)
def ctrl_b(all_data):  
   create_b_objects(all_data)
# -------------------------------------------------------------------------
def create_a_objects(all_data):
   inst_a = ObjA("name_01")
   inst_a.set_renum(1,11)
   all_data.data_a.add_data_to_data_a(1,inst_a)
def create_b_objects(all_data):
   for i in range(5): 
      if i < 2:        inst_b = ObjB(i)
      if i >= 2:       inst_b = ObjB( all_data.data_a.get_len_data_a("STORED")*100 + i )
      print inst_b.get_id()
      all_data.data_b.add_data_to_data_b(i,inst_b)
# --- main ----------------------------------------------------------------
all_data = AllData()
print "after init: ",     all_data.debug_printout()
ctrl_a(all_data)
print "after a: ",        all_data.debug_printout()
all_data.clean_up_memory()
print "memory cleaned: ", all_data.debug_printout()
ctrl_b(all_data)
print "after b: ",        all_data.debug_printout()
all_data.clean_up_memory()
print "memory cleaned: ", all_data.debug_printout()
# -------------------------------------------------------------------------
BlackJack

@ts7343: Ich würde so etwas gar nicht machen. Das läuft ja auf manuelle Speicherverwaltung hinaus und das wäre mir dann viel zu fehleranfällig. Wenn Du mehr Speicher brauchst, kauf mehr Speicher. Wenn das nicht hilft oder geht, dann muss man das IMHO umschreiben.
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

@ts7343: das löst ja Dein eigentliches Problem ja nicht, dass Du nicht weißt, wo welche Objekte verwendet werden, oder noch im Speicher sind, obwohl man sie nicht braucht. Generell würde man eine Storage-Klasse schreiben, in die man beliebige Objekte stecken kann, die automatisch auf die Platte geschrieben werden, wenn der Speicher knapp wird. Dazu muß aber jeder Zugriff auf solch ein Objekt über eine Methode dieser Storage-Klasse erfolgen. Das Interface kann man ja List- oder Wörterbuch-ähnlich machen, so dass die meisten Anwendungsfälle abgedeckt sein sollten.
BlackJack

@Sirius3: Wobei ich jetzt nicht wüsste wie man erkennen kann ob der Speicher knapp wird. Diese Grenze wird man selbst vorgeben müssen, und hoffen das sie nicht zu hoch angesetzt ist. Ausserdem würde ich nicht etwas selber implementieren wollen was es bereits gibt. ZODB beispielsweise.

@ts7343: Auf der anderen Seite könnte es auch einfacher lösbar sein wenn die Daten von den einzelnen Arbeitsschritten in den Arbeitsspeicher passen. Und man müsste wissen wie auf die Daten zugegriffen wird, was verändert wird und was nicht.

Dein Programm hat übrigens das Problem das ja letztlich doch wieder alle Daten im Speicher sind. Ich hatte das eher auf Objektebene gedacht, nicht auf dataset-Ebene.
Benutzeravatar
ts7343
User
Beiträge: 69
Registriert: Mittwoch 5. Mai 2010, 13:48

@Sirius3:
Ich hab etwas über so eine Storage Klasse nachgedacht, aber noch nicht so richtig eine Idee wir man das am besten aufsetzen könnte,
ist diese übergeordnet oder wird die vererbt, oder arbeitet die völlig unabhängig von den anderen Klassen in meinem Beispiel.

@BlackJack:
ja, das Beispiel ist komplett im Speicher, ist aber nur der Einfachheit halber, eigentlich gibt es mehrere solcher Zwischenklassen,
die dann manchmal nicht gebraucht werden.
Auf meiner Maschine mit 16G RAM (Dell Precision M4800) unter Win7 stirbt Python bei 2GB laut Taskmanager, obwohl ich 16G Virtual Mem hab.
Hab dann mal unter Suse gebootet und da braucht der Job 3.7GB aber läuft durch.
Somit hätte ich erst einmal etwas lauffähiges, wenn ich unter Linux bleibe, würde aber trotzdem irgendetwas mit Platte schreiben aufsetzen.

Also wenn ihr noch ein paar Tipps für ein einfach gehaltenes Storage Objekt habt.

Vielen Dank auf alle Fälle für den Input
BlackJack

@ts7343: Das klingt nach 32-Bit bei der Win7-Maschine. Da ist dann egal wie viel RAM der Rechner hat, ein Prozess kann trotzdem nicht mehr als 2 GiB davon auf einmal adressieren. Wenn also das Win7 nicht 32-Bit ist, dann könnte hier ein Python mit 64-Bit helfen den ganzen Speicher auch tatsächlich zu nutzen.
Benutzeravatar
ts7343
User
Beiträge: 69
Registriert: Mittwoch 5. Mai 2010, 13:48

du hast recht, ich hab es mit: struct.calcsize("P") * 8 gecheckt,
eine 32bit Python Version unter Windows,

Vielen Dank erst einmal,

ich werd bei 3.7GB mem usage trotzdem mal schauen, ob ich irgendwie etwas mit spill logic mache
Antworten