הנתיב לבדיקת סוג 4 מיליון שורות של קוד Python. חלק 2

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

הנתיב לבדיקת סוג 4 מיליון שורות של קוד Python. חלק 2

קרא את החלק הראשון

תמיכה בסוג רשמי (PEP 484)

ערכנו את הניסויים הרציניים הראשונים שלנו עם mypy ב-Dropbox במהלך Hack Week 2014. Hack Week הוא אירוע של שבוע אחד בהנחיית Dropbox. במהלך תקופה זו, העובדים יכולים לעבוד על מה שהם רוצים! כמה מהפרויקטים הטכנולוגיים המפורסמים ביותר של Dropbox החלו באירועים כמו אלה. כתוצאה מהניסוי הזה, הגענו למסקנה ש-mypy נראה מבטיח, למרות שהפרויקט עדיין לא מוכן לשימוש נרחב.

באותו זמן, הרעיון של סטנדרטיזציה של מערכות רמז מסוג Python היה באוויר. כפי שאמרתי, מאז Python 3.0 אפשר היה להשתמש בהערות טיפוס לפונקציות, אבל אלו היו רק ביטויים שרירותיים, ללא תחביר וסמנטיקה מוגדרים. במהלך ביצוע התוכנית, ההערות הללו, לרוב, פשוט התעלמו. אחרי ה-Hack Week, התחלנו לעבוד על סטנדרטיזציה של סמנטיקה. עבודה זו הובילה להופעתה PEP 484 (Guido van Rossum, Łukasz Langa ואני שיתפנו פעולה במסמך זה).

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

תחביר הרמז לסוג שאומץ בסופו של דבר היה דומה מאוד למה שתמכה mypy באותה תקופה. PEP 484 שוחרר עם Python 3.5 ב-2015. Python כבר לא הייתה שפה בהקלדה דינמית. אני אוהב לחשוב על האירוע הזה כעל אבן דרך משמעותית בהיסטוריה של פייתון.

תחילת ההגירה

בסוף 2015, Dropbox יצרה צוות של שלושה אנשים שיעבדו על mypy. הם כללו את גידו ואן רוסום, גרג פרייס ודיוויד פישר. מאותו רגע, המצב החל להתפתח במהירות רבה. המכשול הראשון לצמיחתה של mypy היה ביצועים. כפי שרמזתי למעלה, בימים הראשונים של הפרויקט חשבתי לתרגם את היישום mypy ל-C, אבל הרעיון הזה נחצה מהרשימה לעת עתה. נתקענו עם הפעלת המערכת באמצעות מתורגמן CPython, שאינו מהיר מספיק עבור כלים כמו mypy. (גם פרויקט PyPy, מימוש חלופי של Python עם מהדר JIT, לא עזר לנו).

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

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

זו הייתה תקופה של אימוץ מהיר וטבעי של בדיקת סוגים בדרופבוקס. עד סוף 2016, כבר היו לנו כ-420000 שורות של קוד Python עם הערות סוג. משתמשים רבים התלהבו מבדיקת סוגים. יותר ויותר צוותי פיתוח השתמשו ב- Dropbox mypy.

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

יותר פרודוקטיביות!

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

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

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

אפילו יותר פרודוקטיביות!

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

החלטנו לחזור לאחד הרעיונות הקודמים לגבי mypy. כלומר, להמיר קוד Python לקוד C. הניסוי עם Cython (מערכת המאפשרת לך לתרגם קוד שנכתב ב-Python לקוד C) לא העניק לנו שום מהירות נראית לעין, אז החלטנו להחיות את הרעיון של כתיבת מהדר משלנו. מכיוון שבסיס הקוד של mypy (שכתוב ב-Python) כבר הכיל את כל הערות הסוג הנדרשות, חשבנו שכדאי לנסות להשתמש בהערות אלו כדי להאיץ את המערכת. יצרתי במהירות אב טיפוס כדי לבדוק את הרעיון הזה. הוא הראה עלייה של יותר מפי 10 בביצועים במיקרו-בנצ'מרקים שונים. הרעיון שלנו היה להרכיב מודולי Python למודולי C באמצעות Cython, ולהפוך הערות טיפוס לבדיקות טיפוס בזמן ריצה (בדרך כלל מתעלמים מהערות טיפוס בזמן הריצה ומשמשות רק על ידי מערכות בדיקת טיפוס). למעשה תכננו לתרגם את המימוש של mypy מ-Python לשפה שתוכננה להקלדה סטטית, שתיראה (ולרוב, תעבוד) בדיוק כמו Python. (סוג זה של הגירה בין שפות הפך למסורת של פרויקט mypy. היישום המקורי של mypy נכתב ב-Alore, ואז היה הכלאה תחבירית של Java ו-Python).

התמקדות ב-API של הרחבת CPython הייתה המפתח לאיבוד יכולות ניהול פרויקטים. לא היינו צריכים ליישם מכונה וירטואלית או ספריות כלשהן ש-mypy הייתה צריכה. בנוסף, עדיין תהיה לנו גישה לכל המערכת האקולוגית של Python ולכל הכלים (כגון pytest). משמעות הדבר היא שנוכל להמשיך להשתמש בקוד Python מפורש במהלך הפיתוח, מה שמאפשר לנו להמשיך לעבוד עם דפוס מהיר מאוד של ביצוע שינויים בקוד ובדיקתו, במקום לחכות שהקוד יקמפל. זה נראה כאילו אנחנו עושים עבודה נהדרת בישיבה על שני כיסאות, כביכול, ואהבנו את זה.

המהדר, שקראנו לו mypyc (מכיוון שהוא משתמש ב-mypy כחזית לניתוח טיפוסים), התברר כפרויקט מוצלח מאוד. בסך הכל, השגנו מהירות של בערך פי 4 עבור ריצות תכופות של Mypy ללא שמירה במטמון. פיתוח הליבה של פרויקט mypyc לקח צוות קטן של מייקל סאליבן, איבן לבקיבסקי, יו האן ואני כ-4 חודשים קלנדריים. כמות העבודה הזו הייתה קטנה בהרבה ממה שהיה צריך כדי לשכתב את mypy, למשל, ב-C++ או Go. והיינו צריכים לבצע הרבה פחות שינויים בפרויקט ממה שהיינו צריכים לעשות כשכתבנו אותו מחדש בשפה אחרת. קיווינו גם שנוכל להביא את mypyc לרמה כזו שמתכנתי Dropbox אחרים יוכלו להשתמש בו כדי להדר ולהאיץ את הקוד שלהם.

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

כדי לזהות את הפעולות ה"איטיות" הנפוצות ביותר, ביצענו פרופיל קוד. חמושים בנתונים אלה, ניסינו או לצבוט את mypyc כך שהוא יפיק קוד C מהיר יותר עבור פעולות כאלה, או לשכתב את קוד Python המתאים באמצעות פעולות מהירות יותר (ולפעמים פשוט לא היה לנו פתרון פשוט מספיק לבעיה זו או אחרת) . שכתוב הקוד של Python היה לעתים קרובות פתרון קל יותר לבעיה מאשר שהמהדר יבצע אוטומטית את אותה טרנספורמציה. בטווח הארוך, רצינו להפוך הרבה מהטרנספורמציות הללו לאוטומטיות, אבל בזמנו התמקדנו בהאצת Mypy במינימום מאמץ. ובצעדנו לעבר המטרה הזו, חתכנו כמה פינות.

להמשך ...

קוראים יקרים! מה היו ההתרשמות שלך מפרויקט mypy כשנודע לך על קיומו?

הנתיב לבדיקת סוג 4 מיליון שורות של קוד Python. חלק 2
הנתיב לבדיקת סוג 4 מיליון שורות של קוד Python. חלק 2

מקור: www.habr.com

הוספת תגובה