הגודל (של קבצי ריצה) לא קובע

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

אם ניקח את הדוגמאות הבאות:

program test;
var p : pointer;
begin
p := GetMem(1024);
writeln('We have allocated the memory');
freemem(p);
writeln('We have freed the memory');
end.

נהדר את התוכנית, ונבצע עליה strip בשביל להוריד את כל הסמלים הלא נחוצים, התוכנית אצלי על המחשב שוקלת 147K.

אם ניקח תוכנית זהה ב C:

#include <stdio.h>
#include <stdlib.h>
int main()
{
void * p = malloc(1024);
printf("We have allocated the memory\n");
free(p);
printf("We have freed the memory\n");
return 1;
}

נהדר אותה ולא נשתמש ב strip, אצלי התוכנית תשקול 8.7K.

על פניו משהו לא בסדר עם מהדרי פסקל הם יוצרים קבצי ריצה גדולים יותר מאשר קבצי הריצה של C (כאשר לא משתמשים בשום אופטימיזציה כלשהי או דרך להקטין את הקבצים של C). אז מה העניין ?

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

ldd test
not a dynamic executable

או במילים אחרות אין שום ספרייה שמקושרת לקובץ הריצה של פסקל.

בואו נראה מה קורה עם קובץ ריצה של C:

ldd test2
linux-vdso.so.1 => (0x00007ffffd5fe000)
libc.so.6 => /lib64/libc.so.6 (0x00002ab0ad869000)
/lib64/ld-linux-x86-64.so.2 (0x00002ab0ad64e000)

אצלי על המחשב ישנם 3 ספריות דינמיות שמקושרת אל הקובץ ריצה. מעניין ! נראה ש C מאציל סמכויות, ולא באמת רוצה לדאוג לפעולת malloc, printf וכמובן free. נראה שהוא בסה"כ משתמש בקישור אל ספריות שעושות את זה עבורו.

כלומר פיזית יש פחות קוד בC שנכנס לקובץ הריצה. זה מסביר למה הקבצי ריצה קטנים יותר ב C.

אבל למה יותר קוד נכנס לקוד פסקל ? ובכן בפסקל יש יחידה בשם System אשר מכילה דברים מאוד בסיסיים (יחסית) והיא מקושרת לתוכנית שלנו בין אם נרצה או לא. זו הסיבה למשל שיש לנו את writeln, GetMem וכמובן את FreeMem בלי להכריז על שום דבר. הכלים האלו מפורשים כבר לשפת אסמבלר בצורה מאוד אופטימלית ותאורטית בלבד אינם תלויות במערכת שתספק לנו ספריות שנוכל לעשות את הדברים. כלומר הקובץ אסמבלר שנוצר לנו (במידה ונגיד ל FPC לשמור אותו) יכיל פיזית את הקוד שמאתחל זכרון, קוד שמדפיס תוכן על המסך וכמובן תוכן שמשחרר את הזכרון.

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

20 מחשבות על “הגודל (של קבצי ריצה) לא קובע

  1. אורי

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

    כשמשתמשים בספריה דינמית, היא נטענת לזכרון פעם אחת עבור כל ה-20 תוכנות שמשתמשות בה. כך יכול להווצר מצב שאם אתה מריץ 20 תוכנות ב-FPC, אתה תופס את אותו נפח זכרון כמו 2-3 תוכנות סי. כשאתה מסתכל על זה ברמה של מערכת הפעלה, אתה כבר מוכרח להשתמש במהדר שמייצר קוד כמה שיותר קטן.

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

  2. Shai

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

    הגודל לא קובע, אבל השכפול כן.

  3. צפריר כהן

    אם הקישור הוא סטאטי, מה לגבי ה־name service switch?

    מדובר על הפונקציות getpwnam, gethostbyname וכל שאר מה שהפקודה getent יכולה להחזיר.

    הקישור שלהם ל־libc הוא דינאמי לחלוטין: מדובר על shared objects שנטענים בזמן ריצה. זו תמיד מוזכרת כאחת הסיבות שבגללה לא מומלץ להשתמש בקישור דינאמי ל־libc.

    תבדוק, לדוגמה, למה rpm כבר לא מקושרת סטאטית ל־libc החל מרד־האט 9.

  4. ארתיום

    עידו? אפשר לגרום ל־FPC לייצר קובץ ריצה שטוען ספריות FPC באופן דינמי?

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

    מה שהצגת זה לא יתרון אלא חסרון. אני הייתי משיג את אותה הדבר אם הייתי מוסיף ‎-static לקימפול של gcc.

  5. elcuco

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

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

    בגלל הקישור הסטאטי אני רואה שתי בעיות:
    אי אפשר להריץ יותר משניים, שלושה יישומים בפסקל במחשב (אלא אם יש לך גאזיליון־ג'יגה זיכרון)
    זמן העלייה של היישום יהיה יותר גדול, כי היישום צריך לאתחול דברים שמאותחלים על ידי glibc (רשימות מקושרות עבור הזכרון של היישום? מה עוד יש?)

    ובלי קשר, כמה אנשים בדקו את הספרייה התקנית של פסקל שמקושרת סטאטית ליישום? ביחס לספרייה של c…? האם היא באמת טובה? (היא אפילו לא הגיעה לגרסה 1.0!!!).

    ואללה, עכשיו אני יותר שמח שאני מתחיל ללמוד D. תסתכל על זה:

    [elcuco@localhost d]$ ldd loopstest
    linux-gate.so.1 => (0xffffe000)
    libpthread.so.0 => /lib/i686/libpthread.so.0 (0xb7f39000)
    libm.so.6 => /lib/i686/libm.so.6 (0xb7f14000)
    libc.so.6 => /lib/i686/libc.so.6 (0xb7dd0000)
    /lib/ld-linux.so.2 (0xb7f6b000)
    [elcuco@localhost d]$ file loopstest.d
    loopstest.d: a /home/elcuco/src/d/dmd/bin/dmd script text executable
    [elcuco@localhost d]$ head -n 2 loopstest.d
    #! /home/elcuco/src/d/dmd/bin/dmd -run

    אני רואה את כל החסרונות שיש ב־pascal מתוקנים ב־D. בנוסף, כיוון שזאת שפה עילית יותר מ־c, אני בטוח שיש לה כמה מהיתרונות שיש לפסקל (דרך אגב, אני לא הבנתי איזה יתרון יש ל־pascal על c).

  6. ik_5 מאת

    אממ היות ואף אחד לא זוכר דברים שכתבתי בעבר נכניס את זה לכאן.

    ישנם כמה צורות עבודה בפסקל. בניגוד ל C בו יש קישור דינאמי לספריות, כאן כל עוד שניתן יש אפשרות לתת גמישות לאיך שדברים עובדים. למשל אם אני מכניס את היחידה cmem בתור היחידה הראשונה משמאל אז הוא יעבוד עם malloc של libc ווכ'. זה מתקבל ע"י MemoryManager.

    ברירת המחדל בפסקל היא ניהול זכרון עצמאי ולא ע"י פונקציות קיימות כדוגמת malloc. בלינוקס (ושאר מערכות posix), זה מתבצע ע"י שימוש ב mmap לקבל חלק זכרון, וכאשר הוא נגמר אז מבקשים להגדיר וכו'. ההקצאה הזו מגיעה בסופו של דבר למצביע שלנו.
    את הקריאות למערכת אנחנו מבצעים כאן בצורה שונה ממה שאתם רגילים, אבל זה עדיין לא עושה חיבור סטטי ואין שום ספרייה נוספת שצריך לייבא עם התוכנית שנוצרה. שיטה אחרת לקרוא לפונקציות של מערכת הפעלה (אשר טעונות בזכרון בכל מקרה !!!) היא ע"י תרגום שם הפונקציה למספר והעברת פרמטרים (שהראשון הוא מספר הפונקציה) עד 7 או 8 פרמטרים (אני לא זוכר בעל פה) עם הערכים שאנחנו רוצים.

    ככה זה נראה בפסקל (32 ביט):
    function FpSysCall(sysnr,param1,param2,param3,param4,param5,param6: TSysParam):TSysResult; assembler; register; [public,alias:'FPC_SYSCALL6'];
    { Var sysnr located in register eax
    Var param1 located in register edx
    Var param2 located in register ecx
    Var param3 located at ebp+20
    Var param4 located at ebp+16
    Var param5 located at ebp+12
    Var param6 located at ebp+8 }
    asm
    push %ebx
    push %esi
    push %edi
    push %ebp
    // movl sysnr,%eax
    movl %edx,%ebx
    // movl param2,%ecx
    movl param3,%edx
    movl param4,%esi
    movl param5,%edi
    movl param6,%ebp
    cmp $0, sysenter_supported
    jne .LSysEnter
    int $0x80
    jmp .LTail
    .LSysEnter:
    call psysinfo
    .LTail:
    pop %ebp
    pop %edi
    pop %esi
    pop %ebx
    cmpl $-4095,%eax
    jb .LSyscOK
    negl %eax
    call seterrno
    movl $-1,%eax
    .LSyscOK:
    end;

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

  7. ik_5 מאת

    בקשר לקישוריות ה"סטטית" לתוך הקוד. אם לוקחים את הנייר באקדמיה הטכנות כאן נכונות, אבל בפועל לינוקס למשל עובד בצורה הרבה יותר חכמה ולמעשה אם יש לי נגיד את writeln והוא שוקל 200k, אם אני אריץ 10 פרוססים, אני לא אקבל 200,000K אלא משהו בסגנון של 210k גג.
    בגדול מאוד בלי להיכנס לעובי הקורה יש לך בלינוקס משהו שנקרא memory map, שבמקום לטעון מליון פעם את אותן הפונקציות הוא יודע לטעון אותן רק פעם אחת.

    הנה הוכחה לטענה שלי

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

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

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

  8. ik_5 מאת

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

  9. צפריר כהן

    מה "ההוכחה" שלך בדיוק מוכיחה ואיך היא קשורה לקישור סטאטי או דינאמי?

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

  10. ik_5 מאת

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

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

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

  11. צפריר כהן

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

  12. Shlomil

    אוף טופיק:

    לדיאגו – D היא שפה מגניבה בטירוף.
    אבל יש חיסרון אחד עיקרי – dmd שמממש את השפה בצורה המושלמת ביותר, אינו פתוח לגמרי. אני באופן אישי מחכה לראות את המימוש של LLVMDC (וגם אז זה יהיה רק D v1.0 ולא D v2.0 וחבל שכך)

  13. פינגבק: חוסר פתיחות מחשבתית « לראות שונה

  14. ארתיום

    בקשר לקישוריות ה”סטטית” לתוך הקוד. אם לוקחים את הנייר באקדמיה הטכנות כאן נכונות, אבל בפועל לינוקס למשל עובד בצורה הרבה יותר חכמה ולמעשה אם יש לי נגיד את writeln והוא שוקל 200k, אם אני אריץ 10 פרוססים, אני לא אקבל 200,000K אלא משהו בסגנון של 210k גג.

    לא נכון: אם אתה מעלה 10 תוכנות **שונות** שעושות בסה"כ writeln אתה תקבל 2,000K ואני אסביר למה:

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

    לעומת זאת, אם אני מעלה 10 תכנות **שונות** שמתוכן 10K זה תוכנה ועוד 200K זה ספריה מקושרת דינמית אז אתה תצרוך בערת 200K+10*10K=300K. מדוע? כי הספריה הדינמית תיטען פעם אחת בלבד.

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

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

    ד"א טעינת libc בד"כ לא עולה כלום, כי היא בד"כ כבר טעונה בזכרון (או ליתר דיוק mmaped).

    אתה יודע עידו: תעשה תרגיל קטן: תריץ את שתי התכנות האלו עם strace ותראה איזה system calls כל אחת מבצעת. אני די בטוח שאתה תגלה ש־C קורא המון ל־mmap כדי לחבר ספריות דינמיות, תכנה Pascal בטח תקרא את זה פעמים ספורות.

  15. Shai

    עידו,

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

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

  16. ik_5 מאת

    ארתיום הלכתי על ההצעה שלך כתבתי תוכנית שמדפיסה Hello World על המסך ויצרתי strace של הביצוע ב C והביצוע בפסקל.
    אלו הם התוצאות (הספירה התבצעה ע"י פרל ד"א שקראה את תוכן של הקובץ strace וחיפשה mmap):

    פסקל: 21 קריאות ל mmap
    C : 8 קריאות ל mmap.

    אתה ד"א מוזמן לא להאמין לי ולנסות בעצמך:

    program test;
    begin
    writeln('Hello World');
    end.

    fpc -XX test.pp
    strace test

    וכמובן

    #include
    int main() {
    printf("Hello World\n");
    {

    gcc test2.c -o test2
    strace test2

    בהצלחה.

    ד"א ה -XX מדבר על smart link שזו הצורה שצריך להדר דברים (בד"כ ברירת מחדל ב FPC).

  17. פינגבק: הבלוג של ארתיום

כתיבת תגובה

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

הלוגו של WordPress.com

אתה מגיב באמצעות חשבון WordPress.com שלך. לצאת מהמערכת / לשנות )

תמונת Twitter

אתה מגיב באמצעות חשבון Twitter שלך. לצאת מהמערכת / לשנות )

תמונת Facebook

אתה מגיב באמצעות חשבון Facebook שלך. לצאת מהמערכת / לשנות )

תמונת גוגל פלוס

אתה מגיב באמצעות חשבון Google+ שלך. לצאת מהמערכת / לשנות )

מתחבר ל-%s