מדריך Cython: כיצד להאיץ את Python

פייתון היא שפת תכנות עוצמתית שקל ללמוד אותה וקל לעבוד איתה, אך היא לא תמיד מהירה ביותר להפעלה - במיוחד כשמתמודדים עם מתמטיקה או סטטיסטיקה. ספריות צד שלישי כמו NumPy, שעוטפות ספריות C, יכולות לשפר את הביצועים של פעולות מסוימות באופן משמעותי, אך לפעמים אתה פשוט צריך את המהירות והעוצמה הגולמיים של C ישירות ב- Python.

Cython פותח כדי להקל על כתיבת סיומות C עבור Python, ולאפשר הפיכת קוד פיתון קיים ל- C. מה עוד, Cython מאפשר לשלוח את הקוד המותאם עם יישום Python ללא תלות חיצונית.

במדריך זה נעבור בין השלבים הדרושים להפיכת קוד פייתון קיים לסיתון ושימוש בו ביישום ייצור.

סרטון קשור: שימוש בסיתון כדי להאיץ את פייתון

דוגמא לסיתון

נתחיל בדוגמה פשוטה שנלקחה מתיעוד של Cython, יישום לא מאוד יעיל של פונקציה אינטגרלית:

def f (x):

    החזר x ** 2-x

def integrate_f (a, b, N):

    s = 0

    dx = (ba) / N

    עבור אני בטווח (N):

        s + = f (a + i * dx)

    החזר s * dx

הקוד קל לקריאה ולהבנה, אך הוא פועל לאט. הסיבה לכך היא שפייתון חייב להמיר כל הזמן הלוך ושוב בין סוגי האובייקטים שלו לבין סוגי המספרים הגולמיים של המכונה.

שקול כעת את גרסת הסיתון של אותו קוד, כאשר התוספות של סייתון מודגשות:

 cdef f (x כפול):

    החזר x ** 2-x

def integrate_f (כפול a, כפול b, int N):

    cdef int i

    cdef כפול s, x, dx

    s = 0

    dx = (ba) / N

    עבור אני בטווח (N):

        s + = f (a + i * dx)

    החזר s * dx

תוספות אלו מאפשרות לנו להכריז במפורש על סוגים משתנים לאורך כל הקוד, כך שממהד הסיטון יכול לתרגם את התוספות ה"מעוטרות "הללו ל- C. 

סרטון קשור: כיצד פיתון מקל על התכנות

מושלם עבור IT, פייתון מפשט סוגים רבים של עבודה, מאוטומציה של המערכת ועד לעבודה בתחומים חדישים כמו למידת מכונה.

תחביר קיתון

מילות המפתח המשמשות לקישוט קוד הסיתון אינן נמצאות בתחביר פייתון קונבנציונאלי. הם פותחו במיוחד עבור Cython, כך שכל קוד המעוטר בהם לא יפעל כתוכנית פייתון קונבנציונאלית.

אלה האלמנטים הנפוצים ביותר בתחביר של Cython:

סוגים משתנים

חלק מהסוגים משתנה המשמשים Cython הם הדים של הסוגים עצמו Python, כגון  int, float, ו long. סוגים אחרים של משתני סיטון נמצאים גם ב- C, כמו charאו struct, כמו גם הצהרות כמו unsigned long. ואחרים ייחודיים לסיתון, כמו bintייצוג ברמת C של True/Falseערכי פיתון .

cdefואת cpdefסוגי פונקציה

cdefמילת המפתח מציין את השימוש בסוג Cython או C. הוא משמש גם להגדרת פונקציות כפי שהיית עושה בפייתון.

פונקציות שנכתבות בסיתון באמצעות defמילת המפתח של פייתון נראות לקוד פייתון אחר, אך נקלעות לקנס ביצועים. פונקציות המשתמשות cdefבמילת המפתח גלויות רק לקוד Cython או C אחר, אך מבוצעות הרבה יותר מהר. אם יש לך פונקציות הנקראות רק באופן פנימי מתוך מודול Cython, השתמש cdef.

מילת מפתח שלישית,, cpdefמספקת תאימות הן לקוד פייתון והן לקוד C, באופן שקוד C יכול לגשת לפונקציה המוצהרת במלוא המהירות. נוחות זו כרוכה בעלות, אם כי:  cpdefפונקציות מייצרות יותר קוד ויש להן מעט יותר תקורה מאשר cdef.

מילות מפתח אחרות של סיטון

מילות מפתח אחרות ב- Cython מספקות שליטה בהיבטים של זרימת התוכנית והתנהגות שאינן זמינות ב- Python:

  • gilו nogil. אלה מנהלי הקשר המשמשים לתיחום קטעי קוד הדורשים ( with gil:) או שאינם דורשים ( with nogil:) נעילת המתורגמן הגלובלית של Python, או GIL. קוד C שלא מבצע שיחות ל- API של Python יכול לרוץ מהר יותר nogilבבלוק, במיוחד אם הוא מבצע פעולה ארוכת טווח כמו קריאה מחיבור רשת.
  • cimportזה מכוון את Cython לייבא סוגי נתונים, פונקציות, משתנים וסוגי סיומות של נתונים. יישומי Cython המשתמשים במודולי C מקוריים של NumPy, למשל, משתמשים cimportבכדי לקבל גישה לפונקציות אלה.
  • include. זה ממקם את קוד המקור של קובץ Cython אחד בתוך אחר, באותו אופן כמו ב- C. שים לב של- Cython יש דרך מתוחכמת יותר לשתף הצהרות בין קבצי Cython שאינם רק includes.
  • ctypedef. משמש להתייחסות להגדרות סוג בקבצי כותרת C חיצוניים.
  • extern. משמש עם cdefלהתייחס לפונקציות C או משתנים שנמצאים במודולים אחרים.
  • public/api. משמש להכנת הצהרות במודולי Cython שיהיו גלויים לקוד C אחר.
  • inline. המשמש לציון פונקציה נתונה צריך להיות מוטבע, או שהקוד שלה יונח בגוף פונקציית הקריאה בכל פעם שהיא משמשת, למען המהירות. לדוגמה, fניתן לקשט inlineאת הפונקציה בדוגמת הקוד שלעיל כדי להפחית את תקורה של שיחת הפונקציה, מכיוון שהיא משמשת רק במקום אחד. (שים לב שמהדר C עשוי לבצע את ההטבעה שלו באופן אוטומטי, אך inlineמאפשר לך לציין במפורש אם משהו צריך להיות מונח.)

אין צורך להכיר את כל מילות המפתח של Cython מראש. קוד הסיטון נוטה להיכתב באופן הדרגתי - תחילה כותבים קוד פייתון תקף, ואז מוסיפים קישוט לסיתון כדי להאיץ אותו. כך תוכלו לאסוף את תחביר מילות המפתח המורחב של Cython באופן חלקי, לפי הצורך.

קומפילציה של סיטון

עכשיו, כשיש לנו מושג איך נראית תוכנית Cython פשוטה ומדוע היא נראית כפי שהיא נראית, בואו נעבור בין השלבים הדרושים להרכבת Cython לבינארי עובד.

כדי לבנות תוכנית Cython עובדת, נצטרך שלושה דברים:

  1. המתורגמן של פייתון. השתמש בגרסת המהדורה האחרונה, אם אתה יכול.
  2. חבילת הסיתון. אתה יכול להוסיף Cython ל- Python באמצעות pipמנהל החבילה:pip install cython
  3. מהדר AC.

Item #3 can be tricky if you’re using Microsoft Windows as your development platform. Unlike Linux, Windows doesn’t come with a C compiler as a standard component. To address this, grab a copy of Microsoft Visual Studio Community Edition, which includes Microsoft’s C compiler and costs nothing. 

Note that, as of this writing, the most recent release version of Cython is 0.29.16, but a beta version of Cython 3.0 is available for use. If you use pip install cython, the most current non-beta version will be installed. If you want to try out the beta, use pip install cython>=3.0a1 to install the most recent edition of the Cython 3.0 branch. Cython’s developers recommend trying the Cython 3.0 branch whenever possible, because in some cases it generates significantly faster code.

Cython programs use the .pyx file extension. In a new directory, create a file named num.pyx that contains the Cython code example shown above (the second code sample under “A Cython example”) and a file named main.py that contains the following code:

from num import integrate_f

print (integrate_f(1.0, 10.0, 2000))

This is a regular Python progam that will call the integrate_f function found in num.pyx. Python code “sees” Cython code as just another module, so you don’t need to do anything special other than import the compiled module and run its functions.

Finally, add a file named setup.py with the following code:

from distutils.core import setup from distutils.extension import Extension from Cython.Build import cythonize ext_modules = [ Extension( r'num', [r'num.pyx'] ), ] setup( name="num", ext_modules=cythonize(ext_modules),

)

setup.py is normally used by Python to install the module it’s associated with, and can also be used to direct Python to compile C extensions for that module. Here we’re using setup.py to compile Cython code.

If you’re on Linux, and you have a C compiler installed (typically the case), you can compile the .pyx file to C by running the command: 

python setup.py build_ext --inplace

If you’re using Microsoft Windows and Microsoft Visual Studio 2017 or better, you’ll need to make sure you have the most recent version of setuptools installed in Python (version 46.1.3 as of this writing) before that command will work. This ensures that Python’s build tools will be able to auto-detect and use the version of Visual Studio you have installed.

If the compilation is successful, you should see new files appear in the directory: num.c (the C file generated by Cython) and a file with either a .o extension (on Linux) or a .pyd extension (on Windows). That’s the binary that the C file has been compiled into. You may also see a \build subdirectory, which contains the artifacts from the build process.

Run python main.py, and you should see something like the following returned as a response:

283.297530375

That’s the output from the compiled integral function, as invoked by our pure Python code. Try playing with the parameters passed to the function in main.py to see how the output changes.

Note that whenever you make changes to the .pyx file, you will need to recompile it. (Any changes you make to conventional Python code will take effect immediately.)

The resulting compiled file has no dependencies except the version of Python it was compiled for, and so can be bundled into a binary wheel. Note that if you refer to other libraries in your code, like NumPy (see below), you will need to provide those as part of the application’s requirements.

How to use Cython

Now that you know how to “Cythonize” a piece of code, the next step is to determine how your Python application can benefit from Cython. Where exactly should you apply it?

For best results, use Cython to optimize these kinds of Python functions:

  1. Functions that run in tight loops, or require long amounts of processing time in a single “hot spot” of code.
  2. Functions that perform numerical manipulations.
  3. Functions that work with objects that can be represented in pure C, such as basic numerical types, arrays, or structures, rather than Python object types like lists, dictionaries, or tuples.

Python has traditionally been less efficient at loops and numerical manipulations than other, non-interpreted languages. The more you decorate your code to indicate it should use base numerical types that can be turned into C, the faster it will do number-crunching.

Using Python object types in Cython isn’t itself a problem. Cython functions that use Python objects will still compile, and Python objects may be preferable when performance isn’t the top consideration. But any code that makes use of Python objects will be limited by the performance of the Python runtime, as Cython will generate code to directly address Python’s APIs and ABIs.

Another worthy target of Cython optimization is Python code that interacts directly with a C library. You can skip the Python “wrapper” code and interface with the libraries directly.

However, Cython does not automatically generate the proper call interfaces for those libraries. You will need to have Cython refer to the function signatures in the library’s header files, by way of a cdef extern from declaration. Note that if you don’t have the header files, Cython is forgiving enough to let you declare external function signatures that approximate the original headers. But use the originals whenever possible to be safe.

One external C library that Cython can use right out of the box is NumPy. To take advantage of Cython’s fast access to NumPy arrays, use cimport numpy (optionally with as np to keep its namespace distinct), and then use cdef statements to declare NumPy variables, such as cdef np.array or np.ndarray.

Cython profiling

The first step to improving an application’s performance is to profile it—to generate a detailed report of where the time is being spent during execution. Python provides built-in mechanisms for generating code profiles. Cython not only hooks into those mechanisms but has profiling tools of its own.

Python’s own profiler, cProfile, generates reports that show which functions take up the most amount of time in a given Python program. By default, Cython code doesn’t show up in those reports, but you can enable profiling on Cython code by inserting a compiler directive at the top of the .pyx file with functions you want to include in the profiling:

# cython: profile=True

You can also enable line-by-line tracing on the C code generated by Cython, but this imposes a lot of overhead, and so is turned off by default.

Note that profiling imposes a performance hit, so be sure to toggle profiling off for code that is being shipped into production.

Cython can also generate code reports that indicate how much of a given .pyx file is being converted to C, and how much of it remains Python code. To see this in action, edit the setup.py file in our example and add the following two lines at the top:

import Cython.Compiler.Options

Cython.Compiler.Options.annotate = True

(Alternatively, you can use a directive in setup.py to enable annotations, but the above method is often easier to work with.)

מחק את .cהקבצים שנוצרו בפרויקט והפעל מחדש את setup.pyהסקריפט כדי להרכיב מחדש הכל. לאחר שתסיים, אתה אמור לראות קובץ HTML באותה ספריה המשתפת את שם קובץ ה- .pyx שלך - במקרה זה  num.html,. פתח את קובץ ה- HTML ותראה את החלקים בקוד שלך שעדיין תלויים ב- Python מודגשים בצהוב. אתה יכול ללחוץ על האזורים הצהובים כדי לראות את קוד C הבסיסי שנוצר על ידי Cython.