C יותר מהירה מJava

כולם יודעים שC יותר מהירה מג'אווה, נכון?
פרוייקטים רציניים של גריסת מספרים (Number crunching) כמו עיבוד תמונה בזמן אמת, זיהוי קול, רינדור, דחיסה, קידוד ווידאו וכו בדרך כלל נכתבים בC (או C++).
בהינתן שתי פיסות קוד שעושות בדיוק את אותו דבר, מעניין לראות את במה מתבטא היתרון של C על ג'אווה.
למה אפשר לצפות ליתרון?
כי ג'אווה רצה מעל JVM, והJVM מוסיף תקורה, ברור שC תרוץ יותר מהר כי היא רצה ישר על הCPU ולא דרך הJVM.

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

הנה הקוד:
תוכנית C:
[code lang="c"]
#include
#include

int main(int argc, char **argv)
{
int i,j,k;
int N = 2500;
printf("N = %d\n", N);
double *A = malloc(N*N*sizeof(double));
double *B = malloc(N*N*sizeof(double));
double *C = malloc(N*N*sizeof(double));
double *bj = malloc(N*sizeof(double));
for (i = 0; i < N; i++) for (j = 0; j < N; j++) { int n = i*N+j; A[n] = i * j; } for (i = 0; i < N; i++) for (j = 0; j < N; j++) B[i*N+j] = i * j * j; // order 7: jik optimized ala JAMA for (j = 0; j < N; j++) { for (k = 0; k < N; k++) bj[k] = B[k*N+j]; for (i = 0; i < N; i++) { double s = 0; for (k = 0; k < N; k++) { s += A[i*N+k] * bj[k]; } C[i*N+j] = s; } } printf("done\n"); return 0; } [/code] תוכנית ג'אווה: [code lang="java"] public class Matrix { public static void main(String[] args) { int i,j,k; int N = 2500; System.err.println("N = " + N); double A[] = new double[N*N]; double B[] = new double[N*N]; double C[] = new double[N*N]; double bj[] = new double[N]; for (i = 0; i < N; i++) for (j = 0; j < N; j++) { int n = i*N+j; A[n] = i * j; } for (i = 0; i < N; i++) for (j = 0; j < N; j++) B[i*N+j] = i * j * j; // order 7: jik optimized ala JAMA for (j = 0; j < N; j++) { for (k = 0; k < N; k++) bj[k] = B[k*N+j]; for (i = 0; i < N; i++) { double s = 0; for (k = 0; k < N; k++) { s += A[i*N+k] * bj[k]; } C[i*N+j] = s; } } System.err.println("done"); } } [/code] מי לוקח התערבות של בכמה C עוקפת את ג'אווה בזמן הריצה של זה? נקמפל ונבדוק: [code] $javac Matrix.java $gcc Matrix.c -o matrix $ date;java Matrix;date;./matrix;date Thu Jun 26 08:42:10 IDT 2008 N = 2500 done Thu Jun 26 08:42:54 IDT 2008 N = 2500 done Thu Jun 26 08:44:31 IDT 2008 [/code] לתוכנית בג'אווה לקח לקח 44 שניות ולתוכנית בC לקח 107 שניות. מש"ל. אה, רגע. רצינו להראות שC יותר מהירה! טוב, מסתבר שלא כדאי לקחת דברים כמובנים מאליהם, גם אם כולם יודעים שהם נכונים. אם אתם חושבים שרימיתי, תריצו בעצמכם. בדקתי על שני מחשבים, אחד עם שתי ליבות של 3GHZ, ואחד עם ארבע ליבות של 2.4GHZ (כמובן שהראשון הוביל בכמה אחוזים טובים, אבל היחס נשמר). השתמשתי בJava 1.6.06. לדעתי התופעה הזו נובעת מההתקדמות המדהימה של סביבת הריצה של ג'אווה בתחום הHotspot. Hotspot היא טכנולוגיה שמקמפלת חלקים "חמים" בתוכנית בזמן, אבל בזמן ריצה. מכיוון שזמינות לHotspot סטטיסטיקות בזמן הריצה הממשי של התוכנית היא יכולה לשנות את הקוד ככה שירוץ בצורה אופטימלית לאור התנהגות של התוכנית ולא כנסיון מלומד לנחש מה יהיה יותר מהר מהתבוננות ושינוי הקוד, מה שעושה קומפיילר סטאטי. עדכון: קימפלתי את התוכנית C עם אופטימיזציה מקסימלית והתוצאה שלה השתפרה פלאים: [code] gcc -O3 Matrix.c $ date;./matrix;date Thu Jun 26 11:26:00 IDT 2008 N = 2500 done Thu Jun 26 11:26:29 IDT 2008 [/code] הפעם התוצאה של C היא 29 שניות. טוב משמעותית מקודם, וגם יותר מהיר בכ30% מג'אווה. עדכון 2: שמתי לב שקוד שקומפל עם javac איטי מקוד שקומפל בeclipse. נחשתי שeclipse מקמפל עם jikes (אני לא בטוח בזה). ניסיתי עם jikes והתוצאה השתוותה, תיקו 29 שניות. [code] $ javac Matrix.java ; time java Matrix N = 2500 done real 0m42.854s user 0m55.335s sys 0m26.214s $ jikes --bootclasspath /usr/lib/jvm/java-6-sun-1.6.0.06/jre/lib/rt.jar Matrix.java ; time java Matrix N = 2500 done real 0m29.463s user 0m29.366s sys 0m0.108s [/code] אגב, זו תוצאה מדהימה שכדאי שכל מפתח ג'אווה יכיר. מי רוצה לשפר את התוצאות עוד?

Facebook Comments

14 תגובות בנושא “C יותר מהירה מJava”

  1. לא ממש הסתכלי בעמוק בקוד, אז אני לא יודע עד כמה זה אמור לשנות:
    double *bj = malloc(N*sizeof(double));

    double bj[] = new double[N*N];

    המשתנה bj בC הוא יותר קטן פי N מהמשתנים A-C
    והמשתנה bj בג'אווה הוא אותו גודל כמו המשתנים A-C

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

  3. אה, אולי יש מקרים שזה נכון שjava יותר מהירה, אבל זה לא המקרה.

    ראשית כתבת קוד C כאילו שזה JAVA, שזו כמובן הדרך הלא נכונה…
    1. אין שום סיבה לעשות אלוקציה מהזיכרון בשביל מערך מקומי בפונקציה, אלוקציה כזו עושים על הסטאק.
    2. משתני INT שנעשה בהם שימוש כלכך מסיבי צריך לבקש שישבו ברגיסטר
    3. לא קימפלת עם אופטימיזציה. אני לא זוכר אם האופציות הבסיסיות של GCC מקמפלות עם אינפורמצית דיבוג או לא, אבל אחרי שנגמר שלב הדיבוג, תוכנות C מקמפלים עם אופטימיצזיה.

    והכי חשוב…. אני מניח שרוב הזמן מבוזבז על כפל של פלוטים, ולכן המסקנה ההגיונית היא שספרית הפלוט של java יותר יעילה מזו של GCC. אם תשתמש בספרית פלוט יותר טובה סביר להניח שתקבל תוצאות שונות לגמרי. אם מענין אותך לנסות, תחליף את הכפל לכפל של INTS ותחזור על הניסוי.

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

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

  5. 1. לכל כלל יש יוצא מהכלל, אבל נדמה לי שברוב הארכיטקטורות ניתן להגדיל את הסטאק. במקרה הזה האלוקציה הגדולה סביר להניח שמבטל את היתרון של C בכך שהיא משווה את האלוקציה ההתחלית הגדולה של JAVA שגורמת לכך שnew בJAVA או פעולה הרבההההה יותר מהירה מבC.
    2. זו אופטימיזציה שסביר להניח שלא תקבל אם לא תקמפל במפורש עבור אופטימיזציה. תזכור שבגלל שאין מערכת הפעלה וירטואלית בC, אופטימיזציה של קוד, גם מהסוג שמכניס משתנים לרגיסטרים הופכת את הדיבוג למאוד קשה.
    4. אני מצפה מתוכנית כזו שהתוצאות בC ובJAVA יהיו דומות עם הפרש קטן לטובת C.

  6. אני שמח לראות שהאינטואיציה שלי מוצדקת ומאוד מקווה שלא תביא עוד נתונים שיפריכו אותה 🙂
    בכל מקרה יש לי ניטפוק קטן: שיטת המדידה האחרונה שונה מהשיטה בה השתמשת כשמדדת את הC

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

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

  8. שתי ליבות, ארבע ליבות, שבע עשרה ליבות – הקוד שלך רץ עם פתיל אחד!

    מעניין לנסות על JVM-ים אחרים, שלחלקם מוניטין יותר טובים משל Hotspot.

  9. גרשון, נכון. ולכן המכונה עם שתי הליבות המהירות נתנה ביצועים יותר טובים מהמכונה עם ארבע הליבות האיטיות.
    מה אתה רוצה להגיד?

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

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

סגור לתגובות.