۲۹ شهریور ۱۴۰۳

Techboy

اخبار و اطلاعات روز تکنولوژی

از Cython برای تسریع در تکرار آرایه در NumPy استفاده کنید

NumPy به سریع بودن معروف است، اما همیشه جایی برای بهبود وجود دارد. در اینجا نحوه استفاده از Cython برای تکرار روی آرایه های NumPy با سرعت C آورده شده است.

NumPy به سریع بودن معروف است، اما همیشه جایی برای بهبود وجود دارد. در اینجا نحوه استفاده از Cython برای تکرار روی آرایه های NumPy با سرعت C آورده شده است.

NumPy به سریع بودن معروف است، اما آیا می‌تواند حتی سریع‌تر هم پیش برود؟ در اینجا نحوه استفاده از Cython برای تسریع در تکرار آرایه در NumPy آمده است.

NumPy به کاربران پایتون یک کتابخانه بسیار سریع برای کار با داده ها در ماتریس ها می دهد. برای مثال، اگر می‌خواهید یک ماتریس پر از اعداد تصادفی ایجاد کنید، می‌توانید این کار را در کسری از زمانی که در پایتون معمولی طول می‌کشد انجام دهید.

با این وجود، مواقعی وجود دارد که حتی NumPy به خودی خود به اندازه کافی سریع نیست. اگر می‌خواهید روی ماتریس‌های NumPy که در API NumPy در دسترس نیستند، تبدیل‌هایی انجام دهید، یک رویکرد معمولی این است که فقط روی ماتریس در پایتون تکرار کنید … و در وهله اول تمام مزایای عملکرد استفاده از NumPy را از دست بدهید.

خوشبختانه، راه بهتری برای کار مستقیم با داده های NumPy وجود دارد: Cython. با نوشتن کد Python مشروح شده و کامپایل کردن آن به C، می‌توانید روی آرایه‌های NumPy تکرار کنید و مستقیماً با داده‌های آنها با سرعت C کار کنید.

این مقاله به چند مفهوم کلیدی برای نحوه استفاده از Cython با NumPy می پردازد. اگر قبلاً با Cython آشنایی ندارید، اصول Cython را بخوانید و این آموزش ساده برای نوشتن کد Cython را بررسی کنید.

فقط کد محاسباتی اصلی را در Cython برای NumPy بنویسید

متداول‌ترین سناریو برای استفاده از Cython با NumPy، سناریویی است که می‌خواهید یک آرایه NumPy بگیرید، روی آن تکرار کنید و محاسباتی را روی هر عنصری انجام دهید که به راحتی در NumPy قابل انجام نیست.

Cython با این امکان کار می‌کند که به شما اجازه می‌دهد ماژول‌هایی را در یک نسخه مشروح شده از پایتون بنویسید، که سپس به C کامپایل شده و مانند هر ماژول دیگری به اسکریپت پایتون شما وارد می‌شود. به عبارت دیگر، چیزی شبیه به نسخه پایتون از آنچه می‌خواهید انجام دهید، می‌نویسید، سپس با افزودن حاشیه‌نویسی به آن سرعت می‌دهید تا به زبان C ترجمه شود.

برای این منظور، شما فقط باید از Cython برای بخشی از برنامه خود استفاده کنید که محاسبات واقعی را انجام می دهد. هر چیز دیگری که به عملکرد حساس نیست – یعنی هر چیزی که در واقع حلقه ای نیست که روی داده های شما تکرار می شود – باید در پایتون معمولی نوشته شود.

چرا این کار را انجام دهیم؟ ماژول‌های Cython باید هر بار که تغییر می‌کنند دوباره کامپایل شوند، که روند توسعه را کند می‌کند. شما نمی خواهید هر بار که تغییراتی ایجاد می کنید که در واقع مربوط به بخشی از برنامه شما نیست که می خواهید بهینه سازی کنید، مجبور به کامپایل مجدد ماژول های Cython خود شوید.

تکرار از طریق آرایه های NumPy در Cython، نه Python

روش کلی برای کار موثر با NumPy در Cython را می توان در سه مرحله خلاصه کرد:

  1. توابعی را در Cython بنویسید که آرایه های NumPy را به عنوان اشیاء تایپ شده مناسب بپذیرند. وقتی تابع Cython را در کد پایتون خود فرا می‌خوانید، تمام شی آرایه NumPy را به عنوان آرگومان برای آن فراخوانی تابع ارسال کنید.
  2. تمام تکرارها را روی شی در Cython انجام دهید.
  3. یک آرایه NumPy را از ماژول Cython خود به کد پایتون خود برگردانید.

بنابراین، چنین کاری را انجام ندهید:

for index in len(numpy_array):
    numpy_array[index] = cython_function(numpy_array[index])

در عوض، کاری مانند این انجام دهید:


returned_numpy_array = cython_function(numpy_array)

# in cython:

cdef cython_function(numpy_array):
    for item in numpy_array:
        ...
    return numpy_array

من اطلاعات نوع و سایر جزئیات را از این نمونه ها حذف کردم، اما تفاوت باید واضح باشد. تکرار واقعی روی آرایه NumPy باید به طور کامل در Cython انجام شود، نه از طریق تماس های مکرر Cython برای هر عنصر در آرایه.

ارایه های NumPy درست تایپ شده را به توابع Cython منتقل کنید

هر تابعی که آرایه NumPy را به عنوان یک آرگومان می پذیرد، باید به درستی تایپ شود، به طوری که Cython بداند چگونه آرگومان را به عنوان یک آرایه NumPy (سریع) تفسیر کند تا یک شی Python عمومی (آهسته).

در اینجا یک مثال از یک اعلان تابع Cython است که یک آرایه دو بعدی NumPy را می گیرد:


def compute(int[:, ::1] array_1):

در نحو “Pure Python” Cython، از این حاشیه‌نویسی استفاده می‌کنید:


def compute(array_1: cython.int[:, ::1]):

حاشیه نویسی int[] آرایه ای از اعداد صحیح را نشان می دهد که احتمالاً یک آرایه NumPy است. اما برای اینکه تا حد امکان دقیق باشیم، باید تعداد ابعاد آرایه را مشخص کنیم. برای دو بعد، از int[:,:] استفاده می کنیم. برای سه، از int[:,:,:] استفاده می کنیم.

ما همچنین باید چیدمان حافظه آرایه را مشخص کنیم. به‌طور پیش‌فرض در NumPy و Cython، آرایه‌ها به‌صورت پیوسته و سازگار با C قرار می‌گیرند. ::۱ آخرین عنصر ما در نمونه بالا است، بنابراین از int[:,:: استفاده می‌کنیم. ۱] به عنوان امضای ما. (برای جزئیات در مورد سایر گزینه های چیدمان حافظه، به مستندات Cython مراجعه کنید.)

این اعلان‌ها نه تنها به Cython اطلاع می‌دهند که این آرایه‌های NumPy هستند، بلکه نحوه خواندن از آنها به کارآمدترین روش ممکن را نشان می‌دهد.

از Cython memoryviews برای دسترسی سریع به آرایه های NumPy استفاده کنید

Cython یک ویژگی به نام نمایش های حافظه تایپ شده دارد که به شما امکان دسترسی مستقیم خواندن/نوشتن به بسیاری از انواع اشیاء را می دهد که مانند آرایه ها کار می کنند. این شامل آرایه‌های NumPy می‌شود.

برای ایجاد یک مموری ویو، از نحوی مشابه با اعلان‌های آرایه نشان داده شده در بالا استفاده می‌کنید:


# conventional Cython
def compute(int[:, ::1] array_1):
    cdef int [:,:] view2d = array_1

# pure-Python mode    
def compute(array_1: cython.int[:, ::1]):
    view2d: int[:,:] = array_1

توجه داشته باشید که لازم نیست چیدمان حافظه را در اعلان مشخص کنید، زیرا به طور خودکار شناسایی می شود.

از این مرحله به بعد در کد خود، از view2d می‌خوانید و می‌نویسید با همان نحو دسترسی مانند شی array_1 (به عنوان مثال، view2d). هر خواندن و نوشتن مستقیماً در ناحیه زیرین حافظه که آرایه را می‌سازد (دوباره: سریع) انجام می‌شود، نه با استفاده از رابط‌های دسترسی به شی (باز هم: کند).

شاخص، تکرار نکنید، از طریق آرایه های NumPy

کاربران پایتون تاکنون می‌دانند که استعاره ترجیحی برای عبور از عناصر یک شی، برای آیتم در شیء است:. شما می توانید از این استعاره در Cython نیز استفاده کنید، اما هنگام کار با آرایه NumPy یا مموری ویو بهترین سرعت ممکن را ندارد. برای این کار، باید از نمایه سازی به سبک C استفاده کنید.

در اینجا مثالی از نحوه استفاده از نمایه سازی برای آرایه های NumPy آورده شده است:


# conventional Cython:
cimport cython
@cython.boundscheck(False)
@cython.wraparound(False)
def compute(int[:, ::1] array_1):
    # get the maximum dimensions of the array
    cdef Py_ssize_t x_max = array_1.shape[0]
    cdef Py_ssize_t y_max = array_1.shape[1]
    
    #create a memoryview
    cdef int[:, :] view2d = array_1

    # access the memoryview by way of our constrained indexes
    for x in range(x_max):
        for y in range(y_max):
            view2d[x,y] = something()


# pure-Python mode:  
import cython
@cython.boundscheck(False)
@cython.wraparound(False)
def compute(array_1: cython.int[:, ::1]):
    # get the maximum dimensions of the array
    x_max: cython.size_t = array_1.shape[0]
    y_max: cython.size_t = array_1.shape[1]
    
    #create a memoryview
    view2d: int[:,:] = array_1

    # access the memoryview by way of our constrained indexes
    for x in range(x_max):
        for y in range(y_max):
            view2d[x,y] = something()

در این مثال، ما از ویژگی .shape آرایه NumPy برای بدست آوردن ابعاد آن استفاده می کنیم. سپس از range() برای تکرار در مموری ویو با آن ابعاد به عنوان یک محدودیت استفاده می کنیم. ما اجازه دسترسی دلخواه به بخشی از آرایه را نمی دهیم، به عنوان مثال، از طریق یک متغیر ارسال شده توسط کاربر، بنابراین هیچ خطری برای خارج شدن از محدوده وجود ندارد.

همچنین متوجه خواهید شد که دکوراتورهای @cython.boundscheck(False) و @cython.wraparound(False) روی عملکردهای خود داریم. به‌طور پیش‌فرض، Cython گزینه‌هایی را فعال می‌کند که از خطا در دسترسی‌های آرایه محافظت می‌کنند، بنابراین شما به اشتباه خارج از محدوده‌های یک آرایه را مطالعه نکنید. با این حال، بررسی‌ها دسترسی به آرایه را کاهش می‌دهند، زیرا هر عملیاتی باید باند بررسی شود. استفاده از دکوراتورها باعث از کار افتادن این محافظ ها می شود و آنها را غیرضروری می کند. ما قبلاً تعیین کرده‌ایم که محدوده‌های آرایه چیست و از آنها عبور نمی‌کنیم.