Estoy ofreciendo algunos resultados de evaluación comparativa que comparan los enfoques más destacados presentados hasta ahora, a saber, @ bobince findnth()
(basado en str.split()
) frente a @ tgamblin o @Mark Byers ' find_nth()
(basado en str.find()
). También compararé con una extensión C ( _find_nth.so
) para ver qué tan rápido podemos ir. Aqui esta 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
Por supuesto, el rendimiento es más importante si la cadena es grande, así que suponga que queremos encontrar la línea nueva 1000001 ('\ n') en un archivo de 1.3 GB llamado 'bigfile'. Para ahorrar memoria, nos gustaría trabajar en una mmap.mmap
representación de objeto del archivo:
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)
Ya existe el primer problema con findnth()
, ya que los mmap.mmap
objetos no son compatibles split()
. Entonces, en realidad, tenemos que copiar todo el archivo en la memoria:
In [4]: %time s = mm[:]
CPU times: user 813 ms, sys: 3.25 s, total: 4.06 s
Wall time: 17.7 s
¡Ay! Afortunadamente, s
todavía cabe en los 4 GB de memoria de mi Macbook Air, así que hagamos una evaluación comparativa findnth()
:
In [5]: %timeit find_nth.findnth(s, '\n', 1000000)
1 loops, best of 3: 29.9 s per loop
Claramente una actuación terrible. Veamos cómo funciona el enfoque basado en str.find()
:
In [6]: %timeit find_nth.find_nth(s, '\n', 1000000)
1 loops, best of 3: 774 ms per loop
¡Mucho mejor! Claramente, findnth()
el problema es que se ve obligado a copiar la cadena durante split()
, que ya es la segunda vez que copiamos los 1.3 GB de datos después s = mm[:]
. Aquí entra en juego la segunda ventaja de find_nth()
: Podemos usarlo en mm
forma directa, de tal manera que cero se requieren copias del archivo:
In [7]: %timeit find_nth.find_nth(mm, '\n', 1000000)
1 loops, best of 3: 1.21 s per loop
Parece haber una pequeña penalización de rendimiento operando en mm
vs. s
, pero esto ilustra que find_nth()
puede darnos una respuesta en 1.2 s en comparación con findnth
el total de 47 s.
No encontré casos en los que el str.find()
enfoque basado fuera significativamente peor que el str.split()
enfoque basado, por lo que en este punto, diría que la respuesta de @ tgamblin o @Mark Byers debería aceptarse en lugar de la de @ bobince.
En mis pruebas, la versión find_nth()
anterior fue la solución Python pura más rápida que se me ocurrió (muy similar a la versión de @Mark Byers). Veamos cuánto mejor podemos hacer con un módulo de extensión C. Aqui esta _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);
}
Aquí está el setup.py
archivo:
from distutils.core import setup, Extension
module = Extension('_find_nth', sources=['_find_nthmodule.c'])
setup(ext_modules=[module])
Instale como de costumbre con python setup.py install
. El código C tiene una ventaja aquí, ya que se limita a encontrar caracteres individuales, pero veamos qué tan rápido es esto:
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
Claramente, aún un poco más rápido. Curiosamente, no hay diferencia en el nivel C entre los casos en memoria y mmapeados. También es interesante ver que _find_nth2()
, que se basa en string.h
la memchr()
función de la biblioteca, pierde frente a la implementación sencilla en _find_nth()
: Las "optimizaciones" adicionales en memchr()
aparentemente son contraproducentes ...
En conclusión, la implementación en findnth()
(basada en str.split()
) es realmente una mala idea, ya que (a) funciona terriblemente para cadenas más grandes debido a la copia requerida, y (b) no funciona en los mmap.mmap
objetos en absoluto. La implementación en find_nth()
(basado en str.find()
) debe ser preferida en todas las circunstancias (y por lo tanto ser la respuesta aceptada a esta pregunta).
Todavía hay bastante margen de mejora, ya que la extensión C se ejecutó casi un factor 4 más rápido que el código Python puro, lo que indica que podría haber un caso para una función de biblioteca dedicada de Python.