ฉันนำเสนอผลการเปรียบเทียบเปรียบเทียบแนวทางที่โดดเด่นที่สุดที่นำเสนอ ได้แก่ @bobince's findnth()
(based on str.split()
) เทียบกับ @ tgamblin's หรือ @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
แน่นอนว่าประสิทธิภาพมีความสำคัญมากที่สุดหากสตริงมีขนาดใหญ่ดังนั้นสมมติว่าเราต้องการค้นหาบรรทัดใหม่ 1000001st ('\ n') ในไฟล์ 1.3 GB ที่เรียกว่า 'bigfile' เพื่อประหยัดหน่วยความจำเราต้องการดำเนินการกับการ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
ยังพอดีกับหน่วยความจำ 4 GB ของ Macbook Air ของฉันดังนั้นเรามาเปรียบเทียบ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()
ที่มีอยู่แล้วเป็นครั้งที่สองที่เราคัดลอก 1.3 GB s = mm[:]
ของข้อมูลรอบหลัง มาที่นี่ในประโยชน์ที่สองของ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 ระหว่างกรณีในหน่วยความจำและกรณีที่ mmapped นอกจากนี้ยังเป็นที่น่าสนใจที่จะเห็นว่า_find_nth2()
ที่ตั้งอยู่บนพื้นฐานstring.h
ของmemchr()
ฟังก์ชั่นห้องสมุดสูญเสียออกมาต่อต้านการดำเนินการตรงไปตรงมาใน_find_nth()
การเพิ่มเติม 'การเพิ่มประสิทธิภาพ' ในการmemchr()
จะเห็นได้ชัดว่า backfiring ...
สรุปได้ว่าการนำไปใช้งานในfindnth()
(ตามstr.split()
) เป็นความคิดที่ไม่ดีจริงๆเนื่องจาก (a) ทำงานได้อย่างยอดเยี่ยมสำหรับสตริงที่มีขนาดใหญ่ขึ้นเนื่องจากการคัดลอกที่จำเป็นและ (b) ไม่ได้ผลกับmmap.mmap
วัตถุเลย ควรนำไปใช้ในfind_nth()
(อิงstr.find()
) ในทุกสถานการณ์ (ดังนั้นจึงเป็นคำตอบที่ยอมรับสำหรับคำถามนี้)
ยังมีช่องว่างสำหรับการปรับปรุงเล็กน้อยเนื่องจากส่วนขยาย C ทำงานได้เร็วกว่าโค้ด Python แท้เกือบ 4 เท่าซึ่งบ่งชี้ว่าอาจมีกรณีสำหรับฟังก์ชันไลบรารี Python โดยเฉพาะ