ניבים#
ל־Perl נלווית פולקלור ביצועים בן עשורים - הרגלים שהשתלמו על המפרש של upstream והועברו הלאה ככללים. חלק מהעצות הללו נצחי. חלק מהן כיום חסר תועלת לחלוטין ב־pperl, משום ש־JIT או מקפל זמן־ההידור כבר עושים את העבודה. ויש כמה הרגלים חדשים החשובים כאן ושאף מתכנת Perl ב־upstream מעולם לא נזקק להם.
פרק זה ממיין את הניבים הקלאסיים לשלושה דליים:
עדיין תקף - העצה מחזיקה; העלות שהיא חוסכת אמיתית גם ב־pperl.
התייתר - המהדר או ה־JIT של pperl כבר מבטלים את העלות, כך שההתפתלות אינה קונה דבר ורק פוגעת בקריאוּת.
חדש - רלוונטי בגלל אופן ההידור של pperl, ללא תקדים ב־upstream.
הסתייגות גורפת חוצה את כל דלי ה“התייתר“: ה־JIT לוקח לולאה רק כשהלולאה מעוצבת עבורו. לולאה עמוסה בקריאות לשגרות, בהתאמות regexp או בבניית מחרוזות אינה מהודרת ורצה על המפרש - היכן שהעצה הקלאסית חלה שוב במלואה. כך ש“ה־JIT מטפל בזה“ נכון עבור לולאות שה־JIT מטפל בהן. כתיבת pperl מהיר הוא הפרק המשלים על הקפדה שהלולאה שלך היא אחת מהן.
עדיין תקף#
אלה שורדים את המעבר ל־pperl ללא שינוי. העלות שהם חוסכים משולמת על המפרש, ורוב הקוד החם נוגע במפרש היכנשהו.
הוצא עבודה אינווריאנטית אל מחוץ ללולאות. חישוב אותו ערך בכל איטרציה הוא עבודה מבוזבזת, בין אם הלולאה מפורשת ובין אם מהודרת. ה־JIT אינו מוציא עבורך ביטויים שרירותיים; הוצא את האינווריאנט בעצמך.
הימנע מהעתקות מיותרות של מבנים גדולים. העברת מערך גדול לפי ערך, או חיתוכו רק כדי לספור, גוררת העתקה. העבר הפניה, השתמש במשתנה לולאה בעל כינוי, או עבוד במקום. מודל הערכים של pperl נושא את אותן עלויות העתקה ש־Perl תמיד נשא.
הדר מראש regexp־ים בשימוש בתוך לולאה באמצעות
qr. מנוע ה־regex ומטמון ההידור שלו פועלים כמו ב־upstream; תבנית הנבנית מחדש בכל איטרציה משלמת על הידור חוזר. זהו תחום ה־regex - ראה ביצועי regex.בחר את מבנה הנתונים הנכון. חיפוש בהאש מנצח סריקה לינארית באותם גדלים שתמיד ניצח. הסיבוכיות האלגוריתמית אינה מושפעת משום סביבת ריצה; לולאת O(n²) איטית אפילו כשהיא מהודרת JIT במלואה.
קרא קבצים בנתחים בגודל הנכון. שורה־אחר־שורה לעומת slurp־ופיצול היא שאלת I/O, ו־I/O אינו עובר הידור JIT. שיקולי ה־upstream מועברים ישירות.
העדף פעולות רשימה מובנות על פני לולאות שנכתבו ביד היכן שהמובנה הוא נייטיב.
sum,min,max,firstמ־List::Utilרצים במהירות מובנית ללא שכבת XS (ראה מודולים נייטיביים). לולאת צובר שנכתבה ביד עשויה לעבור הידור JIT לאותה מהירות, אך המובנה הנייטיבי מגיע לשם ללא תלות בצורת הלולאה.
התייתר#
אלה היו רווחים אמיתיים ב־Perl של upstream. ב־pperl המהדר או ה־JIT כבר מסירים את העלות, כך שיישום הניב ביד אינו קונה דבר ולרוב עולה בבהירות.
שילוב־פנימי ידני של אריתמטיקה חמה#
ההרגל הישן של פריסת לולאה אריתמטית הדוקה, או שילוב פנימי ידני של עוזר אריתמטי קטן כדי לחמוק מתקורת הקריאה, מכוון לעלות שה־JIT מסיר. לולאת for או while מספרית מעל צוברי שלם או נקודה צפה מהודרת לקוד מכונה נייטיבי, כשמשתני הלולאה מוחזקים ברגיסטרים של ה־CPU - ראה הידור JIT. פריסה ידנית של לולאה כזו מאריכה אותה ומקשה על קריאתה מבלי לנצח את המהדר.
# Don't hand-unroll this. The JIT compiles the natural form to
# native code with $i and $sum in registers.
my $sum = 0;
for my $i (1 .. 1_000_000) {
$sum += $i * $i;
}
ההתפתלות משתלמת רק אם הלולאה אינה עוברת הידור JIT - ואם אינה עוברת, התיקון הוא לגרום לה לעבור הידור JIT (להסיר את הקריאה או פעולת המחרוזת שפסלה אותה), לא לפרוס אותה ביד.
שומרי רישום מבוססי קבוע DEBUG#
הניב של שמירת רישום יקר מאחורי קבוע שקרי בזמן הידור -
use constant DEBUG => 0;
# ...
warn "state: @stuff\n" if DEBUG;
אינו עולה דבר ב־pperl, שזו כל מהות הניב, ו־pperl מספק זאת במלואו. כאשר
DEBUGהוא קבוע שקרי, המהדר מקפל את שומר ה־if DEBUGבזמן ההידור: הפקודה המושבתת מתמוטטת לפעולה ריקה, ללא בדיקת תנאי וללא הסתעפות שנותרת בתוכנית המהודרת. ההשמה"@stuff"לעולם אינה נבנית. אתה יכול להשאיר רישום שמור בנתיבים חמים במצפון נקי; קבועDEBUGשקרי מוחק אותו.
זהו הניב הפועל כפי שתוכנן - המשך לכתוב אותו. מה שמתייתר הוא כל פיקחות נוספת מעל לכך: אינך צריך להפוך את הרישום להערה, להעביר אותו מאחורי שגרה, או להסיר אותו עבור הייצור. הקבוע עושה זאת.
קיפול ביטויים קבועים בעצמך#
חישוב מראש של תוצאה אריתמטית קבועה, או קריאה קבועה למובנה טהור, ואחסונה במשתנה כדי ”להימנע מחישובה מחדש“ אינו קונה דבר - המהדר מקפל ביטויים קבועים ומובנים טהורים בעלי ארגומנט קבוע (sin, cos, sqrt, chr, ord, וכדומה) בזמן ההידור, כל עוד הקריאה נמצאת בתחום תקף. my $two_pi = 2 * 3.14159265358979; ו־my $root = sqrt(2); מחושבים פעם אחת, בזמן ההידור, בין אם ”עזרת“ בכך שהוצאת אותם ובין אם לא.
הניואנס היחיד: הקיפול מדולג עבור ארגומנטים מחוץ לתחום של פונקציה (sqrt של שלילי, log של אפס), כך שאלה נופלים לזמן הריצה ומתים שם בדיוק כפי שראוי. לעולם אינך צריך לקפל קבוע ביד כדי להרוויח מהירות.
הסרת לולאות מתות בעלות תנאי קבוע#
while (0) { ... } או בלוק השמור בתנאי שקרי־קבוע מבוטל בזמן ההידור - הגוף מוסר, לא רק מדולג בזמן הריצה. (for (; 0; ...) בסגנון C מטופל מעט אחרת: לעולם לא נכנסים לגוף, אם כי פעולת התשתית של הלולאה עשויה להיוותר.) קוד שהודר־החוצה מותנה מאחורי קבוע שקרי אינו נושא משקל בזמן ריצה; אינך צריך למחוק אותו פיזית כדי להאיץ את התוכנית.
חדש#
לאלה אין מקבילה ב־upstream. הם חשובים בגלל אופן ההידור וההקבלה של pperl, וטעות בהם מחלטת בשקט את ההאצות הגדולות ביותר ש־pperl מציע.
שמור לולאות חמות מספריות מספריות#
ההרגל החדש בעל הערך הרב ביותר. ה־JIT מהדר לולאה מספרית, וכאשר ההקבלה מאופשרת (ברירת המחדל) והלולאה היא רדוקציה נקייה מעל טווח ידוע, מפזר אותה על פני threads - ההבדל בין האצה פי 76× לבין פי 431× במדד הביצועים Mandelbrot (ראה ביצוע מקבילי).
פעולת מחרוזת בתוך אותה לולאה מחלטת את החצי המקבילי. כאשר ה־JIT רואה משתנה מחרוזת בלולאה - יעד שרשור .=, צובר מחרוזת - הוא מוריד את הלולאה ללולאה מהודרת סדרתית: עדיין קוד נייטיבי, אך כבר לא מפוזר על פני threads.
# Numeric reduction - JIT-compiled and eligible for parallel dispatch.
my $total = 0;
for my $i (0 .. $n) {
$total += compute($i); # (only if compute() itself stays numeric/inlinable)
}
# A string built in the same loop demotes it to sequential JIT -
# the parallel dispatch is gated off the moment a string var appears.
my $log = "";
for my $i (0 .. $n) {
$total += $i * $i;
$log .= "$i "; # forfeits parallel dispatch for this loop
}
אם אתה זקוק גם לרדוקציה המספרית וגם לצבירת המחרוזת, פצל אותן לשתי לולאות: המספרית מוקבלת, ולולאת המחרוזת רצה כלולאה מהודרת סדרתית. עירובן עולה לך בהאצה המקבילית על העבודה המספרית ללא רווח על עבודת המחרוזת.
עצב רדוקציות כצבור־לעולם־אל־תאפס#
המקביל מזהה משתנה רדוקציה בכך שהוא רואה אותו נצבר ולעולם לא מאופס בתוך גוף הלולאה (הניתוח מחסר דפוסי איפוס מדפוסי צבירה; משתנה שהוא גם וגם אינו נחשב לרדוקציה). כתיבת הצובר שלך בצורה הפשוטה $sum += ..., מוצהר לפני הלולאה ולעולם לא מאותחל מחדש בתוכה, היא מה שמאפשר לתבוע את הלולאה לביצוע מקבילי.
my $sum = 0; # declared outside - a reduction
for my $x (@data) {
$sum += weight($x); # accumulate, never reset → parallelizable
}
אתחול מחדש של הצובר בתוך הלולאה, או שזירת איפוס, מסכל את הזיהוי והלולאה רצה סדרתית. הדבר מכוסה במלואו תחת זיהוי רדוקציה.
תן ללולאה לעבור הידור JIT - אל תסתיר אותה בתוך שגרה#
ה־JIT מהדר לולאות, לא גופי שגרות. לולאה מספרית חמה הכתובה בהיקף הקובץ או בתוך שגרה גדולה יותר מהודרת היטב; אותה אריתמטיקה לאחר ארגון מחדש כך שכל איטרציה היא קריאה לשגרה - לא - מסגרת הקריאה היא תחום המפרש, ותקורת הקריאה־לכל היא בדיוק מה שה־JIT אינו יכול להסיר. כאשר לולאה חמה ומספרית, שמור את גופה משולב במקום לחלק כל איטרציה לשגרה לשם סדר. חלק לשם בהירות במקומות אחרים; שמור את הלולאה הפנימית שטוחה.
ראו גם#
כתיבת pperl מהיר - התוכנית החיובית: כיצד לעצב לולאה כך שה־JIT והמקביל יקחו אותה.
מדידה - ודא שניב באמת הזיז את המספר לפני יישומו ואחריו.
הידור JIT - מה מהודר, מדוע שילוב־פנימי ידני של אריתמטיקה מיותר.
ביצוע מקבילי - ניתוח הרדוקציה ושער תופעות־הלוואי שמאחורי הניבים ה“חדשים“.
מיון - הניבים הספציפיים למיון (שוורצי, Guttman-Rosler) מקבלים פרק משלהם.