כולם יודעים ש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]
אגב, זו תוצאה מדהימה שכדאי שכל מפתח ג'אווה יכיר.
מי רוצה לשפר את התוצאות עוד?