הבלוג של ארתיום
בלוג על לינוקס, תוכנה חופשית, מוזיקה, סלסה, ומה לא!
כמה התכנה שוקלת או ניהול זכרון
בעקבות המאמר של עידו על גודל של קובץ ריצה בפסקל ראיתי שיש מספר אי הבנות בנושא בכמה זיכרון התכנה משתמשת?
כיצד קבצי־ריצה וספריות נטענות?
כל תכנה או ספריה נטענת לזיכרון ע"י קריאה לפונקצית מערכת mmap (או פונקציות במערכות אחרות). מדוע mmap ולא קריאה לפונקציה כמו read? הסיבה היא פשוטה. נקח לדוגמה bash. אחת התכנות ה"נטענות" ביותר בלינוקס. כשאני קורא לפונקציה mmap על קובץ הריצה של bash בפעם הראשונה, המערכת מקצה דפי זיכרון וטוענת את הקובץ אליהן, אבל כש־mmap נקראת בפעם השנייה, דפי־הזיכרון שהתכנה מקבלת הם בדיוק אותם דפים פיזיים שהיו לפני. לכן, גם אם יש לי 10 תכנות של bash רצות במקביל, בפועל, היא תיטען רק פעם אחת (ליתר דיוק, רק קוד הריצה שלה ייטען פעם אחת).
אותו דבר קורא לספריות. כשתכנה א' טוענת ספריית gtk ותכנה ב' טוענת את הספרייה, הקוד נשאר משותף רק הנתונים הם שונים.
אז בואו נראה לדוגמה טבלה: מערכת בה רצות 3 תכנות: 2 bash ו־1 calc. נסתכל ככמה כל אחת משתמשת וכמה השימוש קיים בפועל.
זיכרון וירטואלי
Process Data Code Libraries Sum
----------------------------------------------
bash 1 1K, /bin/bash 10K, /lib/libc 10k 21k
bash 2 1K, /bin/bash 10K, /lib/libc 10K 21k
calc 1 1K, /bin/calc 5K, /lib/libc 10K 16k
----------------------------------------------
Seems to be 57K
זיכרון פיזי
Real Memory
---------------
/bin/bash 10K
/bin/calc 5K
/lib/libc 10K
data1 1K
data2 1K
data3 1K
--------------
overall 28K
לכן, הוצאה של כל קוד משותף לספריות דינמיות זאת פעולה שבסופו של דבר חוסכת זיכרון. למשל, כל יישומי KDE בסופו של דבר יטענו עותק יחיד של kdelib ועותק יחיד של qt.
עכשיו קצת פרטים טכניים למי שמעוניין.
איך אותו הקוד רץ בתכנות שונות --- או כיצד זה עובד?
בצורה פשטנית, למשל, בקוד יש פקודה תפנה מקום X או תעשה קפיצה למקום Y. אם אותו הקוד בלי שינויים טעון לתכנות שונות אז אולי גם X ו־Y הם יכולים להיות מקומות שונים?
כאן מתחיל ההבדל בין לינוקס ל־Windows.
בלינוקס (וגם במערכות רבות אחרות) כשבונים ספרייה דינמית, דואגים שהקוד יהיה בלתי תלוי במקום הריצה כלומר, הקוד מקומפל בצורה כזו שכל הרכיבים שהם תלויי מיקום הוצאו לקטע נפרד ששומר נתונים. כך, הקוד נטען בלי שינויים ומשותף בין תהליכים שונים בצורה שקופה.
למשל, ב־calc פונקציה malloc יכולה לשבת בכתובת שונה מ־malloc ב־bash. למעשה, צורת העבודה הזו היא כמו ביצוע link מלא בזמן הריצה.
ב־dll של Windows הקוד הוא כן תלוי מקום. לכל ספריה נבחר מיקום אקראי שבה היא ממוקמת מלכתחילה. כך, כשהספריות נטענות, אז בכל תכנה היא נטענת לאותה כתובת בדיוק. כשמספר הספריות מאוד גדול יכולות לקרות גם התנגשויות. כלומר שתי ספריות שונות אמורות להיטען לאותה כתובת בדיוק. במקרה כזה, מערכת הפעלה מבצע relocation --- הזזה של הקוד כדי למנוע התנגשות.
בסופו של דבר, שתיהן עושות את אותה העבודה בצורה שונה -- אחד יותר טוב ושני פחות טוב.
תגובות
עניין שאף אחד לא הקשיב למה שאמרתי הוא שהמדיד שלכם לא נכון. אתם מודדים את פסקל לפי GCC ולא לפי FPC. אתם שומעים "קובץ סטטי" ומיד יש לכם מגוון רעיונות וסיבות למה הדברים לא עובדים בצורה נכונה וטובה, אבל אף אחד לא טרח לבוא ולבדוק את הדברים שלו מול ההצגה שלי, כי לכולם יש דעה בנושא שהם למדו בה.
ד"א המדידה של mmap לפי strace היא לא נכונה (בגלל שהמערכת הפעלה אחראית על הרצת הקוד שלך והיא מבצעת mmap משל עצמה).
הדבר העיקרי שלמדתי מהפוסט הוא שצריך להכניס נושאים כבדים במנות קטנות ולהאכיל אנשים בכפית, אחרת אי אפשר להראות להם גישות שונות אם זה מגיע על חשבון ידע/תחושה שלהם שהדברים צריכים לעבוד בצורה שונה.
עידו -- אני אקבל את זה אם תגיד ש־FPC המציאו את ld.so משלהם. ואם הם כן, אז מצטער, חבל, הם המציאו גלגל מחדש.
אגב, הסיבה שרובנו "נעולים" היא שבתחום שפות מקומפלות אין כל כך המצאות כבר משנות ה־90
העניין הוא לא החידושים של המהדרים, אלא המימוש שלהם להמצאות, והצורה שהם ניגשים לדברים. עם כל הכבוד לGCC, יש לו הרבה חוסרים אשר כן מתממשים ב FPC. בFPC יש לך smart linking אשר עושה הרבה פעולות ממש חשובות למידע הבינארי שלך שGCC פשוט לא עושה. אחד הדברים ש smart linking יודע לעשות זה לדעת מה העיקר ומה הטפל בקוד שלך. כלומר אם יצרת עכשיו 100 פונקציות ואתה משתמש רק באחת מהן, הוא לא יכניס לך את כל 100 הפונקציות אלא הוא יכניס לך רק פונקציה אחת לקובץ הבינארי. הוא יודע למפות בזכות זה הרבה יותר דברים כדוגמת מיפוי זכרון של פונקציות בצורה נכונה ודינאמית גם כאשר הקוד שלך הוא סטטי (ובדיוק על זה דיברתי אצלי בבלוג).
יש לך הרבה דברים שאתה רגיל מ GCC והגישה שלו, שלא מתקיימים במהדר C/++ אחר (או מנוע מהדרים אחר) כדוגמת המהדר של אינטל שנחשב למאוד אמין ולמאוד יעיל מבחינת הקוד שהוא יוצר.
אחד הדברים שניסיתי הלסביר לכם הוא שהתפיסת עולם שלכם תפוסה במה שאתם מכירים, ואין לכם מקום לפי הגישה הזו לצורה שונה של איך שדברים מתבצעים.
עוד פרט חשוב הוא שלפעמים התחביר של השפה מתורגם בצורה שונה. למשל הרבה יותר יעיל בC אם אתה רוצה לעשות פעולות חיבור לעבוד עם case בצורה הבאה:
switch num case 1 : num ++ case 2 : num ++ case 3 : num ++ ... }
שים לב שבכוונה לא הוספתי break. הפעולה הזו הרבה יותר יעילה מאשר לולאת for למשל. אבל בפסקל אין לך גישה כזו, וגם הלולאת for שונה לגמרי, מה שאומר שהגישה לבעיה שונה לגמרי, ולכן המהדר יטפל בבעיות האלו בצורות שונות ! שוב, לא מדובר בהמצאה חדשה, אלא הכרח של שפה.
אותו הדבר בגישה שלך מול שפות דינמיות. לשפות דימיות יש מקום מאוד חשוב. הן מסוגלות לתת לך לפתח בצורה מאוד מהירה פתרונות שאתה לא יכול לעשות בדרך אחרת באותה המהירות. וכאשר מהירות הריצה היא לא הגורם המשמעותי שלך, התיעדוף שלך לשפה דינמית מתאימה אמורה להיות גובהה יותר (כבר דיברתי על להתאים כלי לבעיה ולא את הבעיה לכלי).
לצערי הדפדפן קרס (או ליתר דיוק adobe reader) בזמן כתיבת התגובה המושקעת והארוכה, לכן אני מקצר:
שוב, אני מדבר על עולם בו יש המון ספריות וכולן נמצאות בשימוש. לכן, ב־99% מהמקרים כשאתה טוען ספריה של 100 פונקציות כדי להשתמש ב־10 מהן, בפועל, אתה רק מתחבר אליה כי כבר מישהו טען אותה ויכול להיות שמשתמש ב־20 פונקציות אחרות.
אני יכול להגיד לך בפירוש מדוע FPC עושה לינק סטטי כברירת מחדש:
בהחלט, רק שהוא לא מייצר קוד של C, C++, Pascal, Fortran, Java, ADA, Objective C, Modula, PL/1, D עבור ARM, PPC, Alpha, SPARC, MIPS, Mototola, VAX, z390 , IA64, x86, x86_64 על Linux, Windows, DOS, OSX וכמעט כל UNIXים ידועים ובנוסף יודע לבצע קרוס קומפילציה בין כל הפלטפרומות האלה ועוד כל מיני דברים ששכחתי לציין. כך שההשוואה לא בדיוק הוגנת מול קומפיילר שמייצר קוד אופטימלי רק למעבדי אינטל.
אני לא אומר ש־Gcc זה מהדר מושלם, אבל בהתחשב בעובדה איפה ואיך הוא אמור לרוץ אני לא חושב שאפשר ליצור משהו הרבה יותר טוב.
שורה תחתונה
יש גישות שונות, ופסקל זאת אחלה של שפה ובמקרה שלה הגישה שהצעת עדיפה -- כי באמת אם יש לך 2 וחצי תכנות מבוססות על פסקל מותקנות אין סיבה להתעסק עם ספריות דינמיות. לכן, במקרה של פסקל הפתרון כנראה נוח ונכון.
זה לא נכון עבור עולם C/C++ בו רוב הספריות כבר נמצאות ואין לך שום סיבה להשתמש בטריקים שהצעת.
הוסף תגובה:
חובה לאפשר JavaScript כדי להגיב.