Ich biete einige Benchmarking-Ergebnisse an, in denen die wichtigsten bisher vorgestellten Ansätze verglichen werden, nämlich @ bobince's findnth()
(basierend auf str.split()
) mit @ tgamblin's oder @Mark Byers find_nth()
(basierend auf str.find()
). Ich werde auch mit einer C-Erweiterung ( _find_nth.so
) vergleichen, um zu sehen, wie schnell wir gehen können. Hier ist find_nth.py
:
def findnth(haystack, needle, n):
parts= haystack.split(needle, n+1)
if len(parts)<=n+1:
return -1
return len(haystack)-len(parts[-1])-len(needle)
def find_nth(s, x, n=0, overlap=False):
l = 1 if overlap else len(x)
i = -l
for c in xrange(n + 1):
i = s.find(x, i + l)
if i < 0:
break
return i
Natürlich ist die Leistung am wichtigsten, wenn die Zeichenfolge groß ist. Nehmen wir also an, wir möchten die 1000001. erste Zeile ('\ n') in einer 1,3-GB-Datei namens 'bigfile' finden. Um Speicherplatz zu sparen, möchten wir an einer mmap.mmap
Objektdarstellung der Datei arbeiten:
In [1]: import _find_nth, find_nth, mmap
In [2]: f = open('bigfile', 'r')
In [3]: mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
Es gibt bereits das erste Problem mit findnth()
, da mmap.mmap
Objekte nicht unterstützen split()
. Wir müssen also tatsächlich die gesamte Datei in den Speicher kopieren:
In [4]: %time s = mm[:]
CPU times: user 813 ms, sys: 3.25 s, total: 4.06 s
Wall time: 17.7 s
Autsch! Zum Glück s
passt immer noch in die 4 GB Speicher meines Macbook Air. Lassen Sie uns also einen Benchmark erstellen findnth()
:
In [5]: %timeit find_nth.findnth(s, '\n', 1000000)
1 loops, best of 3: 29.9 s per loop
Offensichtlich eine schreckliche Leistung. Mal sehen, wie der darauf basierende Ansatz str.find()
funktioniert:
In [6]: %timeit find_nth.find_nth(s, '\n', 1000000)
1 loops, best of 3: 774 ms per loop
Viel besser! findnth()
Das Problem ist natürlich, dass es gezwungen ist, die Zeichenfolge während zu kopieren. Dies split()
ist bereits das zweite Mal, dass wir die 1,3 GB Daten danach kopieren s = mm[:]
. Hier kommt der zweite Vorteil von find_nth()
: Wir können es mm
direkt verwenden, so dass keine Kopien der Datei erforderlich sind:
In [7]: %timeit find_nth.find_nth(mm, '\n', 1000000)
1 loops, best of 3: 1.21 s per loop
Es scheint eine kleine Leistungseinbuße Betriebs auf sein mm
gegenüber s
, aber dies zeigt , dass find_nth()
uns eine Antwort in 1,2 s im Vergleich zu bekommen findnth
‚s insgesamt 47 s.
Ich fand keine Fälle, in denen der str.find()
basierte Ansatz signifikant schlechter war als derstr.split()
basierte Ansatz, daher würde ich an dieser Stelle argumentieren, dass die Antwort von @ tgamblin oder @ Mark Byers anstelle der von @ bobince akzeptiert werden sollte.
In meinen Tests war die find_nth()
obige Version die schnellste reine Python-Lösung, die ich finden konnte (sehr ähnlich der Version von @Mark Byers). Mal sehen, wie viel besser wir mit einem C-Erweiterungsmodul arbeiten können. Hier ist _find_nthmodule.c
:
#include <Python.h>
#include <string.h>
off_t _find_nth(const char *buf, size_t l, char c, int n) {
off_t i;
for (i = 0; i < l; ++i) {
if (buf[i] == c && n-- == 0) {
return i;
}
}
return -1;
}
off_t _find_nth2(const char *buf, size_t l, char c, int n) {
const char *b = buf - 1;
do {
b = memchr(b + 1, c, l);
if (!b) return -1;
} while (n--);
return b - buf;
}
/* mmap_object is private in mmapmodule.c - replicate beginning here */
typedef struct {
PyObject_HEAD
char *data;
size_t size;
} mmap_object;
typedef struct {
const char *s;
size_t l;
char c;
int n;
} params;
int parse_args(PyObject *args, params *P) {
PyObject *obj;
const char *x;
if (!PyArg_ParseTuple(args, "Osi", &obj, &x, &P->n)) {
return 1;
}
PyTypeObject *type = Py_TYPE(obj);
if (type == &PyString_Type) {
P->s = PyString_AS_STRING(obj);
P->l = PyString_GET_SIZE(obj);
} else if (!strcmp(type->tp_name, "mmap.mmap")) {
mmap_object *m_obj = (mmap_object*) obj;
P->s = m_obj->data;
P->l = m_obj->size;
} else {
PyErr_SetString(PyExc_TypeError, "Cannot obtain char * from argument 0");
return 1;
}
P->c = x[0];
return 0;
}
static PyObject* py_find_nth(PyObject *self, PyObject *args) {
params P;
if (!parse_args(args, &P)) {
return Py_BuildValue("i", _find_nth(P.s, P.l, P.c, P.n));
} else {
return NULL;
}
}
static PyObject* py_find_nth2(PyObject *self, PyObject *args) {
params P;
if (!parse_args(args, &P)) {
return Py_BuildValue("i", _find_nth2(P.s, P.l, P.c, P.n));
} else {
return NULL;
}
}
static PyMethodDef methods[] = {
{"find_nth", py_find_nth, METH_VARARGS, ""},
{"find_nth2", py_find_nth2, METH_VARARGS, ""},
{0}
};
PyMODINIT_FUNC init_find_nth(void) {
Py_InitModule("_find_nth", methods);
}
Hier ist die setup.py
Datei:
from distutils.core import setup, Extension
module = Extension('_find_nth', sources=['_find_nthmodule.c'])
setup(ext_modules=[module])
Installieren Sie wie gewohnt mit python setup.py install
. Der C-Code spielt hier eine Rolle, da er sich darauf beschränkt, einzelne Zeichen zu finden. Mal sehen, wie schnell dies geht:
In [8]: %timeit _find_nth.find_nth(mm, '\n', 1000000)
1 loops, best of 3: 218 ms per loop
In [9]: %timeit _find_nth.find_nth(s, '\n', 1000000)
1 loops, best of 3: 216 ms per loop
In [10]: %timeit _find_nth.find_nth2(mm, '\n', 1000000)
1 loops, best of 3: 307 ms per loop
In [11]: %timeit _find_nth.find_nth2(s, '\n', 1000000)
1 loops, best of 3: 304 ms per loop
Klar noch ein bisschen schneller. Interessanterweise gibt es auf C-Ebene keinen Unterschied zwischen In-Memory- und Mmapped-Fällen. Es ist auch interessant zu sehen, dass das _find_nth2()
, was auf string.h
der memchr()
Bibliotheksfunktion basiert , gegen die unkomplizierte Implementierung in verliert _find_nth()
: Die zusätzlichen "Optimierungen" in memchr()
sind anscheinend nach hinten los ...
Zusammenfassend ist die Implementierung in findnth()
(basierend auf str.split()
) wirklich eine schlechte Idee, da (a) sie aufgrund des erforderlichen Kopierens für größere Zeichenfolgen eine schreckliche Leistung erbringt und (b) überhaupt nicht für mmap.mmap
Objekte funktioniert . Die Implementierung in find_nth()
(basierend auf str.find()
) sollte unter allen Umständen bevorzugt werden (und daher die akzeptierte Antwort auf diese Frage sein).
Es gibt noch viel Raum für Verbesserungen, da die C-Erweiterung fast um den Faktor 4 schneller lief als der reine Python-Code, was darauf hinweist, dass möglicherweise eine dedizierte Python-Bibliotheksfunktion vorliegt.