Cython: Lässt sich hier noch etwas rausholen

Python in C/C++ embedden, C-Module, ctypes, Cython, SWIG, SIP etc sind hier richtig.
Antworten
patrice079
User
Beiträge: 13
Registriert: Sonntag 15. April 2012, 13:22

Hallo ich habe eine Frage zu einem Problem das sich durch alle meine Cython Funktionen zieht und hoffe mir kann jemand helfen. Mir wäre auch schon geholfen wenn mir jemand sagen könnte das es sich bezogen auf die Performance nicht lohnt eine Lösung zu finden.

Eine Beispiel Dummy-Funktion ist:

Code: Alles auswählen

@cython.cdivision(True)
@cython.boundscheck(False)
@cython.wraparound(False)		
cpdef foo4(np.ndarray[DTYPE_t, ndim=2] arr, np.ndarray[DTYPE_t, ndim=1] tmp, np.ndarray[DTYPE_t, ndim=2] result):
	cdef index_t y,x,i
	cdef int sy = arr.shape[0]
	cdef int sx = arr.shape[1]
	cdef float val = 0.0
  
	for y in range(sy):
		for x in range(sx):
			if x<sx-20:
				for i in range(20):
					tmp[<unsigned int>i] = arr[<unsigned int>y,<unsigned int>(x+i)]
				val = 0
				for i in range(20):
					val = val+tmp[<unsigned int>i]
				val = val/20.0
				result[<unsigned int>y,<unsigned int>x] = val
			else:
				result[<unsigned int>y,<unsigned int>x] = arr[<unsigned int>y,<unsigned int>x]
Die Html Ausgabe via cython -a meckert immer bei der letzten Zeile von Methoden dieser Art:

103: result[<unsigned int>y,<unsigned int>x] = arr[<unsigned int>y,<unsigned int>x]
/* "code.pyx":103
* result[<unsigned int>y,<unsigned int>x] = val
* else:
* result[<unsigned int>y,<unsigned int>x] = arr[<unsigned int>y,<unsigned int>x] # <<<<<<<<<<<<<<
*
*/
__pyx_t_13 = ((unsigned int)__pyx_v_y);
__pyx_t_14 = ((unsigned int)__pyx_v_x);
__pyx_t_15 = ((unsigned int)__pyx_v_y);
__pyx_t_16 = ((unsigned int)__pyx_v_x);
*__Pyx_BufPtrStrided2d(__pyx_t_4code_DTYPE_t *, __pyx_pybuffernd_result.rcbuffer->pybuffer.buf, __pyx_t_15, __pyx_pybuffernd_result.diminfo[0].strides, __pyx_t_16, __pyx_pybuffernd_result.diminfo[1].strides) = (*__Pyx_BufPtrStrided2d(__pyx_t_4code_DTYPE_t *, __pyx_pybuffernd_arr.rcbuffer->pybuffer.buf, __pyx_t_13, __pyx_pybuffernd_arr.diminfo[0].strides, __pyx_t_14, __pyx_pybuffernd_arr.diminfo[1].strides));
}
__pyx_L7:;
}
}

__pyx_r = Py_None; __Pyx_INCREF(Py_None);
goto __pyx_L0;
__pyx_L1_error:;
{ PyObject *__pyx_type, *__pyx_value, *__pyx_tb;
__Pyx_ErrFetch(&__pyx_type, &__pyx_value, &__pyx_tb);
__Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_arr.rcbuffer->pybuffer);
__Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_result.rcbuffer->pybuffer);
__Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_tmp.rcbuffer->pybuffer);
__Pyx_ErrRestore(__pyx_type, __pyx_value, __pyx_tb);}
__Pyx_AddTraceback("code.foo4", __pyx_clineno, __pyx_lineno, __pyx_filename);
__pyx_r = 0;
goto __pyx_L2;
__pyx_L0:;
__Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_arr.rcbuffer->pybuffer);
__Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_result.rcbuffer->pybuffer);
__Pyx_SafeReleaseBuffer(&__pyx_pybuffernd_tmp.rcbuffer->pybuffer);
__pyx_L2:;
__Pyx_XGIVEREF(__pyx_r);
__Pyx_RefNannyFinishContext();
return __pyx_r;
}

/* Python wrapper */
static PyObject *__pyx_pw_4code_7foo4(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/
static PyObject *__pyx_pw_4code_7foo4(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {
PyArrayObject *__pyx_v_arr = 0;
PyArrayObject *__pyx_v_tmp = 0;
PyArrayObject *__pyx_v_result = 0;
PyObject *__pyx_r = 0;
__Pyx_RefNannyDeclarations
__Pyx_RefNannySetupContext("foo4 (wrapper)", 0);
{
static PyObject **__pyx_pyargnames[] = {&__pyx_n_s__arr,&__pyx_n_s__tmp,&__pyx_n_s__result,0};
PyObject* values[3] = {0,0,0};
if (unlikely(__pyx_kwds)) {
Py_ssize_t kw_args;
const Py_ssize_t pos_args = PyTuple_GET_SIZE(__pyx_args);
switch (pos_args) {
case 3: values[2] = PyTuple_GET_ITEM(__pyx_args, 2);
case 2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
case 1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
case 0: break;
default: goto __pyx_L5_argtuple_error;
}
kw_args = PyDict_Size(__pyx_kwds);
switch (pos_args) {
case 0:
if (likely((values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__arr)) != 0)) kw_args--;
else goto __pyx_L5_argtuple_error;
case 1:
if (likely((values[1] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__tmp)) != 0)) kw_args--;
else {
__Pyx_RaiseArgtupleInvalid("foo4", 1, 3, 3, 1); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 86; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
}
case 2:
if (likely((values[2] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__result)) != 0)) kw_args--;
else {
__Pyx_RaiseArgtupleInvalid("foo4", 1, 3, 3, 2); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 86; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
}
}
if (unlikely(kw_args > 0)) {
if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, pos_args, "foo4") < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 86; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
}
} else if (PyTuple_GET_SIZE(__pyx_args) != 3) {
goto __pyx_L5_argtuple_error;
} else {
values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
values[2] = PyTuple_GET_ITEM(__pyx_args, 2);
}
__pyx_v_arr = ((PyArrayObject *)values[0]);
__pyx_v_tmp = ((PyArrayObject *)values[1]);
__pyx_v_result = ((PyArrayObject *)values[2]);
}
goto __pyx_L4_argument_unpacking_done;
__pyx_L5_argtuple_error:;
__Pyx_RaiseArgtupleInvalid("foo4", 1, 3, 3, PyTuple_GET_SIZE(__pyx_args)); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 86; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
__pyx_L3_error:;
__Pyx_AddTraceback("code.foo4", __pyx_clineno, __pyx_lineno, __pyx_filename);
__Pyx_RefNannyFinishContext();
return NULL;
__pyx_L4_argument_unpacking_done:;
if (unlikely(!__Pyx_ArgTypeTest(((PyObject *)__pyx_v_arr), __pyx_ptype_5numpy_ndarray, 1, "arr", 0))) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 86; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
if (unlikely(!__Pyx_ArgTypeTest(((PyObject *)__pyx_v_tmp), __pyx_ptype_5numpy_ndarray, 1, "tmp", 0))) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 86; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
if (unlikely(!__Pyx_ArgTypeTest(((PyObject *)__pyx_v_result), __pyx_ptype_5numpy_ndarray, 1, "result", 0))) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 86; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
__pyx_r = __pyx_pf_4code_6foo4(__pyx_self, __pyx_v_arr, __pyx_v_tmp, __pyx_v_result);
goto __pyx_L0;
__pyx_L1_error:;
__pyx_r = NULL;
__pyx_L0:;
__Pyx_RefNannyFinishContext();
return __pyx_r;
BlackJack

@patrice079: Je gelber desto mehr C-Code wurde produziert. Bei der letzten Zeile einer Funktion kommen halt noch die unvermeidlichen Fehlerbehandlungs- und Aufräumaktionen hinzu, die man für Python braucht. Da ist völlig egal was in der letzten Zeile steht. Du kannst Das zum Beispiel ausprobieren in dem Du dem Quelltext noch ein überflüssiges ``i = 0`` am Ende hinzufügst, was von Cython zu einem ``i = 0;`` in C übersetzt wird. Effizienter geht es ja nicht, trotzdem ist diese Zeile gelb hinterlegt.
patrice079
User
Beiträge: 13
Registriert: Sonntag 15. April 2012, 13:22

Danke BlackJack,

ich habe mir sowas schon gedacht, aber die Hoffnung doch noch etwas Speed rausholen zu können stirbt ja schließlich zuletzt.
Benutzeravatar
snafu
User
Beiträge: 6744
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Vorab: Du könntest alternativ mal PyPy ausprobieren und schauen, ob dein Algo damit schneller läuft. Sähe dann weniger "kryptisch" aus. ;)

Falls du bei Cython bleiben möchtest/musst (z.B. weil dir die Anpassungen bzw Überführung zurück in "normales" Python zu aufwändig sind), dann würd ich höchstens in den Ablauf des Algos nach Verbesserungspotenzial gucken (was ja auch mehr oder weniger deine Frage war... ^^).

Auffällig ist zum Beispiel die Verwendung von `range()`. Sofen du Python 2.x verwendest, würde ich das auf jeden Fall in `xrange()` ändern, damit nicht unnötigerweise Listen erstellt werden, an Stellen, wo man nur die Zahlwerte braucht und die Liste danach eh wieder "wegschmeißt".

Die zweite Auffälligkeit ist, dass du zweimal eine zumindest vom Schleifenkopf her identische `for`-Schleife durchläufst. Einmal, um einen temporären Container zu befüllen und beim zweiten Mal, um die in ihm enthaltenen Werte zu addieren. Dafür gibt es in Python `sum()`. Du müsstest die erste Schleife im Prinzip nur in eine Generator-Comprehension umwandeln und kannst diese direkt als Argument an `sum()` übergeben. Sähe dann so aus:

Code: Alles auswählen

sum(arr[y, x + i] for i in xrange(20))
Also ein kompakter Einzeiler, der potenziell auch eine bessere Laufzeit haben dürfte, da alles in einem Schleifendurchlauf (anstatt in zwei Durchläufen) passiert. :)

Ansonsten guck halt auch mal in der NumPy-Doku, ob es da nicht vorgefertigte Methoden für deinen Anwendungsfall gibt. In diesem Beispiel wäre das halt die Summe für ein bestimmtest Intervall an Elementen.

Da du Pseudocode gezeigt hast, weiß man natürlich nicht, inwiefern dies deinem tatsächlichen Code entspricht und ergo, wie nützlich meine Anmerkungen für dich überhaupt waren. Wenn du konkrete Vorschläge haben möchtest, dann solltest du auch konkrete Fragen mit *echtem* Code stellen. Das nur nebenbei... ;)

EDIT: Ich hoffe ja mal, ich habe deine Fragestellung jetzt nicht völlig missverstanden. :mrgreen:
Sirius3
User
Beiträge: 17756
Registriert: Sonntag 21. Oktober 2012, 17:20

Im Prinzip läßt sich der Dummy-Code mit numpy auf diese Zeilen reduzieren:

Code: Alles auswählen

def foo4(arr):
    result = np.ndarray(shape=arr.shape)
    result[:,-20:]=arr[:,-20:]
    for x in xrange(result.shape[1]-20):
        result[:,x] = arr[:,x:x+20].sum(1)/20
    return result
Fraglich, ob die letzte Schleife nach C zu übersetzen noch so viel Geschwindigkeit bringt.
patrice079
User
Beiträge: 13
Registriert: Sonntag 15. April 2012, 13:22

@snafu:
Danke für die ausführliche Antwort, aber es ging tatsächlich nur ums Prinzip weil in allen meinen Funktionen die letzte Zeile noch Python Code produziert. Das Beispiel war nur Blödsinn.

Ich habe aber zum Spaß und für zukünftige Leser mal noch schnell die Performance von Sirius3's Vorschlag mit dem maximal optimierten verglichen:

foo4 von Sirius 263 mal schneller als pures Python
foo4 aus meinem ersten Post 613 mal schneller als Python


Danke Euch allen
Antworten