Je propose des résultats d'analyse comparative comparant les approches les plus importantes présentées jusqu'à présent, à savoir @ bobince findnth()
(basé sur str.split()
) vs @ tgamblin's ou @Mark Byers find_nth()
(basé sur str.find()
). Je vais également comparer avec une extension C ( _find_nth.so
) pour voir à quelle vitesse nous pouvons aller. Voici 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
Bien sûr, les performances sont plus importantes si la chaîne est volumineuse, alors supposons que nous souhaitons trouver la 1000001e nouvelle ligne ('\ n') dans un fichier de 1,3 Go appelé 'bigfile'. Pour économiser de la mémoire, nous aimerions travailler sur une mmap.mmap
représentation objet du fichier:
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)
Il y a déjà le premier problème avec findnth()
, puisque les mmap.mmap
objets ne prennent pas en charge split()
. Nous devons donc copier tout le fichier en mémoire:
In [4]: %time s = mm[:]
CPU times: user 813 ms, sys: 3.25 s, total: 4.06 s
Wall time: 17.7 s
Aie! Heureusement, s
tient toujours dans les 4 Go de mémoire de mon Macbook Air, alors comparons-nous findnth()
:
In [5]: %timeit find_nth.findnth(s, '\n', 1000000)
1 loops, best of 3: 29.9 s per loop
Clairement une performance terrible. Voyons comment l'approche basée sur str.find()
fait:
In [6]: %timeit find_nth.find_nth(s, '\n', 1000000)
1 loops, best of 3: 774 ms per loop
Bien mieux! De toute évidence, findnth()
le problème est qu'il est obligé de copier la chaîne pendant split()
, ce qui est déjà la deuxième fois que nous copions les 1,3 Go de données après s = mm[:]
. Voici le deuxième avantage de find_nth()
: Nous pouvons l'utiliser mm
directement, de sorte qu'aucune copie du fichier ne soit requise:
In [7]: %timeit find_nth.find_nth(mm, '\n', 1000000)
1 loops, best of 3: 1.21 s per loop
Il semble y avoir une petite pénalité de performance opérant sur mm
vs. s
, mais cela montre que cela find_nth()
peut nous donner une réponse en 1,2 s par rapport au findnth
total de 47 s.
Je n'ai trouvé aucun cas où l' str.find()
approche basée était significativement pire que l' str.split()
approche basée, donc à ce stade, je dirais que la réponse de @ tgamblin ou de @Mark Byers devrait être acceptée au lieu de celle de @ bobince.
Lors de mes tests, la version find_nth()
ci - dessus était la solution pure Python la plus rapide que je pouvais proposer (très similaire à la version de @Mark Byers). Voyons ce que nous pouvons faire de mieux avec un module d'extension C. Voici _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);
}
Voici le setup.py
fichier:
from distutils.core import setup, Extension
module = Extension('_find_nth', sources=['_find_nthmodule.c'])
setup(ext_modules=[module])
Installez comme d'habitude avec python setup.py install
. Le code C joue ici un avantage puisqu'il se limite à trouver des caractères uniques, mais voyons à quelle vitesse cela est:
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
Clairement encore un peu plus rapide. Fait intéressant, il n'y a aucune différence au niveau C entre les cas en mémoire et mmappés. Il est également intéressant de voir que _find_nth2()
, qui est basé sur string.h
la memchr()
fonction de bibliothèque de 's , perd contre la simple implémentation dans _find_nth()
: Les "optimisations" supplémentaires dans memchr()
sont apparemment contre-productives ...
En conclusion, l'implémentation dans findnth()
(basée sur str.split()
) est vraiment une mauvaise idée, car (a) elle fonctionne terriblement pour des chaînes plus grandes en raison de la copie requise, et (b) elle ne fonctionne pas du tout sur les mmap.mmap
objets. La mise en œuvre dans find_nth()
(basée sur str.find()
) doit être préférée en toutes circonstances (et donc être la réponse acceptée à cette question).
Il reste encore beaucoup à faire, car l'extension C a fonctionné presque 4 fois plus vite que le code Python pur, ce qui indique qu'il pourrait y avoir un cas pour une fonction de bibliothèque Python dédiée.