On 2012-03-27, Stefan Behnel wrote:
Bernd Nawothnig, 27.03.2012 17:03:
Ein Pythonprogramm ruft eine in C geschriebene Extension mit einem Parameter auf, der aus einer in ein numpy.array konvertierten Python-Liste von Objekten besteht. Das C-Programm nutzt dazu einen Zeiger auf dieses nun eng gepackte Array von Zeigern und wählt durch einen geeigneten Algorithmus aus dieser Objektmenge einige aus, die nun in eine Python-Liste von Python-Listen von Objekten gekapselt zurückgegeben werden. Freigegeben oder verändert wird keines der beim Aufruf der Extension übergebenen Objekte. Die Python-Ergebnisliste wird in C so konstruiert: ... PyObject *r = PyList_New(0); ... static PyObject *VLP_asPyList(VectorlistP *vl) { PyObject *result = PyList_New(vl->length); int i;
for (i=0; i<vl->length; i++) PyList_SetItem(result, i, Py_BuildValue("O", vl->list[i]->anomaly)); // ^^^^^^^^^^^^^^^^^^^^ // Zeiger auf eins der ursprünglichen Objekte return result; } ... PyList_Append(r, VLP_asPyList(&bestResult));
Hier war ein "extend()" gemeint, oder?
Nein, ein PyList_Append, also entsprechend einem python <list>.append().
return r;
Zum Vergleich der (mangels umgebendem Code und Daten natürlich ungetestete) äquivalente Code in Cython:
return [ item.anomaly for item in bestResult.list[:bestResult.length] ]
Finde ich hübscher und übersichtlicher.
Im reinen C-Teil verwende ich ja absichtlich _nicht_ die Python-Datenstrukturen, weil ich eben schneller werden will (was auch weit über alle Erwartungen geklappt hat ;-). Gerade die dauernden appends, bzw. auch Listcomprehensions, wie Du sie erwähntest (ich weiß, die sind sehr elegant und ich verwende die auch sehr gerne), holen sich jedes Mal Speicher vom Heap, der praktisch dann gleich wieder freigegeben werden kann, während der mir sehr wohl bekannte wirkliche Speicherverbrauch dieses Algorithmus _sehr_ überschaubar ist. Entsprechend dramatisch war dann auch der Speedup (mehr als Faktor 1000). static PyObject *VLP_asPyList(VectorlistP *vl) macht aus meiner C-Variante ganz zum Schluss erst(!) Python, weil ich das Resultat ja schließlich und letztendlich in Python haben möchte. Aber viel mehr würde mich interessieren, ob PyList_SetItem und PyList_Append automatisch den Referenzzähler der eingefügten, bzw. zugewiesenen Objekte erhöhen. Ich vermute es stark, und die Extension funktioniert auch problemlos, was aber ja keineswegs eine Garantie auf Korrektheit ist, weil der GC asynchron zuschlagen kann (und dann müssen die Referenzzähler korrekt gesetzt sein). Und deswegen wüsste ich es gerne explizit. Kann mir da einer was zu sagen? Bernd -- "Die Antisemiten vergeben es den Juden nicht, dass die Juden Geist haben - und Geld." [Friedrich Nietzsche]
Am 27.03.2012 20:29, schrieb Bernd Nawothnig:
Aber viel mehr würde mich interessieren, ob PyList_SetItem und PyList_Append automatisch den Referenzzähler der eingefügten, bzw. zugewiesenen Objekte erhöhen. Ich vermute es stark, und die Extension funktioniert auch problemlos, was aber ja keineswegs eine Garantie auf Korrektheit ist, weil der GC asynchron zuschlagen kann (und dann müssen die Referenzzähler korrekt gesetzt sein). Und deswegen wüsste ich es gerne explizit.
Kann mir da einer was zu sagen? Siehe http://docs.python.org/c-api/list.html. PyList_SetItem stiehlt die Referenz, was hier aber OK ist, da Py_BuildValue ein neue Referenz liefert und auch die Referenz auf das ursprüngliche Objekt erhöht. PyList_Append erhöht den Zähler, so daß du den Count auf den Rückgabewert von VLP_asPyList selbst reduzieren musst. Aktuell hast du also ein Speicherleck.
Wenn du zuerst eine Liste mit der richtigen Länge anlegst und dann alles mit PyList_SetItem füllst, wirds auch noch ein bischen schneller. Der Grund für das abweichende Verhalten von PyList_SetItem steht hier: http://docs.python.org/c-api/intro.html#objects-types-and-reference-counts Few functions steal references; the two notable exceptions are PyList_SetItem() and PyTuple_SetItem(), which steal a reference to the item (but not to the tuple or list into which the item is put!). These functions were designed to steal a reference because of a common idiom for populating a tuple or list with newly created objects; for example, the code to create the tuple (1, 2, "three") could look like this (forgetting about error handling for the moment; a better way to code this is shown below): Gruß, Dietmar
Bernd Nawothnig, 27.03.2012 20:29:
On 2012-03-27, Stefan Behnel wrote:
Bernd Nawothnig, 27.03.2012 17:03:
Ein Pythonprogramm ruft eine in C geschriebene Extension mit einem Parameter auf, der aus einer in ein numpy.array konvertierten Python-Liste von Objekten besteht. Das C-Programm nutzt dazu einen Zeiger auf dieses nun eng gepackte Array von Zeigern und wählt durch einen geeigneten Algorithmus aus dieser Objektmenge einige aus, die nun in eine Python-Liste von Python-Listen von Objekten gekapselt zurückgegeben werden. Freigegeben oder verändert wird keines der beim Aufruf der Extension übergebenen Objekte. Die Python-Ergebnisliste wird in C so konstruiert: ... PyObject *r = PyList_New(0); ... static PyObject *VLP_asPyList(VectorlistP *vl) { PyObject *result = PyList_New(vl->length); int i;
for (i=0; i<vl->length; i++) PyList_SetItem(result, i, Py_BuildValue("O", vl->list[i]->anomaly)); // ^^^^^^^^^^^^^^^^^^^^ // Zeiger auf eins der ursprünglichen Objekte return result; } ... PyList_Append(r, VLP_asPyList(&bestResult));
Hier war ein "extend()" gemeint, oder?
Nein, ein PyList_Append, also entsprechend einem python <list>.append().
return r;
Zum Vergleich der (mangels umgebendem Code und Daten natürlich ungetestete) äquivalente Code in Cython:
return [ item.anomaly for item in bestResult.list[:bestResult.length] ]
Finde ich hübscher und übersichtlicher.
Im reinen C-Teil verwende ich ja absichtlich _nicht_ die Python-Datenstrukturen
Hatte ich auch so verstanden. Der Code oben (deiner und meiner) dient nur der Konvertierung des Ergebnisses von C nach Python. Der Code, den Cython generiert, hat nur minimal höheren Aufwand als dein handgeschriebener - er erzeugt die ursprüngliche Liste nämlich momentan nicht mit der finalen Länge. Und das auch nur deshalb, weil ich immer noch keine Zeit gefunden habe, die bekannte Länge der Ergebnisliste aus dem Iterator durchzuschleifen. Sprich: mit ein bisschen zusätzlichem Aufwand würde Cython sogar identischen Code generieren, ohne deinen ganzen C Eiertanz oben. Allerdings würde es mich überraschen, wenn der Geschwindigkeitsunterschied jetzt schon sehr auffällig wäre. Der eher meistens vernachlässigbar, weil die Konvertierung der Listeneinträge normalerweise deutlich stärker zu Buche schlägt als die Vergößerung der Liste, die meistens ohnehin von der Speicherverwaltung aufgefangen wird.
, weil ich eben schneller werden will (was auch weit über alle Erwartungen geklappt hat ;-). Gerade die dauernden appends, bzw. auch Listcomprehensions, wie Du sie erwähntest (ich weiß, die sind sehr elegant und ich verwende die auch sehr gerne), holen sich jedes Mal Speicher vom Heap, der praktisch dann gleich wieder freigegeben werden kann
Äh, bitte wie? Warum sollten sie das denn tun?
während der mir sehr wohl bekannte wirkliche Speicherverbrauch dieses Algorithmus _sehr_ überschaubar ist. Entsprechend dramatisch war dann auch der Speedup (mehr als Faktor 1000).
Aber nur gegenüber CPython, richtig?
static PyObject *VLP_asPyList(VectorlistP *vl)
macht aus meiner C-Variante ganz zum Schluss erst(!) Python, weil ich das Resultat ja schließlich und letztendlich in Python haben möchte.
Ja, das ist das übliche Vorgehen. Deshalb haben wir uns mit unseren beiden Implementierungen ja auch auf den Teil beschränkt.
Aber viel mehr würde mich interessieren, ob PyList_SetItem und PyList_Append automatisch den Referenzzähler der eingefügten, bzw. zugewiesenen Objekte erhöhen.
Schau in die Doku (wie Dietmar es für dich getan hat). Unnötig zu sagen, dass Cython dir diese Details abnimmt. Stefan
On 2012-03-27, Dietmar Schwertberger wrote:
Siehe http://docs.python.org/c-api/list.html. PyList_SetItem stiehlt die Referenz, was hier aber OK ist, da Py_BuildValue ein neue Referenz liefert und auch die Referenz auf das ursprüngliche Objekt erhöht. PyList_Append erhöht den Zähler, so daß du den Count auf den Rückgabewert von VLP_asPyList selbst reduzieren musst. Aktuell hast du also ein Speicherleck.
Ah, verstanden, gefixt und Danke für die Erklärung.
Wenn du zuerst eine Liste mit der richtigen Länge anlegst und dann alles mit PyList_SetItem füllst, wirds auch noch ein bischen schneller.
Die inneren Listen erstelle ich bereits so, aber bei der äußeren würde das den C-Code unnötig verkomplizieren, denn ich kenne die wirkliche Länge erst ganz zum Schluss. Und auch das, was ich dann in C entsprechend machen müsste, wäre natürlich nicht kostenfrei. Nein, das ist unter dem Strich so schon die sauberste und auch naheliegendste Lösung. Bernd -- "Die Antisemiten vergeben es den Juden nicht, dass die Juden Geist haben - und Geld." [Friedrich Nietzsche]
Dietmar Schwertberger, 28.03.2012 00:58:
Wenn du zuerst eine Liste mit der richtigen Länge anlegst und dann alles mit PyList_SetItem füllst, wirds auch noch ein bischen schneller.
Mit Betonung auf "bisschen", zumindest unter Linux. Unter Windows (und wohl auch Solaris) scheint die Speicherverwaltung so schlecht zu sein, dass es da merkliche Unterschiede geben kann, aber auf vernünftigen Betriebsystemen stellt das realloc() für das Listenarray unter normalen "genug Speicher da" Bedingungen kein Problem dar. Und Python's eigene Speicherverwaltung schlägt da ja auch nochmal zu und mildert das Problem insbesondere unter Windows merklich ab. Mir ist es zumindest nicht gelungen, einen wirklichen Unterschied zwischen einer List-Comprehension in Cython und dem C-API-Code mit PyList_SetItem() zu messen, zumindest wenn das Erstellen der Objekte auch nur annähernd nicht-trivial ist. In Einzelfällen mag es einen Unterschied machen, aber da würde ich erstmal abwarten, ob jetzt genau das auch wirklich ein Problem in meinem Programm darstellt. Stefan
participants (3)
-
Bernd Nawothnig
-
Dietmar Schwertberger
-
Stefan Behnel