Triển khai vectơ trong Python

Python không phải là ngôn ngữ lập trình nhanh nhất. Vì vậy, khi bạn cần xử lý một lượng lớn dữ liệu đồng nhất một cách nhanh chóng, bạn được yêu cầu dựa vào “vector hóa. ”

Điều này dẫn đến nhiều câu hỏi hơn

  • "Vector hóa" thực sự có nghĩa là gì?
  • Khi nào áp dụng?
  • Làm thế nào để vector hóa thực sự làm cho mã nhanh hơn?

Để trả lời câu hỏi đó, chúng tôi sẽ xem xét các chỉ số hiệu suất thú vị, tìm hiểu một số thông tin hữu ích về cách thức hoạt động của CPU và khám phá ra rằng các nhà phát triển NumPy đang nỗ lực làm việc để làm cho mã của bạn nhanh hơn

"Vector hóa" nghĩa là gì và khi nào nó được áp dụng

Giả sử chúng ta có một vài triệu số trong một danh sách hoặc mảng và chúng ta muốn thực hiện một số phép toán trên chúng. Vì chúng tôi biết chúng đều là số và nếu chúng tôi đang thực hiện cùng một thao tác trên tất cả các số, chúng tôi có thể "vectơ hóa" thao tác, tôi. e. tận dụng tính đồng nhất của dữ liệu và hoạt động

Định nghĩa này vẫn còn mơ hồ, tuy nhiên. Để cụ thể hơn, có ít nhất ba nghĩa có thể, tùy thuộc vào người đang nói

  1. thiết kế API. API vector hóa được thiết kế để hoạt động trên các mảng dữ liệu đồng nhất cùng một lúc, thay vì từng mục một trong vòng lặp for. Điều này là trực giao với hiệu suất, mặc dù. về lý thuyết, bạn có thể có vòng lặp for nhanh hoặc bạn có thể có API hàng loạt chậm
  2. Một hoạt động hàng loạt được thực hiện bằng ngôn ngữ nhanh. Đây là ý nghĩa dành riêng cho Python và có hàm ý về hiệu suất. Bằng cách thực hiện tất cả những công việc đó trong C hoặc Rust, bạn có thể tránh gọi vào Python chậm
  3. Một hoạt động tận dụng các tính năng cụ thể của CPU. Như chúng ta sẽ thấy sau này, CPU có hỗ trợ chuyên dụng để thực hiện cùng một thao tác trên nhiều phần tử dữ liệu

Đối với mục đích của chúng tôi, chúng tôi sẽ chủ yếu tập trung vào ý nghĩa thứ hai và sau đó chúng tôi sẽ nói về ý nghĩa thứ ba. Tuy nhiên, trong mọi trường hợp, chúng tôi cho rằng chúng tôi có

  1. Dữ liệu đơn giản và đồng nhất, e. g. số nguyên hoặc số dấu phẩy động
  2. Nhiều hơn một mục dữ liệu
  3. Chúng tôi đang thực hiện cùng một thao tác trên nhiều mục dữ liệu

Tăng tốc mã với vector hóa

Làm thế nào chính xác thì vector hóa giúp với hiệu suất?

Có ba cách khác nhau để chúng ta có thể tăng tốc mã Python bằng cách tận dụng tính đồng nhất của dữ liệu

Chúng tôi sẽ sử dụng trình quản lý ngữ cảnh sau đây sẽ sử dụng công cụ Linux

from benchmark import perf
from random import random

DATA = [random() for _ in range(30_000_000)]

with perf():
    mean = sum(DATA) / len(DATA)
    result = [DATA[i] - mean for i in range(len(DATA))]
0 để đo một số chỉ số hiệu suất cho một khối mã Python. Cách thức hoạt động của nó là gắn vào quy trình đang chạy hiện tại, sau đó nhận các số liệu khác nhau từ CPU khi trình quản lý bối cảnh kết thúc

Vui lòng và không đọc đoạn mã cụ thể này; . Điều quan trọng là đầu ra, sẽ được giải thích trong phần tiếp theo. Nếu bạn quan tâm, bạn luôn có thể quay lại và đọc nó sau

from contextlib import contextmanager
from subprocess import Popen
from os import getpid
from signal import SIGINT
from time import sleep, time
from resource import getrusage, RUSAGE_SELF

events = [
    "instructions",
    "cache-references",
    "cache-misses",
    "avx_insts.all",
]

@contextmanager
def perf():
    """Benchmark this process with Linux's perf util.
    
    Example usage:

        with perf():
            x = run_some_code()
            more_code(x)
            all_this_code_will_be_measured()
    """
    p = Popen([
            # Run perf stat
            "perf", "stat",
            # for the current Python process
            "-p", str(getpid())
            # record the list of events mentioned above
            "-e", ",".join(events)])
    # Ensure perf has started before running more
    # Python code. This will add ~0.1 to the elapsed
    # time reported by perf, so we also track elapsed
    # time separately.
    sleep(0.1)
    start = time()
    try:
        yield
    finally:
        print(f"Elapsed (seconds): {time() - start}")
        print("Peak memory (MiB):",
            int(getrusage(RUSAGE_SELF).ru_maxrss / 1024))
        p.send_signal(SIGINT)

Phần 1. Giảm hướng dẫn CPU

Cách đầu tiên mà vector hóa có thể giúp ích là giảm các lệnh của CPU

Hãy xem một ví dụ. chúng ta sẽ bình thường hóa một mảng số float kép (tôi. e. độ chính xác 64-bit) bằng cách trừ đi giá trị trung bình. Sau đó, chúng ta có thể thấy giá trị trung bình của mỗi mục trên hoặc dưới

CPython chậm

Đây là nỗ lực đầu tiên

from benchmark import perf
from random import random

DATA = [random() for _ in range(30_000_000)]

with perf():
    mean = sum(DATA) / len(DATA)
    result = [DATA[i] - mean for i in range(len(DATA))]

Như mong đợi, chúng tôi tính giá trị trung bình, sau đó tạo một danh sách mới gồm các số mà chúng tôi đã trừ giá trị trung bình. Khi chúng tôi chạy cái này, chúng tôi nhận được đầu ra sau

Elapsed (seconds): 3.3306941986083984
Peak memory (MiB): 2327
...
    31,426,844,839      instructions:u
        11,882,126      cache-references:u
         4,024,256      cache-misses:u
...

Như chúng ta sẽ sớm thấy, điều này rất rất chậm. Và lý do là trình thông dịch Python mặc định (“CPython”) đang triển khai tất cả mã này theo một cách hoàn toàn chung chung

CPython không biết nó đang xử lý một danh sách các số dấu phẩy động; . Điều đó có nghĩa là trình thông dịch không thể chỉ sử dụng các hướng dẫn bổ sung dấu phẩy động của CPU. nó phải chọc vào

from benchmark import perf
from random import random

DATA = [random() for _ in range(30_000_000)]

with perf():
    mean = sum(DATA) / len(DATA)
    result = [DATA[i] - mean for i in range(len(DATA))]
1 và tra cứu hàm cộng của nó, sau đó nó phải trích xuất số dấu phẩy động từ cả hai đối tượng, sau đó nó phải phân bổ một
from benchmark import perf
from random import random

DATA = [random() for _ in range(30_000_000)]

with perf():
    mean = sum(DATA) / len(DATA)
    result = [DATA[i] - mean for i in range(len(DATA))]
1 mới, v.v.

Và điều này cũng có nghĩa là sử dụng thêm bộ nhớ để biểu diễn dữ liệu;

PyPy rất nhanh

Sự chậm chạp mà chúng tôi thấy trong CPython là một lựa chọn triển khai, do cách hoạt động của trình thông dịch CPython mặc định. Trình thông dịch PyPy thông minh hơn. PyPy nhận thấy rằng danh sách này là một danh sách đồng nhất các số dấu phẩy động và tận dụng kiến ​​thức đó. Thay vì sử dụng mã chung, nó sử dụng trình biên dịch tức thời trong thời gian chạy để tạo mã máy được điều chỉnh cụ thể để thêm các số dấu phẩy động

Đây là kết quả của việc chạy cùng mã với PyPy

Elapsed (seconds): 0.18796753883361816
Peak memory (MiB): 535
...
     1,280,847,750      instructions:u
           862,534      cache-references:u
           515,675      cache-misses:u
...

Ít hướng dẫn hơn, mức sử dụng bộ nhớ thấp hơn nhiều và thực thi nhanh hơn nhiều

C nhanh

Thư viện NumPy, được viết bằng C, cũng cho phép chúng tôi chạy cùng mã theo cách vector hóa. Không giống như PyPy, tự động tìm ra những việc cần làm, NumPy yêu cầu chúng tôi sử dụng rõ ràng các đối tượng mảng chuyên biệt với các API chuyên dụng. Đây là mã tương ứng

________số 8_______

Và đây là kết quả khi chạy

Elapsed (seconds): 0.0806736946105957
Peak memory (MiB): 483
...
       162,356,695      instructions:u
         9,669,587      cache-references:u
         3,366,269      cache-misses:u
...

Bố cục bộ nhớ được sử dụng bởi mã C trong NumPy là một mảng float64, hay còn gọi là float kép. số dấu phẩy động được lưu trữ trong 8 byte. Vì NumPy biết nó đang xử lý một mảng float64s, nên nó có thể cộng hai số chỉ với một lệnh CPU. Do đó, số lượng lệnh do CPU chạy nhỏ hơn 20 lần so với CPython

Để so sánh cả ba

Đã trôi qua (giây)Hướng dẫn CPU (M)Lỗi bộ đệm (M)CPython3. 3331,4264. 0PyPy0. 181,2800. 5NumPy0. 081623. 4

Phần 2. Giảm số lần đọc bộ nhớ và lỗi bộ nhớ cache

Bạn có thể nhận thấy ở trên rằng mức sử dụng bộ nhớ cao nhất đối với NumPy thấp hơn nhiều so với CPython. Điều này là do cách bố trí bộ nhớ tốt hơn của NumPy

  • Trong trường hợp của CPython, danh sách các số lưu trữ
    from benchmark import perf
    from random import random
    
    DATA = [random() for _ in range(30_000_000)]
    
    with perf():
        mean = sum(DATA) / len(DATA)
        result = [DATA[i] - mean for i in range(len(DATA))]
    
    1, có nghĩa là số thực nằm ở đâu đó trong bộ nhớ
  • Ngược lại, NumPy lưu trữ các số dấu phẩy động trực tiếp trong mảng
  • Trình thông dịch PyPy đủ thông minh để nhận thấy nội dung của dữ liệu và sắp xếp nó một cách thích hợp trong bộ nhớ dưới dạng một mảng float64

Đọc RAM chậm hơn so với chạy lệnh CPU, vì vậy CPU có một loạt bộ đệm để tăng tốc độ truy cập bộ nhớ. Nếu bộ nhớ không có trong bộ đệm, điều đó dẫn đến lỗi bộ đệm đắt tiền. Sử dụng ít bộ nhớ hơn có nghĩa là ít tra cứu bộ nhớ hơn (chậm hơn hướng dẫn CPU) và hy vọng ít lỗi bộ nhớ cache hơn, chậm hơn nhiều

Đó là một cách khác mà vector hóa giúp. là điều kiện tiên quyết để làm điều đó, bạn cần lưu trữ dữ liệu theo cách nhỏ gọn hơn, thân thiện với bộ đệm. Và điều đó có nghĩa là mã chạy nhanh hơn

Để so sánh cả ba

Bộ nhớ cao nhất (MiB)Tham chiếu bộ nhớ cache (M)Lỗi bộ nhớ cache (M)CPython2,32711. 94. 0PyPy5350. 90. 5NumPy4839. 73. 4

Tôi không rõ PyPy quản lý như thế nào để có ít tham chiếu bộ đệm hơn và bỏ lỡ

Phần #3. Song song với SIMD

Cho đến nay chúng ta đã nói về ý nghĩa vector hóa cụ thể của Python. chuyển sang chạy tất cả mã một cách nhanh chóng trong thứ gì đó dịch trực tiếp sang mã máy, bỏ qua triển khai mặc định linh hoạt nhưng chậm của Python

Nhưng có một ý nghĩa khác cho "vector hóa" được sử dụng bên ngoài thế giới Python. tận dụng chức năng CPU cụ thể được thiết kế cho loại hoạt động này. Thêm cùng một số vào toàn bộ dãy số là một hoạt động phổ biến. Nói rộng hơn, mã máy tính thường sẽ chạy các hoạt động liên quan đến việc thực hiện cùng một việc với nhiều mục của một mảng

Để làm cho các thao tác này nhanh hơn nữa, các CPU hiện đại cung cấp các lệnh “SIMD”. Một lệnh, nhiều dữ liệu

  • Với các lệnh CPU thông thường, mỗi lần chỉ có thể thêm một cặp số
  • Sử dụng lệnh SIMD, phần cứng CPU có thể thêm nhiều cặp cùng một lúc

Từ quan điểm của một lập trình viên Python, hướng dẫn SIMD quá thấp để truy cập trực tiếp. Thay vào đó, bạn cần thực hiện vector hóa kiểu Python. ủy quyền tất cả công việc cho mã nhanh hơn bỏ qua tính linh hoạt thông thường của Python. Bất cứ điều gì cung cấp mã nhanh đó, ví dụ như NumPy, sau đó có thể triển khai SIMD cho bạn

Các phiên bản gần đây của NumPy đã bắt đầu triển khai SIMD, với nhiều thao tác được thêm vào sau mỗi bản phát hành. Vì vậy, hãy so sánh NumPy 1. 18, không có nhiều hỗ trợ SIMD, với NumPy 1. 22. 1, mà không

Đặc biệt, tôi sẽ làm việc với 300 triệu số float64 và đo xem có bao nhiêu lệnh AVX đang được chạy. AVX là một họ hướng dẫn SIMD được cung cấp bởi các CPU Intel và AMD hiện đại, và đặc biệt, đây là hướng dẫn tiên tiến nhất được hỗ trợ bởi máy tính cá nhân của tôi. Máy tính của bạn có thể sẽ sử dụng các hướng dẫn SIMD khác với hướng dẫn của tôi

Đây là đầu ra với NumPy 1. 18, mà tôi đã sử dụng trong các phần trước của bài viết này

Elapsed (seconds): 5.30308723449707
...
     1,623,405,861      instructions:u
               160      avx_insts.all:u
...

Và đây là kết quả với NumPy 1. 22. 1

Elapsed (seconds): 3.5732483863830566
...
     1,100,665,494      instructions:u
       225,000,272      avx_insts.all:u
...

Để so sánh

Đã trôi qua (giây)Hướng dẫn (M)Hướng dẫn AVX (M)Không có SIMD5. 31,6230SIMD3. 61,100225

Chúng tôi đang sử dụng ít hướng dẫn hơn, vì hướng dẫn AVX cho phép thêm bốn float64 cùng một lúc, thay vì mỗi lần một. Và một loạt hướng dẫn đã chuyển từ “thêm một số” sang “thêm bốn số song song” dựa trên AVX. ”

Ghi chú. Việc có bất kỳ công cụ hoặc kỹ thuật cụ thể nào sẽ tăng tốc mọi thứ hay không tùy thuộc vào vị trí tắc nghẽn trong phần mềm của bạn

Cần xác định các tắc nghẽn về hiệu suất và bộ nhớ trong mã xử lý dữ liệu Python của riêng bạn?

Triển khai vectơ trong Python

Triển khai vectơ trong Python

Bộ ba hiệu suất vector hóa

Tóm lại, nếu bạn có dữ liệu đồng nhất mà bạn sẽ xử lý theo cùng một cách, thì bạn có thể tăng tốc bằng cách sử dụng vector hóa

Trong ngữ cảnh của Python, điều đó có nghĩa là chuyển sang biểu diễn bộ nhớ mà mã cấp thấp có thể truy cập hiệu quả. Sau đó, mã của bạn sẽ chạy nhanh hơn do ba nguyên nhân khác nhau

  1. Các hoạt động đơn giản trên dữ liệu đồng nhất có thể được dịch sang mã máy hiệu quả hơn, giảm số lượng lệnh CPU cần chạy
  2. Bố cục bộ nhớ hiệu quả hơn sẽ giảm lỗi bộ nhớ cache
  3. Mã cấp thấp có thể sử dụng các tính năng phần cứng như SIMD. Trong thế giới Python, điều này được thực hiện bởi các phiên bản gần đây của NumPy và trong một số trường hợp bởi Numba

Tất nhiên, không có gì là hoàn hảo, vì vậy bạn cũng có thể muốn đọc thêm về các giới hạn và các lựa chọn thay thế cho phong cách vector hóa của NumPy


Bài viết tiếp theo. CPU, máy ảo đám mây và hàng xóm ồn ào. giới hạn của song song
bài báo trước. Vị trí bộ nhớ quan trọng đối với hiệu suất


Tìm các tắc nghẽn về hiệu suất và bộ nhớ trong mã xử lý dữ liệu của bạn với trình cấu hình Sciagraph

Các công việc chạy chậm sẽ lãng phí thời gian của bạn trong quá trình phát triển, cản trở người dùng của bạn và tăng chi phí tính toán của bạn. Tăng tốc mã của bạn và bạn sẽ lặp lại nhanh hơn, có người dùng hài lòng hơn và bám sát ngân sách của mình—nhưng trước tiên, bạn cần xác định nguyên nhân của sự cố

Tìm các nút thắt cổ chai về hiệu suất và bộ nhớ trong khoa học dữ liệu của bạn Công việc Python với trình hồ sơ Sciagraph. Cấu hình trong quá trình phát triển và sản xuất, với sự hỗ trợ đa xử lý và hơn thế nữa

Triển khai vectơ trong Python

Tìm hiểu các kỹ năng kỹ thuật phần mềm Python thực tế mà bạn có thể sử dụng trong công việc của mình

Đăng ký nhận bản tin của tôi và tham gia cùng hơn 6700 nhà phát triển Python và nhà khoa học dữ liệu học các công cụ và kỹ thuật thực tế, từ hiệu suất Python đến đóng gói Docker, với một bài viết mới miễn phí trong hộp thư đến của bạn mỗi tuần

Làm cách nào để viết mã vectơ trong Python?

Ví dụ - .
nhập numpy dưới dạng np
danh sách1 = [10,20,30,40,50]
danh sách2 = [5,2,4,3,1]
vtr1 = np. mảng (danh sách1)
vtr2=np. mảng (danh sách2)
print("Ta tạo vector từ danh sách 1. ")
in(vtr1)
print("Ta tạo vector từ danh sách 2. ")

Chúng ta có thể sử dụng vector trong Python không?

Vectơ trong một thuật ngữ đơn giản có thể được coi là một mảng một chiều. Đối với Python, vector là mảng một chiều gồm các danh sách . Nó chiếm các phần tử theo cách tương tự như danh sách Python.

Mô-đun vectơ trong Python là gì?

Vectơ là a Python 3. Hơn 7 thư viện dành cho vectơ 2D, 3D và Lorentz, đặc biệt là mảng vectơ, để giải các bài toán vật lý phổ biến theo cách giống như NumPy . Các tính năng chính của Vectơ. Python thuần túy với NumPy là phụ thuộc duy nhất của nó.