מקרה לשמירה על פרימיטיבים בג'אווה

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

שאלה : מהם שלושת הגורמים החשובים ביותר ברכישת נדל"ן?

תשובה : מיקום, מיקום, מיקום.

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

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

הכללתם של פרימיטיביים בג'אווה הייתה אחת מההחלטות היותר שנויות במחלוקת על עיצוב השפה, כפי שמעידים מספר המאמרים והפוסטים בפורום הקשורים להחלטה זו. סיימון ריטר ציין בנאום המרכזי שלו ב- JAX בלונדון בנובמבר 2011 כי נבחנת רצינית להסרת פרימיטיבים בגרסה עתידית של Java (ראה שקופית 41). במאמר זה אציג בקצרה פרימיטיבים ומערכת הכפול מסוג Java. באמצעות דוגמאות קוד ומידונים פשוטים, אביא את דעתי מדוע יש צורך בפרימיטיבי Java עבור סוגים מסוימים של יישומים. אני גם אשווה את ביצועי Java לזו של Scala, C ++ ו- JavaScript.

מדידת ביצועי תוכנה

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

פרימיטיבים לעומת אובייקטים

כפי שאתה בוודאי כבר יודע אם אתה קורא מאמר זה, ל- Java יש מערכת מסוג כפול, המכונה בדרך כלל סוגים פרימיטיביים וסוגי עצמים, לרוב בקיצור פרימיטיביים ואובייקטים. ישנם שמונה סוגים פרימיטיביים המוגדרים מראש ב- Java, ושמותיהם הם מילות מפתח שמורות. בדרך כלל דוגמה בשימוש כלל int, double, ו boolean. למעשה כל הסוגים האחרים ב- Java, כולל כל הסוגים המוגדרים על ידי המשתמש, הם סוגי אובייקטים. (אני אומר "בעצם" מכיוון שסוגי מערכים הם מעט היברידיים, אך הם דומים הרבה יותר לאובייקטים מאשר לסוגים פרימיטיביים.) לכל סוג פרימיטיבי יש מחלקת עטיפה תואמת שהיא סוג אובייקט; דוגמאות כוללות Integerעבור int, Doubleעבור double, ו Booleanעבור boolean.

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

 int n1 = 100; Integer n2 = new Integer(100); 

באמצעות רישום אוטומטי, תכונה שנוספה ל- JDK 5, יכולתי לקצר את ההצהרה השנייה לפשטות

 Integer n2 = 100; 

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

ההבדל בין פרימיטיבי n1לאובייקט העטיפה n2מודגם על ידי התרשים באיור 1.

ג'ון הראשון מור ג'וניור

המשתנה n1מחזיק בערך מספר שלם, אך המשתנה n2מכיל הפניה לאובייקט והוא האובייקט שמחזיק בערך המספר השלם. בנוסף, האובייקט אליו מפנים n2מכיל גם הפניה לאובייקט המחלקה Double.

הבעיה עם פרימיטיבים

לפני שאנסה לשכנע אותך בצורך בסוגים פרימיטיביים, עלי להכיר בכך שאנשים רבים לא יסכימו איתי. שרמן אלפרט ב"סוגים פרימיטיביים הנחשבים למזיקים "טוען שפרימיטיבים הם מזיקים מכיוון שהם מערבבים" סמנטיקה פרוצדוראלית למודל מונחה עצמים אחיד אחרת. פרימיטיביים אינם אובייקטים מהשורה הראשונה, אולם הם קיימים בשפה הכוללת, בעיקר, ראשונה- אובייקטים בכיתה. " פרימיטיבים וחפצים (בצורה של שיעורי עטיפה) מספקים שתי דרכים לטיפול בסוגים דומים מבחינה לוגית, אך יש להם סמנטיקה בסיסית שונה מאוד. לדוגמא, כיצד יש להשוות בין שני מקרים לשוויון? עבור סוגים פרימיטיביים משתמשים ==באופרטור, אך עבור אובייקטים הבחירה המועדפת היא לקרואequals()שיטה, שאינה אפשרות לפרימיטיבים. באופן דומה, סמנטיקה שונה קיימת בעת הקצאת ערכים או העברת פרמטרים. אפילו ערכי ברירת המחדל שונים; למשל, 0עבור intלעומת nullעבור Integer.

לקבלת רקע נוסף בנושא זה, ראה פוסט בבלוג של אריק ברונו, "דיון פרימיטיבי מודרני", המסכם כמה מהיתרונות והחסרונות של פרימיטיבים. מספר דיונים בנושא Stack Overflow מתמקדים גם בפרימיטיבים, כולל "מדוע אנשים עדיין משתמשים בסוגים פרימיטיביים בג'אווה?" ו"האם יש סיבה להשתמש תמיד בחפצים במקום בפרימיטיבים? " מתכנתים Stack Exchange מארחים דיון דומה שכותרתו "מתי להשתמש ב- Primitive לעומת Class ב- Java?".

ניצול זיכרון

A doubleב- Java תופס תמיד 64 ביט בזיכרון, אך גודל ההפניה תלוי במכונה הווירטואלית של Java (JVM). המחשב שלי מריץ את גרסת 64 סיביות של Windows 7 ו- JVM של 64 סיביות, ולכן הפניה במחשב שלי תופסת 64 סיביות. בהתבסס על הדיאגרמה באיור 1 הייתי מצפה לסינגל doubleכזה n1שיעסיק 8 בתים (64 סיביות), והייתי מצפה מסינגל Doubleכזה n2שיכבוש 24 בתים - 8 להתייחסות לאובייקט, 8 doubleלערך המאוחסן ב האובייקט, ו- 8 להתייחסות לאובייקט המחלקה עבור Double. בנוסף, ג'אווה משתמשת בזיכרון נוסף כדי לתמוך באיסוף אשפה לסוגי אובייקטים אך לא לסוגים פרימיטיביים. בוא נבדוק את זה.

באמצעות גישה דומה לזו של גלן מק'קלוסי ב"סוגים פרימיטיביים של ג'אווה לעומת עטיפות ", השיטה המוצגת ברשימה 1 מודדת את מספר הבתים שנכבשים על ידי מטריצה ​​n-by-n (מערך דו-ממדי) של double.

רישום 1. חישוב ניצול הזיכרון מסוג כפול

 public static long getBytesUsingPrimitives(int n) { System.gc(); // force garbage collection long memStart = Runtime.getRuntime().freeMemory(); double[][] a = new double[n][n]; // put some random values in the matrix for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) a[i][j] = Math.random(); } long memEnd = Runtime.getRuntime().freeMemory(); return memStart - memEnd; } 

שינוי הקוד ברשימה 1 עם שינויי הסוג הברורים (לא מוצג), נוכל גם למדוד את מספר הבתים שנכבשים על ידי מטריצה ​​n-by-n Double. כשאני בודק את שתי השיטות הללו במחשב שלי באמצעות מטריצות של 1000 על 1000, אני מקבל את התוצאות המוצגות בטבלה 1 להלן. כפי שמודגם, הגרסה לסוג פרימיטיבי doubleשווה לקצת יותר מ -8 בתים לערך במטריצה, בערך מה שציפיתי. עם זאת, הגרסה לסוג האובייקט Doubleדרשה קצת יותר מ -28 בתים לכל ערך במטריקס. לפיכך, במקרה זה, ניצול הזיכרון של Doubleהוא יותר מפי שלושה מניצול הזיכרון של double, מה שלא אמור להפתיע את מי שמבין את פריסת הזיכרון המוצגת באיור 1 לעיל.

טבלה 1. ניצול זיכרון כפול לעומת כפול

גִרְסָה סה"כ בתים בתים לכל ערך
באמצעות double 8,380,768 8.381
באמצעות Double 28,166,072 28.166

ביצועי זמן ריצה

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

Listing 2. Multiplying two matrices of type double

 public static double[][] multiply(double[][] a, double[][] b) { if (!checkArgs(a, b)) throw new IllegalArgumentException("Matrices not compatible for multiplication"); int nRows = a.length; int nCols = b[0].length; double[][] result = new double[nRows][nCols]; for (int rowNum = 0; rowNum < nRows; ++rowNum) { for (int colNum = 0; colNum < nCols; ++colNum) { double sum = 0.0; for (int i = 0; i < a[0].length; ++i) sum += a[rowNum][i]*b[i][colNum]; result[rowNum][colNum] = sum; } } return result; } 

I ran the two methods to multiply two 1000-by-1000 matrices on my computer several times and measured the results. The average times are shown in Table 2. Thus, in this case, the runtime performance of double is more than four times as fast as that of Double. That is simply too much of a difference to ignore.

Table 2. Runtime performance of double versus Double

Version Seconds
Using double 11.31
Using Double 48.48

The SciMark 2.0 benchmark

עד כה השתמשתי במדד היחיד והפשוט של כפל מטריקס כדי להוכיח שפרימיטיבים יכולים להניב ביצועי מחשוב גדולים משמעותית מאשר אובייקטים. כדי לחזק את טענותיי אשתמש במדד מדעי יותר. SciMark 2.0 הוא מדד ג'אווה למחשוב מדעי ומספרי הזמין מהמכון הלאומי לתקנים וטכנולוגיה (NIST). הורדתי את קוד המקור עבור אמת מידה זו ויצרתי שתי גרסאות, הגרסה המקורית באמצעות פרימיטיבים וגרסה שנייה באמצעות שיעורי עטיפה. עבור הגרסה השנייה החלפתי intעם Integerו doubleעם Doubleכדי לקבל את האפקט המלא של משתמש כיתות מעטפת. שתי הגרסאות זמינות בקוד המקור של מאמר זה.

הורד את Benchmarking Java: הורד את קוד המקור John I. Moore, Jr.

The SciMark benchmark measures performance of several computational routines and reports a composite score in approximate Mflops (millions of floating point operations per second). Thus, larger numbers are better for this benchmark. Table 3 gives the average composite scores from several runs of each version of this benchmark on my computer. As shown, the runtime performances of the two versions of the SciMark 2.0 benchmark were consistent with the matrix multiplication results above in that the version with primitives was almost five times faster than the version using wrapper classes.

Table 3. Runtime performance of the SciMark benchmark

SciMark version Performance (Mflops)
Using primitives 710.80
Using wrapper classes 143.73

You've seen a few variations of Java programs doing numerical calculations, using both a homegrown benchmark and a more scientific one. But how does Java compare to other languages? I'll conclude with a quick look at how Java's performance compares to that of three other programming languages: Scala, C++, and JavaScript.

Benchmarking Scala

Scala היא שפת תכנות הפועלת ב- JVM ונראה שהיא צוברת פופולריות. לסקאלה מערכת סוגים מאוחדת, כלומר אינה מבחינה בין פרימיטיביים לאובייקטים. על פי אריק אוסיים במחלקת הסוגים הנומריים של סקאלה (נק '1), סקאלה משתמש בסוגים פרימיטיביים במידת האפשר, אך ישתמש באובייקטים במידת הצורך. באופן דומה, תיאורו של מרטין אודרסקי על מערכי Scala אומר כי "... מערך Scala Array[Int]מיוצג כ- Java int[], ו- Array[Double]מיוצג כ- Java double[]..."

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