Saya menawarkan beberapa hasil pembandingan yang membandingkan pendekatan paling menonjol yang disajikan sejauh ini, yaitu @ bobince findnth()
(berdasarkan str.split()
) vs. @ tgamblin atau @Mark Byers ' find_nth()
(berdasarkan str.find()
). Saya juga akan membandingkan dengan ekstensi C ( _find_nth.so
) untuk melihat seberapa cepat kita bisa pergi. Ini dia 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
Tentu saja, kinerja paling penting jika stringnya besar, jadi misalkan kita ingin mencari baris baru ke-1000001 ('\ n') dalam file 1,3 GB yang disebut 'bigfile'. Untuk menghemat memori, kami ingin mengerjakan mmap.mmap
representasi objek dari file:
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)
Sudah ada masalah pertama dengan findnth()
, karena mmap.mmap
objek tidak mendukung split()
. Jadi kami sebenarnya harus menyalin seluruh file ke dalam memori:
In [4]: %time s = mm[:]
CPU times: user 813 ms, sys: 3.25 s, total: 4.06 s
Wall time: 17.7 s
Aduh! Untungnya s
masih muat di memori 4 GB Macbook Air saya, jadi mari benchmark findnth()
:
In [5]: %timeit find_nth.findnth(s, '\n', 1000000)
1 loops, best of 3: 29.9 s per loop
Performa yang jelas mengerikan. Mari kita lihat bagaimana pendekatan berdasarkan str.find()
itu:
In [6]: %timeit find_nth.find_nth(s, '\n', 1000000)
1 loops, best of 3: 774 ms per loop
Jauh lebih baik! Jelas, findnth()
masalahnya adalah bahwa string dipaksa untuk menyalin selama split()
, yang sudah kedua kalinya kami menyalin 1,3 GB data setelahnya s = mm[:]
. Inilah keuntungan kedua dari find_nth()
: Kita dapat menggunakannya secara mm
langsung, sehingga tidak ada salinan file yang diperlukan:
In [7]: %timeit find_nth.find_nth(mm, '\n', 1000000)
1 loops, best of 3: 1.21 s per loop
Tampaknya ada hukuman kinerja kecil yang beroperasi pada mm
vs. s
, tetapi ini menggambarkan bahwa find_nth()
dapat memberi kita jawaban dalam 1,2 d dibandingkan dengan findnth
total 47 d.
Saya tidak menemukan kasus di mana str.find()
pendekatan berbasis secara signifikan lebih buruk daripada str.split()
pendekatan berbasis, jadi pada titik ini, saya berpendapat bahwa jawaban @ tgamblin atau @Mark Byers harus diterima daripada @ bobince.
Dalam pengujian saya, versi di find_nth()
atas adalah solusi Python murni tercepat yang dapat saya buat (sangat mirip dengan versi @Mark Byers). Mari kita lihat seberapa baik yang bisa kita lakukan dengan modul ekstensi C. Ini dia _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);
}
Ini setup.py
filenya:
from distutils.core import setup, Extension
module = Extension('_find_nth', sources=['_find_nthmodule.c'])
setup(ext_modules=[module])
Instal seperti biasa dengan python setup.py install
. Kode C memainkan keuntungan di sini karena terbatas pada menemukan karakter tunggal, tetapi mari kita lihat seberapa cepat ini:
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
Jelas masih lebih cepat. Menariknya, tidak ada perbedaan pada level C antara in-memory dan case mmapped. Hal ini juga menarik untuk melihat bahwa _find_nth2()
, yang didasarkan pada string.h
's memchr()
fungsi perpustakaan, kehilangan menentang pelaksanaan langsung di _find_nth()
: The tambahan 'optimasi' di memchr()
rupanya knalpot ...
Kesimpulannya, implementasi dalam findnth()
(berdasarkan str.split()
) benar-benar ide yang buruk, karena (a) ia bekerja sangat buruk untuk string yang lebih besar karena penyalinan yang diperlukan, dan (b) tidak bekerja pada mmap.mmap
objek sama sekali. Penerapan dalam find_nth()
(berdasarkan str.find()
) harus diutamakan dalam semua keadaan (dan karena itu menjadi jawaban yang diterima untuk pertanyaan ini).
Masih ada sedikit ruang untuk perbaikan, karena ekstensi C berjalan hampir 4 kali lipat lebih cepat daripada kode Python murni, menunjukkan bahwa mungkin ada kasus untuk fungsi pustaka Python khusus.