6.19. ביצועים¶
אותן החלטות תכן שהופכות את numpy למהיר על המצלמה – קריאות ספרייה על מערך שלם, חוצצים ארוזים בעלי טיפוס, ותצוגות שחולקות נתונים עם מקורן – חושפות גם קבוצה של הרגלים שכדאי להכיר. הדף צורה וצעדים (strides) כבר כיסה את כלל פריסת הציר האחרון; דף זה מקטלג את הרגלי ההקצאה והטיפוס (dtype) החשובים ביותר בלולאת זרימה.
6.19.1. בחרו dtype סביר¶
ה-dtype ברירת המחדל של כל בנאי הוא float. עבור נתונים שהם מטבעם 8 סיביות או 16 סיביות – דגימות ADC, פיקסלים של תמונה, קריאות חיישן – העבירו dtype= במפורש לאחד מטיפוסי המספרים השלמים:
adc = np.array(adc_samples, dtype=np.uint16)
החיסכון ב-RAM הוא פי 2 עבור uint16 ופי 4 עבור uint8 לעומת ברירת המחדל float בת 4 הבתים. החישוב גם רץ מהר יותר משום שנתיבי הקוד למספרים שלמים בתוך numpy הדוקים יותר מאלה הגנריים למספרים ממשיים. כלל הגלישה של מספרים שלמים שתואר ב-Dtypes חל – המירו לטיפוס רחב יותר לפני פעולות חשבון שעלולות לגלוש.
6.19.2. העדיפו ndarray על פני iterable¶
רוב הצמצומים והפונקציות האוניברסליות מקבלים או iterable או ndarray
np.sum([1, 2, 3, 4, 5]) # works, but slow
np.sum(np.array([1, 2, 3, 4, 5])) # ~3x faster
צורת ה-iterable מאלצת את numpy לעבור על הקלט אובייקט פייתון אחד בכל פעם, ולהמיר כל אחד למספר לפני שניתן להשתמש בו. מול ndarray ההמרה כבר בוצעה והקריאה רצה ישירות דרך החוצץ הארוז.
כאשר אותם נתונים משמשים יותר מפעם אחת, בנו את ה-ndarray פעם אחת והעבירו אותו הלאה. כאשר הנתונים קיימים רק כרשימת פייתון ונצרכים פעם אחת, עלות ההמרה עלולה לגבור על שיפור המהירות – הבנאי array() עצמו צריך לעבור על הרשימה ולהקצות.
6.19.3. העדיפו תצוגות על פני העתקים¶
חיתוך, אינדוקס לפי ציר יחיד של מערך מדרגה גבוהה יותר, reshape(), transpose() ו-frombuffer() כולם מחזירים תצוגות שחולקות נתונים עם המקור. הן בעצם חינמיות.
copy(), flatten(), אינדוקס בוליאני (a[mask]), וכל ביטוי חשבוני מקצים העתק. השתמשו בהם רק כאשר באמת נדרש חוצץ עצמאי.
כשמתעורר ספק, ndinfo() מדפיס את מיקום החוצץ הבסיסי; שני מערכים המדווחים על אותה כתובת חולקים את נתוניהם. טבלת תצוגה-מול-העתק המלאה נמצאת ב-תצוגות והעתקים.
6.19.4. הקצו פעם אחת, ואז כתבו¶
מלכודת הביצועים הגדולה ביותר על המצלמה היא הקצאת מערכים חדשים בתוך לולאה הרצה פעמים רבות בשנייה. כל ndarray חדש מבקש מהמצלמה זיכרון RAM, והקצאות חדשות תכופות מבזבזות אותו.
רוב הפונקציות האוניברסליות מקבלות out= כך שניתן לכתוב את התוצאה לתוך מערך שכבר קיים:
x = np.linspace(0, 2 * np.pi, num=512)
y = np.zeros(512) # allocate once
while True:
np.sin(x, out=y)
# use y ...
image.Image.to_ndarray() מקבל buffer= מאותה סיבה; spectrogram() והממירים מסוג from_int32_buffer() מקבלים גם out= וגם scratchpad=. הקצו הכול פעם אחת והשתמשו בו מחדש.
6.19.5. השתמשו באופרטורים במקום (in-place)¶
b = b + 1 מקצה משתנה זמני בגודל b, מעתיק, ומשייך מחדש. b += 1 משנה את b ישירות:
# makes a temporary
b = b + 1
# no temporary
b += 1
אותו רעיון חל על ביטויים מורכבים. a + b * c מקצה משתנה זמני עבור b * c. פיצול הביטוי לתת-השמות פשוטות הכותבות לתוך חוצץ שהוקצה מראש מבטל את המשתנים הזמניים:
# one temporary for (a + b), another for the ``* 2``
out = (a + b) * 2
# zero temporaries
out[:] = a
out += b
out *= 2
6.19.6. בנו את התוצאה, אל תוסיפו אליה (append)¶
ל-ndarray אין append – בכוונה. הגדלת מערך תחייב הקצאת חוצץ חדש וגדול יותר והעתקת התוכן הישן לתוכו. על מיקרו-בקר, הקצו מראש את הגודל הסופי ומלאו אותו:
out = np.zeros(N, dtype=np.float)
for i in range(N):
out[i] = some_calculation(i)
כאשר N באמת אינו ידוע מראש, כתבו אל list של פייתון והמירו פעם אחת בסוף עם array().
6.19.7. השמה לפרוסה במקום מערכים חדשים¶
תבניות רבות של ”בניית מערך חדש מחלקים של אחרים“ ניתנות לביטוי כהשמות לפרוסות בתוך חוצץ שהוקצה מראש במקום הקצאה חדשה בכל קריאה.
חלון מתגלגל על פני זרם דגימות – היסוד של מסנן ממוצע נע – הוא המקרה הקנוני. החוצץ מחזיק את N הדגימות האחרונות; כל איטרציה משמיטה את הישנה ביותר ומוסיפה את החדשה ביותר. הצורה המתבקשת בונה מחדש את החוצץ בכל איטרציה:
while True:
sample = read_sample()
buf = np.concatenate((buf[1:], # new buffer every loop
np.array([sample])))
avg = np.mean(buf)
זוהי הקצאה חדשה – והעתק של N - 1 איברים – בכל דגימה. צורת ההשמה-לפרוסה מסיטה במקום:
N = 16
buf = np.zeros(N, dtype=np.float) # allocate once
while True:
sample = read_sample()
buf[:-1] = buf[1:] # shift left by one
buf[-1] = sample # append at the end
avg = np.mean(buf)
buf[:-1] = buf[1:] היא השורה המעניינת: שתי תצוגות חופפות לתוך אותו חוצץ, הפרוסה הימנית נקראת מקצה אחד ונכתבת לקצה האחר. numpy עובר על הזיכרון הבסיסי בסדר ההופך את ההסטה במקום לבטוחה. שום מערך חדש אינו מוקצה בתוך הלולאה.
6.19.8. היזהרו ממסכות בוליאניות בלולאות זרימה¶
אינדוקס בוליאני ו-where() מייצרים מערך חדש בכל קריאה – גודל התוצאה תלוי בנתונים, ולכן שום חוצץ שהוקצה מראש אינו יכול לקלוט את ההקצאה. בניית מסכה חוזרת ונשנית בלולאת זרימה ממלאת את ה-RAM במערכים חד-פעמיים. קריאת gc.collect() תקופתית משחררת את המקום:
import gc
for i in range(1000):
mask = a < threshold
_ = a[mask]
if i % 100 == 0:
gc.collect()
אותה הסתייגות חלה על ביטויים בוליאניים מורכבים כמו (a > lo) & (a < hi) – כל אופרטור מקצה מערך bool חדש. כאשר מסכה משמשת מחדש, בנו אותה פעם אחת ושמרו אותה:
mask = a < threshold
foo[mask] = 0
bar[mask] = 1