Kasus untuk memelihara primitif di Jawa

Primitif telah menjadi bagian dari bahasa pemrograman Java sejak rilis awal pada tahun 1996, namun tetap menjadi salah satu fitur bahasa yang lebih kontroversial. John Moore membuat kasus yang kuat untuk mempertahankan primitif dalam bahasa Java dengan membandingkan tolok ukur Java sederhana, baik dengan dan tanpa primitif. Dia kemudian membandingkan kinerja Java dengan Scala, C ++, dan JavaScript dalam jenis aplikasi tertentu, di mana primitif membuat perbedaan yang mencolok.

Pertanyaan : Apa tiga faktor terpenting dalam membeli real estat?

Jawaban : Lokasi, lokasi, lokasi.

Pepatah lama dan sering digunakan ini dimaksudkan untuk menyiratkan bahwa lokasi sepenuhnya mendominasi semua faktor lain dalam hal real estat. Dalam argumen serupa, tiga faktor terpenting yang perlu dipertimbangkan untuk menggunakan tipe primitif di Java adalah kinerja, kinerja, kinerja. Ada dua perbedaan antara argumen real estat dan argumen primitif. Pertama, dengan real estat, lokasi mendominasi hampir di semua situasi, tetapi kinerja yang diperoleh dari penggunaan jenis primitif dapat sangat bervariasi dari satu jenis aplikasi ke aplikasi lainnya. Kedua, dengan real estat, ada faktor lain yang perlu dipertimbangkan meskipun biasanya kecil dibandingkan dengan lokasi. Dengan tipe primitif, hanya ada satu alasan untuk menggunakannya - kinerja; dan hanya jika aplikasinya adalah jenis yang bisa mendapatkan keuntungan dari penggunaannya.

Primitif menawarkan sedikit nilai untuk sebagian besar yang terkait dengan bisnis dan aplikasi Internet yang menggunakan model pemrograman klien-server dengan database di backend. Tetapi kinerja aplikasi yang didominasi oleh perhitungan numerik bisa mendapatkan keuntungan besar dari penggunaan primitif.

Dimasukkannya bahasa primitif di Jawa telah menjadi salah satu keputusan desain bahasa yang lebih kontroversial, terbukti dari jumlah artikel dan postingan forum terkait keputusan ini. Simon Ritter mencatat dalam keynote address JAX London pada November 2011 bahwa pertimbangan serius sedang diberikan untuk penghapusan primitif di versi Java yang akan datang (lihat slide 41). Pada artikel ini saya akan secara singkat memperkenalkan primitif dan sistem tipe ganda Java. Dengan menggunakan contoh kode dan tolok ukur sederhana, saya akan menjelaskan mengapa primitif Java diperlukan untuk jenis aplikasi tertentu. Saya juga akan membandingkan kinerja Java dengan Scala, C ++, dan JavaScript.

Mengukur kinerja perangkat lunak

Kinerja perangkat lunak biasanya diukur dari segi waktu dan ruang. Waktu dapat berupa waktu berjalan aktual, seperti 3,7 menit, atau urutan pertumbuhan berdasarkan ukuran input, seperti O ( n 2). Ukuran serupa ada untuk kinerja ruang, yang sering dinyatakan dalam penggunaan memori utama tetapi juga dapat meluas ke penggunaan disk. Meningkatkan kinerja biasanya melibatkan pertukaran waktu-ruang di mana perubahan untuk meningkatkan waktu sering kali memiliki efek merugikan pada ruang, dan sebaliknya. Pengukuran urutan pertumbuhan bergantung pada algoritme, dan beralih dari kelas pembungkus ke primitif tidak akan mengubah hasilnya. Tetapi ketika datang ke kinerja ruang dan waktu aktual, penggunaan kelas primitif dan bukan pembungkus menawarkan peningkatan baik dalam waktu dan ruang secara bersamaan.

Primitif versus objek

Seperti yang mungkin sudah Anda ketahui saat membaca artikel ini, Java memiliki sistem tipe ganda, biasanya disebut tipe primitif dan tipe objek, sering disingkat hanya sebagai primitif dan objek. Ada delapan tipe primitif yang telah ditentukan sebelumnya di Java, dan namanya adalah kata kunci yang dipesan. Umumnya contoh digunakan meliputi int, double, dan boolean. Pada dasarnya semua tipe lain di Java, termasuk semua tipe yang ditentukan pengguna, adalah tipe objek. (Saya katakan "pada dasarnya" karena tipe array adalah sedikit hibrid, tetapi mereka lebih seperti tipe objek daripada tipe primitif.) Untuk setiap tipe primitif ada kelas pembungkus yang sesuai yang merupakan tipe objek; contoh mencakup Integeruntuk int, Doubleuntuk double, dan Booleanuntuk boolean.

Tipe primitif berbasis nilai, tetapi tipe objek berbasis referensi, dan di situlah letak kekuatan dan sumber kontroversi tipe primitif. Untuk mengilustrasikan perbedaannya, perhatikan dua pernyataan di bawah ini. Deklarasi pertama menggunakan tipe primitif dan yang kedua menggunakan kelas pembungkus.

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

Menggunakan autoboxing, fitur yang ditambahkan ke JDK 5, saya dapat mempersingkat deklarasi kedua menjadi sederhana

 Integer n2 = 100; 

tetapi semantik yang mendasarinya tidak berubah. Autoboxing menyederhanakan penggunaan kelas pembungkus dan mengurangi jumlah kode yang harus ditulis oleh pemrogram, tetapi tidak mengubah apa pun saat runtime.

Perbedaan antara objek primitif n1dan wrapper n2diilustrasikan oleh diagram pada Gambar 1.

John I. Moore, Jr.

Variabel n1memegang nilai integer, tetapi variabel n2berisi referensi ke suatu objek, dan itu adalah objek yang menyimpan nilai integer. Selain itu, objek yang dirujuk n2juga berisi referensi ke objek kelas Double.

Masalah dengan primitif

Sebelum saya mencoba meyakinkan Anda tentang perlunya tipe primitif, saya harus mengakui bahwa banyak orang tidak akan setuju dengan saya. Sherman Alpert dalam "Tipe primitif dianggap berbahaya" berpendapat bahwa primitif berbahaya karena mereka mencampur "semantik prosedural ke dalam model berorientasi objek yang seragam. Primitif bukanlah objek kelas satu, namun mereka ada dalam bahasa yang melibatkan, terutama, pertama objek kelas. " Primitif dan objek (dalam bentuk kelas pembungkus) menyediakan dua cara menangani tipe yang mirip secara logis, tetapi keduanya memiliki semantik dasar yang sangat berbeda. Misalnya, bagaimana seharusnya dua contoh dibandingkan untuk persamaan? Untuk tipe primitif, seseorang menggunakan ==operator, tetapi untuk objek pilihan yang lebih disukai adalah memanggilequals()metode, yang bukan merupakan pilihan untuk primitif. Demikian pula, semantik berbeda ada saat menetapkan nilai atau parameter lewat. Bahkan nilai defaultnya berbeda; misalnya, 0untuk intversus nulluntuk Integer.

Untuk latar belakang lebih lanjut tentang masalah ini, lihat entri blog Eric Bruno, "Sebuah diskusi primitif modern," yang merangkum beberapa pro dan kontra dari kaum primitif. Sejumlah diskusi tentang Stack Overflow juga berfokus pada primitif, termasuk "Mengapa orang masih menggunakan tipe primitif di Java?" dan "Apakah ada alasan untuk selalu menggunakan Object daripada primitif ?." Pemrogram Stack Exchange mengadakan diskusi serupa yang berjudul "Kapan menggunakan primitif vs kelas di Java?".

Pemanfaatan memori

A doubledi Java selalu menempati 64 bit di memori, tetapi ukuran referensi bergantung pada mesin virtual Java (JVM). Komputer saya menjalankan Windows 7 versi 64-bit dan JVM 64-bit, dan oleh karena itu referensi di komputer saya menggunakan 64 bit. Berdasarkan diagram pada Gambar 1 saya mengharapkan satu doubleseperti n1untuk menempati 8 byte (64 bit), dan saya mengharapkan satu Doubleseperti n2untuk menempati 24 byte - 8 untuk referensi ke objek, 8 untuk doublenilai yang disimpan di objek, dan 8 untuk referensi ke objek kelas untuk Double. Selain itu, Java menggunakan memori ekstra untuk mendukung pengumpulan sampah untuk tipe objek tetapi tidak untuk tipe primitif. Mari kita lihat.

Menggunakan pendekatan yang mirip dengan Glen McCluskey dalam "Java primitive types vs. wrappers," metode yang ditunjukkan pada Listing 1 mengukur jumlah byte yang ditempati oleh matriks n-by-n (array dua dimensi) double.

Kode 1. Menghitung pemanfaatan memori tipe ganda

 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; } 

Memodifikasi kode dalam Daftar 1 dengan perubahan tipe yang jelas (tidak ditampilkan), kita juga dapat mengukur jumlah byte yang ditempati oleh matriks n-oleh-n Double. Ketika saya menguji kedua metode ini di komputer saya menggunakan matriks 1000-by-1000, saya mendapatkan hasil yang ditunjukkan pada Tabel 1 di bawah ini. Seperti yang diilustrasikan, versi untuk tipe primitif doublesetara dengan sedikit lebih dari 8 byte per entri dalam matriks, kira-kira seperti yang saya harapkan. Namun, versi untuk tipe objek Doublemembutuhkan lebih dari 28 byte per entri dalam matriks. Jadi, dalam hal ini, penggunaan memori Doublelebih dari tiga kali lipat penggunaan memori double, yang seharusnya tidak mengejutkan siapa pun yang memahami tata letak memori yang diilustrasikan pada Gambar 1 di atas.

Tabel 1. Pemanfaatan memori ganda versus Ganda

Versi: kapan Total byte Byte per entri
Using double 8,380,768 8.381
Using Double 28,166,072 28.166

Runtime performance

To compare the runtime performances for primitives and objects, we need an algorithm dominated by numerical calculations. For this article I have chosen matrix multiplication, and I compute the time required to multiply two 1000-by-1000 matrices. I coded matrix multiplication for double in a straightforward manner as shown in Listing 2 below. While there may be faster ways to implement matrix multiplication (perhaps using concurrency), that point is not really relevant to this article. All I need is common code in two similar methods, one using the primitive double and one using the wrapper class Double. The code for multiplying two matrices of type Double is exactly like that in Listing 2 with the obvious type changes.

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

Sejauh ini saya telah menggunakan tolok ukur tunggal perkalian matriks untuk menunjukkan bahwa primitif dapat menghasilkan kinerja komputasi yang jauh lebih besar daripada objek. Untuk memperkuat klaim saya, saya akan menggunakan patokan yang lebih ilmiah. SciMark 2.0 adalah tolok ukur Java untuk komputasi ilmiah dan numerik yang tersedia dari National Institute of Standards and Technology (NIST). Saya mengunduh kode sumber untuk benchmark ini dan membuat dua versi, versi asli menggunakan primitif dan versi kedua menggunakan kelas pembungkus. Untuk versi kedua saya mengganti intdengan Integerdan doubledengan Doubleuntuk mendapatkan efek penuh menggunakan kelas pembungkus. Kedua versi tersedia dalam kode sumber untuk artikel ini.

unduh Benchmarking Java: Unduh kode sumber 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 adalah bahasa pemrograman yang berjalan di JVM dan tampaknya mulai populer. Scala memiliki sistem tipe terpadu, artinya tidak membedakan antara primitif dan objek. Menurut Erik Osheim dalam kelas tipe Numerik Scala (Pt. 1), Scala menggunakan tipe primitif bila memungkinkan tetapi akan menggunakan objek jika perlu. Demikian pula, deskripsi Martin Odersky tentang Scala's Arrays mengatakan bahwa "... array Scala Array[Int]direpresentasikan sebagai Java int[], an Array[Double]direpresentasikan sebagai Java double[]..."

Jadi, apakah ini berarti bahwa sistem tipe terpadu Scala akan memiliki kinerja runtime yang sebanding dengan tipe primitif Java? Ayo lihat.