Kes untuk menjaga primitif di Jawa

Primitives telah menjadi bagian dari bahasa pengaturcaraan Java sejak pertama kali dilancarkan pada tahun 1996, namun mereka tetap menjadi salah satu ciri bahasa yang lebih kontroversial. John Moore membuat kesimpulan yang kuat untuk menjaga primitif dalam bahasa Java dengan membandingkan tanda aras 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 perbezaan penting.

Soalan : Apakah tiga faktor terpenting dalam membeli harta tanah?

Jawapan : Lokasi, lokasi, lokasi.

Pepatah lama dan sering digunakan ini bertujuan untuk menunjukkan bahawa lokasi benar-benar menguasai semua faktor lain ketika datang ke harta tanah. Dalam hujah yang serupa, tiga faktor terpenting yang perlu dipertimbangkan untuk menggunakan jenis primitif di Java adalah prestasi, prestasi, prestasi. Terdapat dua perbezaan antara hujah untuk harta tanah dan hujah untuk primitif. Pertama, dengan harta tanah, lokasi mendominasi dalam hampir semua keadaan, tetapi peningkatan prestasi menggunakan jenis primitif dapat sangat berbeza dari satu jenis aplikasi ke aplikasi yang lain. Kedua, dengan harta tanah, ada faktor lain yang perlu dipertimbangkan walaupun biasanya kecil berbanding lokasi. Dengan jenis primitif, hanya ada satu alasan untuk menggunakannya - prestasi; dan hanya jika aplikasi itu adalah jenis yang dapat memanfaatkan penggunaannya.

Primitives menawarkan sedikit nilai untuk kebanyakan aplikasi yang berkaitan dengan perniagaan dan Internet yang menggunakan model pengaturcaraan pelayan pelanggan dengan pangkalan data di backend. Tetapi prestasi aplikasi yang dikuasai oleh pengiraan berangka boleh mendapat keuntungan besar dari penggunaan primitif.

Kemasukan primitif di Jawa telah menjadi salah satu keputusan reka bentuk bahasa yang lebih kontroversial, seperti yang dibuktikan oleh jumlah artikel dan catatan forum yang berkaitan dengan keputusan ini. Simon Ritter menyatakan dalam ucaptama JAX London pada bulan November 2011 bahawa pertimbangan serius sedang diberikan kepada penghapusan primitif dalam versi Java yang akan datang (lihat slaid 41). Dalam artikel ini saya akan memperkenalkan secara ringkas primitif dan sistem dua-jenis Java. Dengan menggunakan contoh kod dan penanda aras yang mudah, saya akan menjelaskan mengapa primitif Java diperlukan untuk jenis aplikasi tertentu. Saya juga akan membandingkan prestasi Java dengan Scala, C ++, dan JavaScript.

Mengukur prestasi perisian

Prestasi perisian biasanya diukur dari segi masa dan ruang. Masa boleh menjadi waktu berjalan yang sebenarnya, seperti 3.7 minit, atau urutan pertumbuhan berdasarkan ukuran input, seperti O ( n 2). Ukuran serupa berlaku untuk prestasi ruang, yang sering dinyatakan dalam penggunaan memori utama tetapi juga dapat meluas ke penggunaan cakera. Meningkatkan prestasi biasanya melibatkan pertukaran ruang-waktu kerana perubahan untuk memperbaiki masa sering memberi kesan buruk pada ruang, dan sebaliknya. Pengukuran urutan pertumbuhan bergantung pada algoritma, dan beralih dari kelas pembungkus ke primitif tidak akan mengubah hasilnya. Tetapi ketika melihat prestasi waktu dan ruang yang sebenarnya, penggunaan primitif dan bukannya kelas pembungkus menawarkan peningkatan pada waktu dan ruang secara serentak.

Primitif berbanding objek

Seperti yang anda mungkin sudah tahu jika anda membaca artikel ini, Java mempunyai sistem jenis dua, biasanya disebut sebagai jenis primitif dan jenis objek, sering disingkat hanya sebagai primitif dan objek. Terdapat lapan jenis primitif yang telah ditentukan di Jawa, dan namanya adalah kata kunci terpelihara. Biasa contoh yang digunakan termasuk int, doubledan boolean. Pada dasarnya semua jenis lain di Java, termasuk semua jenis yang ditentukan pengguna, adalah jenis objek. (Saya katakan "pada dasarnya" kerana jenis larik adalah sedikit hibrid, tetapi ia lebih menyerupai jenis objek daripada jenis primitif.) Untuk setiap jenis primitif terdapat kelas pembungkus yang sesuai yang merupakan jenis objek; contoh merangkumi Integeruntuk int, Doubleuntuk double, dan Booleanuntuk boolean.

Jenis primitif berdasarkan nilai, tetapi jenis objek berdasarkan rujukan, dan di dalamnya terletak kekuatan dan sumber kontroversi jenis primitif. Untuk menggambarkan perbezaannya, pertimbangkan dua pernyataan di bawah. Deklarasi pertama menggunakan jenis primitif dan yang kedua menggunakan kelas pembungkus.

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

Dengan menggunakan autoboxing, satu ciri yang ditambahkan pada JDK 5, saya dapat memendekkan deklarasi kedua menjadi sederhana

 Integer n2 = 100; 

tetapi semantik yang mendasari tidak berubah. Autoboxing mempermudah penggunaan kelas wrapper dan mengurangkan jumlah kod yang harus ditulis oleh pengaturcara, tetapi tidak mengubah apa-apa pada waktu berjalan.

Perbezaan antara n1objek primitif dan pembungkus n2digambarkan oleh rajah dalam Rajah 1.

John I. Moore, Jr.

Pemboleh ubah tersebut n1memegang nilai integer, tetapi pemboleh ubah tersebut n2berisi rujukan ke suatu objek, dan objek tersebut memegang nilai integer. Selain itu, objek yang dirujuk n2juga mengandungi rujukan ke objek kelas Double.

Masalah dengan primitif

Sebelum saya meyakinkan anda tentang keperluan jenis primitif, saya harus mengakui bahawa banyak orang tidak akan bersetuju dengan saya. Sherman Alpert dalam "Jenis primitif yang dianggap berbahaya" berpendapat bahawa primitif berbahaya kerana mereka mencampuradukkan "semantik prosedural menjadi model berorientasikan objek yang tidak seragam. Primitif bukan objek kelas pertama, namun ia wujud dalam bahasa yang melibatkan, terutamanya, pertama - objek kelas. " Primitif dan objek (dalam bentuk kelas pembungkus) menyediakan dua cara menangani jenis yang serupa secara logik, tetapi mereka mempunyai semantik asas yang sangat berbeza. Sebagai contoh, bagaimana dua keadaan harus dibandingkan untuk kesamaan? Untuk jenis primitif, seseorang menggunakan ==operator, tetapi untuk objek pilihan yang lebih disukai adalah memanggilequals()kaedah, yang bukan pilihan untuk primitif. Begitu juga, semantik yang berbeza wujud ketika memberikan nilai atau melewati parameter. Bahkan nilai lalai berbeza; contohnya, 0untuk intlawan nulluntuk Integer.

Untuk lebih banyak latar belakang mengenai isu ini, lihat catatan blog Eric Bruno, "Perbincangan primitif moden," yang merangkum beberapa kebaikan dan keburukan primitif. Sejumlah perbincangan mengenai Stack Overflow juga memfokuskan pada primitif, termasuk "Mengapa orang masih menggunakan jenis primitif di Java?" dan "Adakah alasan untuk selalu menggunakan Objek dan bukannya primitif?" Programmers Stack Exchange mengadakan perbincangan serupa yang berjudul "Kapan menggunakan kelas vs primitif di Java?".

Penggunaan memori

A doubledi Java selalu menggunakan 64 bit dalam memori, tetapi ukuran rujukan bergantung pada mesin virtual Java (JVM). Komputer saya menjalankan Windows 7 versi 64-bit dan JVM 64-bit, dan oleh itu rujukan pada komputer saya mempunyai 64 bit. Berdasarkan rajah dalam Rajah 1, saya mengharapkan satu doubleseperti n1untuk menempati 8 bait (64 bit), dan saya mengharapkan satu Doubleseperti n2untuk menempati 24 bait - 8 untuk rujukan ke objek, 8 untuk doublenilai yang tersimpan di objek, dan 8 untuk rujukan ke objek kelas untuk Double. Plus, Java menggunakan memori tambahan untuk menyokong pengumpulan sampah untuk jenis objek tetapi tidak untuk jenis primitif. Mari kita periksa.

Menggunakan pendekatan yang serupa dengan Glen McCluskey dalam "Jenis primitif Java vs. pembungkus," kaedah yang ditunjukkan dalam Penyenaraian 1 mengukur jumlah bait yang ditempati oleh matriks n-by-n (array dua dimensi) dari double.

Penyenaraian 1. Mengira penggunaan memori jenis 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; } 

Modifying the code in Listing 1 with the obvious type changes (not shown), we can also measure the number of bytes occupied by an n-by-n matrix of Double. When I test these two methods on my computer using 1000-by-1000 matrices, I get the results shown in Table 1 below. As illustrated, the version for primitive type double equates to a little more than 8 bytes per entry in the matrix, roughly what I expected. However, the version for object type Double required a little more than 28 bytes per entry in the matrix. Thus, in this case, the memory utilization of Double is more than three times the memory utilization of double, which should not be a surprise to anyone who understands the memory layout illustrated in Figure 1 above.

Table 1. Memory utilization of double versus Double

Version Total bytes Bytes per entry
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 penanda aras tunggal pendaraban matriks sederhana untuk menunjukkan bahawa primitif dapat menghasilkan prestasi pengkomputeran yang jauh lebih besar daripada objek. Untuk mengukuhkan tuntutan saya, saya akan menggunakan tanda aras yang lebih saintifik. SciMark 2.0 adalah penanda aras Java untuk pengkomputeran saintifik dan numerik yang tersedia dari National Institute of Standards and Technology (NIST). Saya memuat turun kod sumber untuk penanda aras ini dan membuat dua versi, versi asal menggunakan primitif dan versi kedua menggunakan kelas pembungkus. Untuk versi kedua saya digantikan intdengan Integerdan doubledengan Doubleuntuk mendapatkan kesan yang penuh dengan menggunakan kelas pembungkus. Kedua-dua versi terdapat dalam kod sumber untuk artikel ini.

muat turun Benchmarking Java: Muat turun kod 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 pengaturcaraan yang berjalan di JVM dan nampaknya semakin popular. Scala mempunyai sistem jenis bersatu, yang bermaksud bahawa ia tidak membezakan antara primitif dan objek. Menurut Erik Osheim dalam kelas jenis Numerik Scala (Pt. 1), Scala menggunakan jenis primitif apabila mungkin tetapi akan menggunakan objek jika perlu. Demikian pula, keterangan Martin Odersky tentang Scala's Arrays mengatakan bahawa "... array Scala Array[Int]diwakili sebagai Java int[], dan Array[Double]diwakili sebagai Java double[]..."

Jadi adakah ini bermaksud bahawa sistem jenis bersatu Scala akan mempunyai prestasi jangka masa yang setanding dengan jenis primitif Java? Mari lihat.