כתיבת pperl מהיר#

שאר הפרקים במדריך זה עוסקים בהסרת עלות. פרק זה עוסק בהשגת ההאצות ש־pperl מציע ושאף Perl אחר אינו מציע: ה־JIT שמהדר לולאות חמות לקוד מקומי, והמקבילן שפורש לולאה כשירה על פני ליבות המעבד. שניהם אוטומטיים - אינכם קוראים להם, מסמנים עבורם או מגדירים אותם. אך הם נכנסים לפעולה רק על קוד מעוצב בצורה הנכונה, ופעולה אחת שאינה במקומה עלולה להשבית כל אחד מהם בשקט.

פרק זה הוא התוכנית החיובית: כתבו את הלולאה כך, והמהדר יקבל אותה. הפרקים JIT ו־ביצוע מקבילי מסבירים את המנגנון; פרק זה מספר לכם מה להקליד.

הצורה שה־JIT רוצה#

ה־JIT מכוון אל לולאות - for ו־while - שגופן עשוי חשבון והשוואה על משתנים מספריים. כשהוא מזהה צורה זו, הוא מהדר את הגוף לקוד מכונה מקומי ומחזיק את משתני הלולאה באוגרי המעבד במקום במחסנית המפרש. משם מגיעות ההאצות בסדר גודל.

לולאה שה־JIT מהדר בנקיון:

my $sum = 0;
for my $i (1 .. 1_000_000) {
    $sum += $i * $i;
}

חשבון של מספרים שלמים ושל נקודה צפה (+, -, *, /), השוואות (<, >, <=, >=, ==), ובקרת זרימה (if, last) בתוך הלולאה - כולם עוברים הידור. גם לולאות מקוננות עוברות הידור, כמה רמות לעומק, כשכל רמה מחוברת לרמה הבאה. הצורה הטבעית והאידיומטית היא הצורה המהירה - אינכם פורשים אותה, מעלים לתוך משתנים זמניים או מעוותים אותה בדרך אחרת.

מה מחזיר לולאה אל המפרש#

ה־JIT דוחה לולאה שגופה מבצע עבודה שאין ביכולתו לבטא בקוד מקומי. כאשר הוא דוחה, הלולאה רצה על המפרש - במהירות המפרש, שעדיין מהירה לעבודה לא־מספרית, אך ללא מכפיל הקוד המקומי. הפוסלים:

  • קריאות לתת־שגרה. ה־JIT מהדר גופי לולאה, לא מסגרות קריאה. לולאה שקוראת לתת־שגרה בכל איטרציה רצה בפירוש - תקורת הקריאה היא בדיוק הדבר שה־JIT אינו יכול להסיר. אם העבודה לכל איטרציה קטנה וחמה, שלבו אותה בתוך גוף הלולאה במקום לפצל אותה לתת־שגרה.

  • פעולות regex. למנוע ה־regex יש מיטוב משלו ונתיב משלו; התאמה או החלפה בגוף הלולאה אינה עוברת הידור JIT. ראו ביצועי regex לכוונון עבודה זו בתנאים שלה.

  • קלט/פלט. print, קריאת קובץ, קריאת מערכת - עבודה חסומת קלט/פלט אינה נהנית מהידור מקומי ואינה עוברת הידור.

  • גישה למבני נתונים מורכבים. פעולות על האש ומערך עם מפתחות דינמיים נופלות מחוץ למודל המספרי של ה־JIT.

אף אחד מאלה אינו שגיאה; הלולאה פשוט רצה בדרך המפורשת. אך אם לולאה שציפיתם שתהיה מהירה אינה כזו, הריצו אותה עם --no-jit מול ברירת המחדל (מדידה): אם הזמנים תואמים, ה־JIT כבר דחה אותה, ואחד מהמקרים שלעיל הוא הסיבה.

השאירו מחרוזות מחוץ ללולאות מספריות#

ה־JIT אכן מטפל בפעולת מחרוזת אחת - .= שרשור־השמה, וניקוי מחרוזת באמצעות $x = "" - דרך קריאות חזרה אל זמן הריצה. לכן לולאה שבונה מחרוזת עדיין עוברת הידור. אך זהו המקרה היחיד שבו צורת הלולאה משנה אילו ממטבים חלים, וזהו הכלל בעל הערך הגבוה ביותר בפרק זה.

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

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

# Parallelizable: pure numeric reduction.
my $total = 0;
for my $i (0 .. $n) {
    $total += $i * $i;
}

# Same arithmetic, but the string build demotes it to a
# single-core compiled loop - the parallel speedup is forfeited.
my $report = "";
my $total  = 0;
for my $i (0 .. $n) {
    $total  += $i * $i;
    $report .= "$i ";
}

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

הצורה שהמקבילן רוצה#

כאשר המיקבול מופעל - הוא פעיל כברירת מחדל; --no-parallel מכבה אותו - לולאה מהודרת־JIT משוגרת על פני threads אם הניתוח יכול להוכיח שהיא בטוחה. שני תנאים נושאים את המשקל הרב ביותר:

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

my $sum = 0;                    # declared outside the loop
for my $x (@data) {
    $sum += score($x);          # accumulate only → recognised as a reduction
}

אתחול מחדש של $sum בתוך הלולאה, או שזירת איפוס, מסכל את הזיהוי (הניתוח מחסר תבניות איפוס מתבניות צבירה) והלולאה רצה סדרתית. פירוט מלא בזיהוי רדוקציה.

גוף לולאה נקי מתופעות לוואי נצפות. המנתח שמרני במכוון. כל אחד מאלה פוסל לולאה משיגור מקבילי:

  • קלט/פלט (print, open, קריאות קובץ)

  • כתיבות למשתני חבילה/גלובליים

  • קריאות לתת־שגרה שלא הוכחו כטהורות

  • פעולות regex בעלות תופעות לוואי (s///)

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

פונקציות מובנות שמתמקבלות#

map ו־grep עם פונקציות חזרה טהורות על אוסף גדול מספיק יכולים לרוץ במקביל, תוך שמירת סדר הפלט:

my @out      = map  { expensive($_) } @big;
my @filtered = grep { costly_test($_) } @big;

אותם שערים חלים: פונקציית החזרה חייבת להיות נקייה מתופעות לוואי וממצב משותף־ומשתנה, והאוסף חייב לעבור את סף המיקבול (ניתן לכוונון באמצעות --parallel-threshold). טרנספורם טהור על מערך גדול הוא הזכייה המקבילית הקלה ביותר ש־pperl מציע - אין לולאה לעצב, רק פונקציית חזרה נקייה.

כאשר העבודה אינה לולאה#

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

  • בחרו בפונקציות מובנות מקומיות על פני לולאות שנכתבו ידנית במקום שהן קיימות - List::Util רץ במהירות של פונקציה מובנית.

  • החילו את האידיומים הקלאסיים שעדיין תקפים מתוך אידיומים: העלו ביטויים קבועים אל מחוץ ללולאה, הימנעו מהעתקות, הדרו regexp מראש.

  • המירו מיונים יקרים (מיון).

  • קראו ל־C דרך ה־FFI רק כאשר תדירות הקריאות נמוכה - עלות המרשלינג לכל קריאה הופכת אותו לכלי הלא־נכון בתוך לולאה פנימית חמה.

רשימת תיוג ללולאה חמה#

  1. האם הגוף עשוי חשבון והשוואה על משתנים מספריים? אם כן, ה־JIT מהדר אותו. אם לא, הפוסל הוא קריאה, regex, קלט/פלט או גישה דינמית לנתונים - טפלו בכך תחילה.

  2. האם יש פעולת מחרוזת בלולאה? אם כן, והלולאה היא בכל זאת רדוקציה מספרית, פצלו את עבודת המחרוזת החוצה כדי שהלולאה המספרית תוכל לרוץ במקביל.

  3. האם הצובר מוצהר מחוץ ללולאה ורק נצבר, לעולם לא מאופס? זה מה שהופך אותו לרדוקציה מזוהה.

  4. האם הגוף נקי מקלט/פלט, מכתיבות גלובליות ומקריאות לא־טהורות? זה מה שמאפשר ללולאה לרוץ במקביל.

  5. מדדו את ברירת המחדל מול --no-jit מול --no-parallel (מדידה) כדי לוודא איזה ממטב נכנס לפעולה.

ראו גם#

  • הידור JIT - צינור ההידור, מודל טיפוס־המשתנה, השמירה במטמון.

  • ביצוע מקבילי - ניתוח הרדוקציה, שער תופעות הלוואי, בקרות ה־thread.

  • מדידה - כיצד לוודא שה־JIT והמקבילן אכן נכנסים לפעולה על הקוד שלכם.

  • אידיומים - אילו מיטובים קלאסיים עדיין חשובים ואילו המהדר כבר מטפל בהם.

  • מיון - נתיב חם נפוץ אחד שה־JIT אינו מגיע אליו.