我提供了一些基准测试结果,比较了迄今为止提出的最突出的方法,即@bobince 的findnth()(基于str.split())与@tgamblin 或@Mark Byers 的find_nth()(基于str.find() )。我还将与 C 扩展 (_find_nth.so) 进行比较,看看我们能走多快。这里是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
当然,如果字符串很大,性能最重要,所以假设我们想在一个名为“bigfile”的 1.3 GB 文件中找到第 1000001 个换行符(“\n”)。为了节省内存,我们想处理文件的mmap.mmap 对象表示:
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)
findnth() 已经存在第一个问题,因为mmap.mmap 对象不支持split()。所以我们实际上必须将整个文件复制到内存中:
In [4]: %time s = mm[:]
CPU times: user 813 ms, sys: 3.25 s, total: 4.06 s
Wall time: 17.7 s
哎哟!幸运的是 s 仍然适合我的 Macbook Air 的 4 GB 内存,所以让我们对 findnth() 进行基准测试:
In [5]: %timeit find_nth.findnth(s, '\n', 1000000)
1 loops, best of 3: 29.9 s per loop
显然是糟糕的表现。让我们看看基于str.find() 的方法是如何做的:
In [6]: %timeit find_nth.find_nth(s, '\n', 1000000)
1 loops, best of 3: 774 ms per loop
好多了!显然,findnth() 的问题在于它在split() 期间被强制复制字符串,这已经是我们在s = mm[:] 之后第二次复制大约 1.3 GB 的数据。 find_nth() 的第二个优点是:我们可以直接在 mm 上使用它,这样就需要 零 个文件副本:
In [7]: %timeit find_nth.find_nth(mm, '\n', 1000000)
1 loops, best of 3: 1.21 s per loop
在mm 与s 上运行似乎存在小的性能损失,但这说明find_nth() 可以在1.2 秒内为我们提供答案,而findnth 的总时间为47 秒。
我没有发现基于str.find() 的方法明显比基于str.split() 的方法差的情况,所以在这一点上,我认为应该接受@tgamblin 或@Mark Byers 的答案而不是@bobince 的答案。
在我的测试中,上面find_nth() 的版本是我能想到的最快的纯Python 解决方案(与@Mark Byers 的版本非常相似)。让我们看看使用 C 扩展模块可以做得更好。这里是_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);
}
这是setup.py 文件:
from distutils.core import setup, Extension
module = Extension('_find_nth', sources=['_find_nthmodule.c'])
setup(ext_modules=[module])
使用python setup.py install 照常安装。 C 代码在这里发挥了优势,因为它仅限于查找单个字符,但让我们看看这有多快:
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
显然还是要快很多。有趣的是,内存中的情况和映射的情况在 C 级别上没有区别。同样有趣的是,基于string.h 的memchr() 库函数的_find_nth2() 输给了_find_nth() 中的直接实现:memchr() 中的额外“优化”显然适得其反。 ..
总之,findnth()(基于str.split())中的实现确实是个坏主意,因为(a)由于需要复制,它对较大的字符串执行得非常糟糕,以及(b)
它根本不适用于mmap.mmap 对象。在任何情况下都应该首选find_nth()(基于str.find())中的实现(因此是这个问题的公认答案)。
仍有相当大的改进空间,因为 C 扩展的运行速度几乎是纯 Python 代码的 4 倍,这表明可能需要专门的 Python 库函数。