Mendeklarasikan beberapa larik dengan 64 elemen 1000 kali lebih cepat daripada mendeklarasikan larik yang terdiri dari 65 elemen

91

Baru-baru ini saya menyadari bahwa mendeklarasikan array yang berisi 64 elemen jauh lebih cepat (> 1000 kali lipat) daripada mendeklarasikan tipe array yang sama dengan 65 elemen.

Inilah kode yang saya gunakan untuk menguji ini:

public class Tests{
    public static void main(String args[]){
        double start = System.nanoTime();
        int job = 100000000;//100 million
        for(int i = 0; i < job; i++){
            double[] test = new double[64];
        }
        double end = System.nanoTime();
        System.out.println("Total runtime = " + (end-start)/1000000 + " ms");
    }
}

Ini berjalan di sekitar 6 ms, jika saya mengganti new double[64]dengan new double[65]yang dibutuhkan sekitar 7 detik. Masalah ini menjadi lebih parah secara eksponensial jika pekerjaan tersebar di semakin banyak utas, dari situlah masalah saya berasal.

Masalah ini juga terjadi dengan berbagai jenis array seperti int[65]atau String[65]. Masalah ini tidak terjadi dengan string besar:String test = "many characters"; tetapi mulai terjadi saat ini diubah menjadiString test = i + "";

Saya bertanya-tanya mengapa hal ini terjadi dan apakah mungkin untuk menghindari masalah ini.

Sipko
sumber
3
Di luar catatan: System.nanoTime()lebih disukai daripada System.currentTimeMillis()untuk pembandingan.
rocketboy
4
Saya hanya penasaran Apakah Anda menggunakan Linux? Apakah perilaku berubah dengan OS?
bsd
9
Bagaimana mungkin pertanyaan ini mendapat Downvote ??
Rohit Jain
2
FWIW, saya melihat perbedaan kinerja yang serupa jika saya menjalankan kode ini dengan bytealih - alih double.
Oliver Charlesworth
3
@ThomasJungblut: Jadi apa yang menjelaskan perbedaan dalam eksperimen OP?
Oliver Charlesworth

Jawaban:

88

Anda mengamati perilaku yang disebabkan oleh pengoptimalan yang dilakukan oleh compiler JIT pada VM Java Anda. Perilaku ini dapat direproduksi dipicu dengan larik skalar hingga 64 elemen, dan tidak dipicu dengan larik yang lebih besar dari 64.

Sebelum masuk ke detailnya, mari kita lihat lebih dekat badan loop:

double[] test = new double[64];

Tubuh tidak berpengaruh (perilaku yang dapat diamati) . Itu berarti tidak ada perbedaan di luar eksekusi program apakah pernyataan ini dijalankan atau tidak. Hal yang sama berlaku untuk seluruh loop. Jadi mungkin saja terjadi, bahwa pengoptimal kode menerjemahkan loop menjadi sesuatu (atau tidak sama sekali) dengan perilaku waktu fungsional dan berbeda yang sama.

Untuk tolok ukur, Anda setidaknya harus mematuhi dua pedoman berikut. Jika Anda melakukannya, perbedaannya akan jauh lebih kecil.

  • Lakukan pemanasan kompiler JIT (dan pengoptimal) dengan menjalankan benchmark beberapa kali.
  • Gunakan hasil dari setiap ekspresi dan cetak di akhir tolok ukur.

Sekarang mari kita bahas detailnya. Tidak mengherankan jika ada pengoptimalan yang dipicu untuk array skalar tidak lebih dari 64 elemen. Pengoptimalan adalah bagian dari analisis Escape . Ini menempatkan objek kecil dan larik kecil ke tumpukan alih-alih mengalokasikannya di heap - atau bahkan lebih baik mengoptimalkannya sepenuhnya. Anda dapat menemukan beberapa informasi tentang itu dalam artikel berikut oleh Brian Goetz yang ditulis pada tahun 2005:

Pengoptimalan dapat dinonaktifkan dengan opsi baris perintah -XX:-DoEscapeAnalysis. Nilai ajaib 64 untuk larik skalar juga dapat diubah pada baris perintah. Jika Anda menjalankan program Anda sebagai berikut, tidak akan ada perbedaan antara array dengan 64 dan 65 elemen:

java -XX:EliminateAllocationArraySizeLimit=65 Tests

Karena itu, saya sangat tidak menyarankan menggunakan opsi baris perintah seperti itu. Saya ragu itu membuat perbedaan besar dalam aplikasi yang realistis. Saya hanya akan menggunakannya, jika saya benar-benar yakin akan kebutuhannya - dan tidak berdasarkan hasil dari beberapa tolok ukur palsu.

nosid
sumber
9
Tetapi mengapa pengoptimal mendeteksi bahwa larik ukuran 64 dapat dilepas tetapi tidak 65
ug_
10
@nosid: Meskipun kode OP mungkin tidak realistis, itu jelas memicu perilaku yang menarik / tidak terduga di JVM, yang mungkin berdampak pada situasi lain. Saya pikir itu sah untuk bertanya mengapa ini terjadi.
Oliver Charlesworth
1
@ThomasJungblut Saya tidak berpikir loop akan dihapus. Anda dapat menambahkan "int total" di luar pengulangan dan menambahkan "total + = test [0];" untuk contoh di atas. Kemudian mencetak hasilnya Anda akan melihat bahwa total = 100 juta dan itu berjalan dalam waktu kurang dari satu detik.
Sipko
1
Penggantian on stack adalah tentang mengganti kode yang ditafsirkan dengan yang dikompilasi dengan cepat, alih-alih mengganti alokasi heap dengan alokasi tumpukan. EliminateAllocationArraySizeLimit adalah ukuran batas array yang dianggap dapat diganti skalar dalam analisis escape. Jadi poin utama bahwa efeknya adalah karena pengoptimalan compiler benar, tetapi ini bukan karena alokasi tumpukan, tetapi karena fase analisis pelolosan gagal untuk melihat alokasi tidak diperlukan.
kiheru
2
@Sipko: Anda menulis bahwa aplikasi tidak diskalakan dengan jumlah utas. Itu indikasi, bahwa masalahnya tidak terkait dengan pengoptimalan mikro yang Anda tanyakan. Saya merekomendasikan untuk melihat gambaran besarnya daripada bagian kecilnya.
nosid
2

Ada beberapa cara untuk membuat perbedaan, berdasarkan ukuran sebuah benda.

Seperti yang dinyatakan nosid, JITC mungkin (kemungkinan besar) mengalokasikan objek "lokal" kecil pada tumpukan, dan batas ukuran untuk larik "kecil" mungkin pada 64 elemen.

Mengalokasikan pada stack secara signifikan lebih cepat daripada mengalokasikan di heap, dan, lebih tepatnya, stack tidak perlu dikumpulkan dari sampah, sehingga overhead GC sangat berkurang. (Dan untuk kasus uji ini, overhead GC kemungkinan 80-90% dari total waktu eksekusi.)

Selanjutnya, setelah nilai dialokasikan tumpukan, JITC dapat melakukan "penghapusan kode mati", menentukan bahwa hasil newtidak pernah digunakan di mana pun, dan, setelah memastikan tidak ada efek samping yang akan hilang, hilangkan seluruh newoperasi, dan kemudian loop (sekarang kosong) itu sendiri.

Meskipun JITC tidak melakukan alokasi tumpukan, sangat mungkin objek yang lebih kecil dari ukuran tertentu untuk dialokasikan di heap secara berbeda (misalnya, dari "ruang" yang berbeda) dari objek yang lebih besar. (Namun, biasanya ini tidak akan menghasilkan perbedaan waktu yang begitu dramatis.)

Licks panas
sumber
Terlambat untuk utas ini. Mengapa mengalokasikan di stack lebih cepat daripada mengalokasikan di heap? Menurut beberapa artikel, mengalokasikan di heap membutuhkan ~ 12 instruksi. Tidak banyak ruang untuk perbaikan.
Vortex
@Vortex - Mengalokasikan ke tumpukan membutuhkan 1-2 instruksi. Tapi itu untuk mengalokasikan seluruh bingkai tumpukan. Kerangka stack harus dialokasikan bagaimanapun juga untuk memiliki area penyimpanan register untuk rutinitas, sehingga variabel lain yang dialokasikan pada saat yang sama adalah "gratis". Dan seperti yang saya katakan, tumpukan tidak membutuhkan GC. Overhead GC untuk item heap jauh lebih besar daripada biaya operasi alokasi heap.
Hot Licks