Tôi đang cung cấp một số kết quả đo điểm chuẩn so sánh các phương pháp tiếp cận nổi bật nhất được trình bày cho đến nay, cụ thể là @ bobince's findnth()
(dựa trên str.split()
) so với @ tgamblin's hoặc @Mark Byers ' find_nth()
(dựa trên str.find()
). Tôi cũng sẽ so sánh với phần mở rộng C ( _find_nth.so
) để xem chúng ta có thể đi nhanh như thế nào. Đây là 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
Tất nhiên, hiệu suất quan trọng nhất nếu chuỗi lớn, vì vậy giả sử chúng ta muốn tìm dòng mới thứ 1000001 ('\ n') trong tệp 1,3 GB được gọi là 'bigfile'. Để tiết kiệm bộ nhớ, chúng tôi muốn làm việc trên một mmap.mmap
biểu diễn đối tượng của tệp:
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)
Đã có vấn đề đầu tiên với findnth()
, vì mmap.mmap
các đối tượng không hỗ trợ split()
. Vì vậy, chúng tôi thực sự phải sao chép toàn bộ tệp vào bộ nhớ:
In [4]: %time s = mm[:]
CPU times: user 813 ms, sys: 3.25 s, total: 4.06 s
Wall time: 17.7 s
Oái oăm! May mắn thay, s
vẫn phù hợp với bộ nhớ 4 GB của Macbook Air của tôi, vì vậy hãy điểm chuẩn findnth()
:
In [5]: %timeit find_nth.findnth(s, '\n', 1000000)
1 loops, best of 3: 29.9 s per loop
Rõ ràng là một hiệu suất khủng khiếp. Hãy xem cách tiếp cận dựa trên str.find()
làm như thế nào:
In [6]: %timeit find_nth.find_nth(s, '\n', 1000000)
1 loops, best of 3: 774 ms per loop
Tốt hơn nhiều! Rõ ràng, findnth()
vấn đề của nó là nó buộc phải sao chép chuỗi trong suốt split()
, đây đã là lần thứ hai chúng tôi sao chép 1,3 GB dữ liệu sau đó s = mm[:]
. Ở đây có lợi thế thứ hai là find_nth()
: Chúng tôi có thể sử dụng nó mm
trực tiếp, sao cho không yêu cầu bản sao của tệp:
In [7]: %timeit find_nth.find_nth(mm, '\n', 1000000)
1 loops, best of 3: 1.21 s per loop
Có vẻ như có một hình phạt hiệu suất nhỏ khi hoạt động mm
so với s
, nhưng điều này minh họa find_nth()
có thể giúp chúng ta có câu trả lời trong 1,2 giây so với findnth
tổng số 47 giây của.
Tôi không tìm thấy trường hợp nào mà str.find()
phương pháp dựa trên tệ hơn đáng kể so với str.split()
phương pháp dựa trên, vì vậy tại thời điểm này, tôi sẽ tranh luận rằng câu trả lời của @ tgamblin hoặc @Mark Byers nên được chấp nhận thay vì của @ bobince.
Trong thử nghiệm của tôi, phiên bản find_nth()
trên là giải pháp Python thuần túy nhanh nhất mà tôi có thể nghĩ ra (rất giống với phiên bản của @Mark Byers). Hãy xem chúng ta có thể làm tốt hơn thế nào với mô-đun mở rộng C. Đây là _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);
}
Đây là setup.py
tệp:
from distutils.core import setup, Extension
module = Extension('_find_nth', sources=['_find_nthmodule.c'])
setup(ext_modules=[module])
Cài đặt như bình thường với python setup.py install
. Mã C có lợi thế ở đây vì nó bị giới hạn trong việc tìm các ký tự đơn lẻ, nhưng hãy xem tốc độ của nó như thế nào:
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
Rõ ràng là vẫn còn nhanh hơn một chút. Điều thú vị là không có sự khác biệt về mức C giữa trường hợp trong bộ nhớ và trường hợp được ánh xạ. Nó cũng là thú vị khi thấy rằng _find_nth2()
, mà là dựa trên string.h
's memchr()
chức năng thư viện, phải thua thiệt so với thực hiện đơn giản trong _find_nth()
: Các 'tối ưu hóa' bổ sung trong memchr()
được rõ ràng lấp đầy sỏi ...
Tóm lại, việc triển khai trong findnth()
(dựa trên str.split()
) thực sự là một ý tưởng tồi, vì (a) nó hoạt động rất tệ đối với các chuỗi lớn hơn do yêu cầu sao chép và (b) nó hoàn toàn không hoạt động trên mmap.mmap
các đối tượng. Việc triển khai trong find_nth()
(dựa trên str.find()
) nên được ưu tiên trong mọi trường hợp (và do đó là câu trả lời được chấp nhận cho câu hỏi này).
Vẫn còn khá nhiều chỗ để cải thiện, vì phần mở rộng C chạy nhanh hơn gần như gấp 4 lần so với mã Python thuần túy, cho thấy rằng có thể có một trường hợp cho một hàm thư viện Python chuyên dụng.