• בלוג
  • היום למדתי: שיפור ביצועים עם Metadata ב Clojure

היום למדתי: שיפור ביצועים עם Metadata ב Clojure

09/05/2023

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

בקריאה רגילה של קוד אנחנו לא נראה מתי הקומפיילר משתמש ב Reflection ומתי לא, אבל אפשר לבקש ממנו להדפיס אזהרות על שימוש כזה בעזרת המשתנה *warn-on-reflection*. בפרויקט לניגן נגדיר אותו עם:

:global-vars {*warn-on-reflection* true}

ובתוך ה repl אפשר להפעיל פשוט:

(set! *warn-on-reflection* true)

כדי להדליק את האזהרות.

עכשיו עם המשתנה דלוק אני יכול לראות שבקוד הבא קלוז'ר מבין בקלות מה הקלאס של str:

=> (set! *warn-on-reflection* true)
=> (let [s "hello there"] (.charAt s 3))
\l

אבל בקוד הזה אין לו מושג ולכן הוא צריך להשתמש ב Reflection כדי לזהות בזמן ריצה את הקלאס:

=> (set! *warn-on-reflection* true)
=> (let [s (apply str "hello")] (.charAt s 3)
Reflection warning, NO_SOURCE_PATH:1:30 - call to method charAt can't be resolved (target class is unknown).
\l

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

אנחנו מדביקים Metadata לערך עם המקרו המיוחד ^, ומושכים את ה metadata עם פונקציית meta. הקוד הבא מגדיר את המשתנה x להחזיק רשימה עם המספרים 1,2,3 ומשייך אליו את ה Metadata שנקרא :yay:

user=> (def x ^:yay [1 2 3])
#'user/x
user=> x
[1 2 3]
user=> (meta x)
{:yay true}

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

user=> (set! *warn-on-reflection* true)
user=> (let [s ^String (apply str "hello")] (.charAt s 3))
\l

עכשיו אני יודע מה אתם חושבים ... מה אם ננסה לעבוד על הקומפיילר ונשייך Metadata לא נכון? מסתבר שאם ה Type Hint לא מתאים קלוז'ר יוותר על האופטימיזציה וינסה למצוא את הפונקציה בזמן ריצה:

user=> (let [s ^Integer (apply str "hello")] (.charAt s 3))
Reflection warning, NO_SOURCE_PATH:1:39 - call to method charAt on java.lang.Integer can't be resolved (no such method).
\l