Đối tượng giống cấu trúc (để truy cập) nhanh nhất trong Python là gì?


77

Tôi đang tối ưu hóa một số mã có nút cổ chai chính đang chạy qua và truy cập vào một danh sách rất lớn các đối tượng giống như cấu trúc. Hiện tại tôi đang sử dụng các nhóm có tên để dễ đọc. Nhưng một số điểm chuẩn nhanh bằng cách sử dụng 'timeit' cho thấy rằng đây thực sự là một cách sai lầm khi coi hiệu suất là một yếu tố:

Được đặt tên tuple với a, b, c:

>>> timeit("z = a.c", "from __main__ import a")
0.38655471766332994

Lớp sử dụng __slots__, với a, b, c:

>>> timeit("z = b.c", "from __main__ import b")
0.14527461047146062

Từ điển với các phím a, b, c:

>>> timeit("z = c['c']", "from __main__ import c")
0.11588272541098377

Tuple với ba giá trị, sử dụng khóa không đổi:

>>> timeit("z = d[2]", "from __main__ import d")
0.11106188992948773

Liệt kê với ba giá trị, sử dụng khóa hằng số:

>>> timeit("z = e[2]", "from __main__ import e")
0.086038238242508669

Tuple với ba giá trị, sử dụng khóa cục bộ:

>>> timeit("z = d[key]", "from __main__ import d, key")
0.11187358437882722

Liệt kê với ba giá trị, sử dụng khóa cục bộ:

>>> timeit("z = e[key]", "from __main__ import e, key")
0.088604143037173344

Trước hết, có điều gì về những timeitthử nghiệm nhỏ này khiến chúng không hợp lệ không? Tôi đã chạy mỗi lần một vài lần, để đảm bảo không có sự kiện hệ thống ngẫu nhiên nào làm mất chúng và kết quả gần như giống hệt nhau.

Có vẻ như từ điển cung cấp sự cân bằng tốt nhất giữa hiệu suất và khả năng đọc, với các lớp đứng thứ hai. Điều này thật không may, vì theo mục đích của tôi, tôi cũng cần đối tượng phải có trình tự; do đó sự lựa chọn của tôi về nametuple.

Danh sách về cơ bản nhanh hơn đáng kể, nhưng các khóa không đổi là không thể xác định được; Tôi phải tạo một loạt các hằng số chỉ mục, tức là KEY_1 = 1, KEY_2 = 2, v.v. cũng không phải là lý tưởng.

Tôi bị mắc kẹt với những lựa chọn này, hay có một giải pháp thay thế nào mà tôi đã bỏ qua?


1
Nếu hiệu suất được ưu tiên như vậy, tại sao không sử dụng C?
Skilldrick

10
@Skilldrick: Đây chỉ là một phần nhỏ của một chương trình lớn hơn, được hưởng lợi từ việc được viết bằng Python. Viết lại phần này dưới dạng phần mở rộng C là một tùy chọn, nhưng hơi không mong muốn, vì mã khác cũng chạm vào dữ liệu, làm phức tạp mọi thứ một chút. Hiệu suất là quan trọng, nhưng không phải là điều quan trọng; Tôi khá hài lòng với cải tiến 4x được cung cấp bởi các danh sách, nếu không phải vì khả năng bảo trì giảm. Tôi chỉ đang tìm kiếm các lựa chọn khác trước khi quyết định chọn con đường nào.
DNS

1
@Warren P: Có; Tôi không tối ưu hóa quá sớm. Đây là một vòng lặp rất chặt chẽ trong đó chỉ cần truy cập các cấu trúc là một phần đáng kể của công việc. Đây là vòng lặp chậm nhất còn lại trong chương trình. Ngay cả một cải tiến khiêm tốn cũng có thể cắt giảm một hoặc hai giây so với thời gian chạy trong thế giới thực. Vì toàn bộ sự việc được lặp lại, điều đó sẽ tăng lên.
DNS

1
Cũng nên xem xét thử pypy. Với pypy, tôi không nhận được bất kỳ sự khác biệt nào về hiệu suất giữa các trường hợp.
Thomas Ahle

2
numpy có một số loại cấu trúc và có thể cho hiệu suất tốt hơn C trong một số trường hợp. docs.scipy.org/doc/numpy/user/basics.rec.html Tôi chưa thử cái này và YMMV!
Sam Watkins

Câu trả lời:


55

Một điều cần lưu ý là các bộ được đặt tên được tối ưu hóa để truy cập dưới dạng bộ giá trị. Nếu bạn thay đổi bộ truy cập của mình thành a[2]thay vì a.c, bạn sẽ thấy hiệu suất tương tự như các bộ giá trị. Lý do là các trình truy cập tên đang chuyển thành lời gọi tự [idx] một cách hiệu quả, vì vậy, hãy trả cả giá lập chỉ mục giá tra cứu tên.

Nếu kiểu sử dụng của bạn là phổ biến để truy cập theo tên, nhưng truy cập bằng tuple thì không, bạn có thể viết nhanh tương đương với nametuple thực hiện những việc ngược lại: xác định các tra cứu chỉ mục để truy cập theo tên. Tuy nhiên, bạn sẽ phải trả giá khi tra cứu chỉ mục. Ví dụ: đây là một triển khai nhanh chóng:

def makestruct(name, fields):
    fields = fields.split()
    import textwrap
    template = textwrap.dedent("""\
    class {name}(object):
        __slots__ = {fields!r}
        def __init__(self, {args}):
            {self_fields} = {args}
        def __getitem__(self, idx): 
            return getattr(self, fields[idx])
    """).format(
        name=name,
        fields=fields,
        args=','.join(fields), 
        self_fields=','.join('self.' + f for f in fields))
    d = {'fields': fields}
    exec template in d
    return d[name]

Nhưng thời gian rất tệ khi __getitem__phải gọi:

namedtuple.a  :  0.473686933517 
namedtuple[0] :  0.180409193039
struct.a      :  0.180846214294
struct[0]     :  1.32191514969

tức là, hiệu suất tương tự như một __slots__lớp để truy cập thuộc tính (không có gì đáng ngạc nhiên - đó là điều đó), nhưng bị phạt rất lớn do tra cứu hai lần trong các truy cập dựa trên chỉ mục. (Đáng chú ý là điều __slots__đó không thực sự giúp ích nhiều về tốc độ. Nó giúp tiết kiệm bộ nhớ, nhưng thời gian truy cập vẫn như nhau nếu không có chúng.)

Một tùy chọn thứ ba sẽ là sao chép dữ liệu, ví dụ: lớp con từ danh sách và lưu trữ các giá trị trong cả thuộc tính và dữ liệu danh sách. Tuy nhiên, bạn không thực sự nhận được hiệu suất tương đương với danh sách. Có một tác động lớn về tốc độ chỉ khi phân lớp (kiểm tra quá tải thuần python). Do đó, struct [0] vẫn mất khoảng 0,5 giây (so với 0,18 cho danh sách thô) trong trường hợp này và bạn tăng gấp đôi mức sử dụng bộ nhớ, vì vậy điều này có thể không đáng.


3
Hãy cẩn thận với công thức này trong đó các trường có thể có dữ liệu do người dùng nhập - người thực thi trên các trường có thể chạy mã tùy ý. Nếu không thì siêu tuyệt.
Michael Scott Cuthbert

13
Không phải là ngớ ngẩn khi truy cập theo tên chậm hơn truy cập bằng chỉ mục cho các nhóm có tên? Nếu triển khai NAMEDtuple tại sao tôi lại tối ưu hóa để truy cập theo chỉ mục?
Rotareti

44

Câu hỏi này khá cũ (thời gian có internet), vì vậy tôi nghĩ hôm nay tôi sẽ thử sao chép bài kiểm tra của bạn, cả với CPython thông thường (2.7.6) và với pypy (2.2.1) và xem các phương pháp khác nhau được so sánh như thế nào. (Tôi cũng đã thêm vào một tra cứu được lập chỉ mục cho tuple được đặt tên.)

Đây là một chút tiêu chuẩn vi mô, vì vậy YMMV, nhưng pypy dường như tăng tốc độ truy cập tuple được đặt tên bằng hệ số 30 so với CPython (trong khi truy cập từ điển chỉ được tăng tốc bởi hệ số 3).

from collections import namedtuple

STest = namedtuple("TEST", "a b c")
a = STest(a=1,b=2,c=3)

class Test(object):
    __slots__ = ["a","b","c"]

    a=1
    b=2
    c=3

b = Test()

c = {'a':1, 'b':2, 'c':3}

d = (1,2,3)
e = [1,2,3]
f = (1,2,3)
g = [1,2,3]
key = 2

if __name__ == '__main__':
    from timeit import timeit

    print("Named tuple with a, b, c:")
    print(timeit("z = a.c", "from __main__ import a"))

    print("Named tuple, using index:")
    print(timeit("z = a[2]", "from __main__ import a"))

    print("Class using __slots__, with a, b, c:")
    print(timeit("z = b.c", "from __main__ import b"))

    print("Dictionary with keys a, b, c:")
    print(timeit("z = c['c']", "from __main__ import c"))

    print("Tuple with three values, using a constant key:")    
    print(timeit("z = d[2]", "from __main__ import d"))

    print("List with three values, using a constant key:")
    print(timeit("z = e[2]", "from __main__ import e"))

    print("Tuple with three values, using a local key:")
    print(timeit("z = d[key]", "from __main__ import d, key"))

    print("List with three values, using a local key:")
    print(timeit("z = e[key]", "from __main__ import e, key"))

Kết quả Python:

Named tuple with a, b, c:
0.124072679784
Named tuple, using index:
0.0447055962367
Class using __slots__, with a, b, c:
0.0409136944224
Dictionary with keys a, b, c:
0.0412045334915
Tuple with three values, using a constant key:
0.0449477955531
List with three values, using a constant key:
0.0331083467148
Tuple with three values, using a local key:
0.0453569025139
List with three values, using a local key:
0.033030056702

Kết quả PyPy:

Named tuple with a, b, c:
0.00444889068604
Named tuple, using index:
0.00265598297119
Class using __slots__, with a, b, c:
0.00208616256714
Dictionary with keys a, b, c:
0.013897895813
Tuple with three values, using a constant key:
0.00275301933289
List with three values, using a constant key:
0.002760887146
Tuple with three values, using a local key:
0.002769947052
List with three values, using a local key:
0.00278806686401

2
Điều thú vị là với pypy, điều tồi tệ nhất là lưỡng phân.
RomainL.

6

Vấn đề này có thể lỗi thời sớm. CPython dev rõ ràng đã thực hiện những cải tiến đáng kể đối với hiệu suất truy cập các giá trị tuple được đặt tên theo tên thuộc tính. Các thay đổi được lên lịch phát hành bằng Python 3.8 , vào gần cuối tháng 10 năm 2019.

Xem: https://bugs.python.org/issue32492https://github.com/python/cpython/pull/10495 .


1
Cảm ơn bạn về thông tin ! Thật vậy, trích dẫn từ docs.python.org/3/whatsnew/3.8.html : "Tra cứu trường tăng tốc trong collection.nametuple (). Giờ đây, chúng nhanh hơn hai lần, khiến chúng trở thành dạng tra cứu biến phiên bản nhanh nhất trong Con trăn. "
Ismael EL ATIFI

3

Một vài điểm và ý tưởng:

  1. Bạn đang truy cập vào cùng một chỉ mục nhiều lần liên tiếp. Chương trình thực tế của bạn có thể sử dụng truy cập ngẫu nhiên hoặc tuyến tính, sẽ có hành vi khác nhau. Đặc biệt, sẽ có nhiều lỗi bộ nhớ cache của CPU. Bạn có thể nhận được kết quả hơi khác khi sử dụng chương trình thực tế của mình.

  2. OrderedDictionary được viết dưới dạng một trình bao bọc xung quanh dict, nó sẽ chậm hơn dict. Đó là một giải pháp không.

  3. Bạn đã thử cả lớp kiểu mới và lớp kiểu cũ chưa? (các lớp kiểu mới kế thừa từ object; các lớp kiểu cũ thì không)

  4. Bạn đã thử sử dụng psyco hoặc Unladen Swallow chưa? (Cập nhật năm 2020 - hai dự án này đã chết)

  5. Vòng lặp bên trong của bạn để sửa đổi dữ liệu hay chỉ truy cập nó? Có thể chuyển đổi dữ liệu thành dạng hiệu quả nhất có thể trước khi vào vòng lặp, nhưng hãy sử dụng dạng thuận tiện nhất ở nơi khác trong chương trình.


1

Tôi sẽ bị cám dỗ để (a) phát minh ra một số loại bộ nhớ đệm cụ thể của khối lượng công việc và giảm tải bộ nhớ và truy xuất dữ liệu của tôi sang một quy trình giống như memcachedb, để cải thiện khả năng mở rộng thay vì chỉ hiệu suất hoặc (b) viết lại dưới dạng phần mở rộng C, với lưu trữ dữ liệu gốc. Có lẽ là một loại từ điển có thứ tự.

Bạn có thể bắt đầu với cái này: http://www.xs4all.nl/~anthon/Python/ordereddict/


-1

Bạn có thể tạo trình tự các lớp của mình như bằng cách thêm __iter____getitem__ các phương thức, để làm cho chúng có trình tự giống như (có thể lập chỉ mục và có thể lặp lại).

Sẽ OrderedDictlàm việc? Có một số triển khai có sẵn và nó được bao gồm trong collectionsmô-đun Python31 .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.