Bagaimana cara meningkatkan ukuran tumpukan Java?

123

Saya mengajukan pertanyaan ini untuk mengetahui cara meningkatkan ukuran tumpukan panggilan runtime di JVM. Saya mendapat jawaban untuk ini, dan saya juga mendapat banyak jawaban dan komentar berguna yang relevan dengan cara Java menangani situasi di mana tumpukan runtime yang besar diperlukan. Saya telah memperpanjang pertanyaan saya dengan ringkasan tanggapan.

Awalnya saya ingin meningkatkan ukuran tumpukan JVM sehingga program seperti berjalan tanpa file StackOverflowError.

public class TT {
  public static long fact(int n) {
    return n < 2 ? 1 : n * fact(n - 1);
  }
  public static void main(String[] args) {
    System.out.println(fact(1 << 15));
  }
}

Pengaturan konfigurasi yang sesuai adalah tanda java -Xss...baris perintah dengan nilai yang cukup besar. Untuk program di TTatas, ia bekerja seperti ini dengan JVM OpenJDK:

$ javac TT.java
$ java -Xss4m TT

Salah satu jawaban juga menunjukkan bahwa -X...flag bergantung pada implementasi. Saya menggunakan

java version "1.6.0_18"
OpenJDK Runtime Environment (IcedTea6 1.8.1) (6b18-1.8.1-0ubuntu1~8.04.3)
OpenJDK 64-Bit Server VM (build 16.0-b13, mixed mode)

Dimungkinkan juga untuk menentukan tumpukan besar hanya untuk satu utas (lihat di salah satu jawaban caranya). Ini disarankan java -Xss...untuk menghindari pemborosan memori untuk utas yang tidak membutuhkannya.

Saya ingin tahu seberapa besar tumpukan yang dibutuhkan program di atas, jadi saya telah menjalankannya nmeningkat:

  • -Xss4m bisa cukup untuk fact(1 << 15)
  • -Xss5m sudah cukup untuk fact(1 << 17)
  • -Xss7m sudah cukup untuk fact(1 << 18)
  • -Xss9m sudah cukup untuk fact(1 << 19)
  • -Xss18m sudah cukup untuk fact(1 << 20)
  • -Xss35m sudah cukup untuk fact(1 << 21)
  • -Xss68m sudah cukup untuk fact(1 << 22)
  • -Xss129m sudah cukup untuk fact(1 << 23)
  • -Xss258m sudah cukup untuk fact(1 << 24)
  • -Xss515m sudah cukup untuk fact(1 << 25)

Dari angka-angka di atas, tampaknya Java menggunakan sekitar 16 byte per stack frame untuk fungsi di atas, ini wajar.

Pencacahan di atas berisi bisa cukup, bukan cukup , karena persyaratan tumpukan tidak deterministik: menjalankannya beberapa kali dengan file sumber yang sama dan -Xss...terkadang sama berhasil dan terkadang menghasilkan StackOverflowError. Misalnya untuk 1 << 20, -Xss18msudah cukup dalam 7 habis dari 10, dan -Xss19mtidak selalu cukup juga, tetapi -Xss20msudah cukup (di semua 100 dari 100). Apakah pengumpulan sampah, JIT yang bekerja, atau sesuatu yang lain menyebabkan perilaku nondeterministik ini?

Pelacakan tumpukan yang dicetak di StackOverflowError(dan mungkin juga di pengecualian lain) hanya menampilkan 1024 elemen terbaru dari tumpukan runtime. Jawaban di bawah ini menunjukkan bagaimana menghitung kedalaman yang dicapai dengan tepat (yang mungkin jauh lebih besar dari 1024).

Banyak orang yang menanggapi telah menunjukkan bahwa adalah praktik pengkodean yang baik dan aman untuk mempertimbangkan alternatif, implementasi yang tidak terlalu haus tumpukan dari algoritme yang sama. Secara umum, dimungkinkan untuk mengonversi ke satu set fungsi rekursif ke fungsi iteratif (menggunakan Stackobjek eg , yang diisi di heap dan bukan di stack runtime). Untuk factfungsi khusus ini , cukup mudah untuk mengubahnya. Versi iteratif saya akan terlihat seperti ini:

public class TTIterative {
  public static long fact(int n) {
    if (n < 2) return 1;
    if (n > 65) return 0;  // Enough powers of 2 in the product to make it (long)0.
    long f = 2;
    for (int i = 3; i <= n; ++i) {
      f *= i;
    }
    return f;
  }
  public static void main(String[] args) {
    System.out.println(fact(1 << 15));
  }
}

FYI, seperti yang ditunjukkan oleh solusi berulang di atas, factfungsi tersebut tidak dapat menghitung faktorial angka yang tepat di atas 65 (sebenarnya, bahkan di atas 20), karena tipe bawaan Java longakan meluap. Refactoring factsehingga akan mengembalikan a, BigIntegerbukan longakan menghasilkan hasil yang tepat untuk input yang besar juga.

poin
sumber
Terlihat lebih sederhana dari yang sebenarnya. fakta () dipanggil 32K kali secara rekursif. Itu harus kurang dari 1MB tumpukan. : - /
Aaron Digulla
@ Aaron: + Fungsi overhead, yang .. BANYAK
halfdan
4
Selain masalah tumpukan Anda. perhatikan bahwa Anda meledakkan panjang dan int Anda. 1 << 4 adalah nilai maksimal yang dapat saya gunakan sebelum negatif dan kemudian menjadi 0. Coba gunakan BigInteger
Sean
Tidak yakin bahwa fungsi overhead sebenarnya terlalu banyak-- Saya pikir Anda masih dapat melakukan panggilan 2 ^ 15 dalam urutan beberapa megabyte ruang tumpukan.
Neil Coffey
7
Catatan: Anda menyetel ukuran tumpukan setiap utas dan menghasilkan hasil yang tidak berarti, semuanya untuk menghindari pemfaktoran ulang satu baris kode. Saya senang Anda telah mengatur prioritas Anda. : P
Peter Lawrey

Jawaban:

78

Hmm ... ini berfungsi untuk saya dan dengan tumpukan kurang dari 999MB:

> java -Xss4m Test
0

(Windows JDK 7, VM klien build 17.0-b05, dan Linux JDK 6 - informasi versi yang sama seperti yang Anda posting)

Jon Skeet
sumber
1
kemungkinan besar itu untuk komentar saya, saya menghapusnya ketika saya menyadari hal yang sama seperti yang diposting Neil.
Sean
Berkat pertanyaan ini dan jawaban Anda, saya berhasil menyelesaikan tugas saya. Fungsi DFS saya harus muncul kembali pada grafik dengan ~ 10 ^ 5 simpul. Akhirnya ini bekerja dengan -Xss129m: D
bholagabbar
11

Saya berasumsi Anda menghitung "kedalaman 1024" dengan garis berulang di jejak tumpukan?

Jelas, panjang array pelacakan tumpukan di Throwable tampaknya dibatasi hingga 1024. Coba program berikut:

public class Test {

    public static void main(String[] args) {

        try {
            System.out.println(fact(1 << 15));
        }
        catch (StackOverflowError e) {
            System.err.println("true recursion level was " + level);
            System.err.println("reported recursion level was " +
                               e.getStackTrace().length);
        }
    }

    private static int level = 0;
    public static long fact(int n) {
        level++;
        return n < 2 ? n : n * fact(n - 1);
    }
}
Jay
sumber
9

Jika Anda ingin bermain dengan ukuran tumpukan utas, Anda akan ingin melihat opsi -Xss di Hotspot JVM. Ini mungkin sesuatu yang berbeda pada VM non Hotspot karena parameter -X ke JVM spesifik untuk distribusi, IIRC.

Di Hotspot, ini terlihat seperti java -Xss16Mjika Anda ingin membuat ukuran 16 MB.

Tipe java -X -help jika Anda ingin melihat semua parameter JVM spesifik distribusi yang dapat Anda berikan. Saya tidak yakin apakah ini berfungsi sama pada JVM lain, tetapi akan mencetak semua parameter spesifik Hotspot.

Untuk apa nilainya - saya akan merekomendasikan membatasi penggunaan metode rekursif Anda di Java. Tidak terlalu bagus dalam mengoptimalkannya - untuk satu JVM tidak mendukung rekursi ekor (lihat Apakah JVM mencegah pengoptimalan panggilan ekor? ). Coba lakukan pemfaktoran ulang kode faktorial Anda di atas untuk menggunakan loop sementara, bukan panggilan metode rekursif.

ikan paus
sumber
8

Satu-satunya cara untuk mengontrol ukuran tumpukan dalam proses adalah memulai yang baru Thread. Tapi Anda juga bisa mengontrol dengan membuat proses sub Java yang memanggil sendiri dengan -Xssparameter.

public class TT {
    private static int level = 0;

    public static long fact(int n) {
        level++;
        return n < 2 ? n : n * fact(n - 1);
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(null, null, "TT", 1000000) {
            @Override
            public void run() {
                try {
                    level = 0;
                    System.out.println(fact(1 << 15));
                } catch (StackOverflowError e) {
                    System.err.println("true recursion level was " + level);
                    System.err.println("reported recursion level was "
                            + e.getStackTrace().length);
                }
            }

        };
        t.start();
        t.join();
        try {
            level = 0;
            System.out.println(fact(1 << 15));
        } catch (StackOverflowError e) {
            System.err.println("true recursion level was " + level);
            System.err.println("reported recursion level was "
                    + e.getStackTrace().length);
        }
    }

}
Dennis C
sumber
Terima kasih atas jawaban informatif ini, senang mengetahui tentang opsi selain java -Xss....
poin
1
Saya bersemangat tentang ini, tetapi kemudian setelah membaca docs.oracle.com/javase/6/docs/api/java/lang/Thread.html#Thread - konstruktor stacksize - kegembiraan itu hilang.
kellogs
Saya ingin tahu platform mana mereka ketika dokumen hanya mengatakan - "Di beberapa platform"
Dennis C
3

Tambahkan opsi ini

--driver-java-options -Xss512m

ke perintah spark-submit Anda akan memperbaiki masalah ini.

Guibin Zhang
sumber
2

Sulit untuk memberikan solusi yang masuk akal karena Anda sangat ingin menghindari semua pendekatan yang waras. Refactoring satu baris kode adalah solusi yang senible.

Catatan: Menggunakan -Xss menyetel ukuran tumpukan setiap utas dan merupakan ide yang sangat buruk.

Pendekatan lain adalah manipulasi kode byte untuk mengubah kode sebagai berikut;

public static long fact(int n) { 
    return n < 2 ? n : n > 127 ? 0 : n * fact(n - 1); 
}

diberikan setiap jawaban untuk n> 127 adalah 0. Hal ini untuk menghindari perubahan kode sumber.

Peter Lawrey
sumber
1
Terima kasih telah menunjukkan bahwa menyetel ukuran tumpukan tinggi akan membuang memori untuk utas yang tidak membutuhkannya. Juga terima kasih telah menunjukkan bahwa factfungsi dalam pertanyaan tersebut dapat difaktorkan ulang untuk menggunakan lebih sedikit ruang tumpukan.
poin
1
@pts, terima kasih Anda dicatat. Saya pikir ini adalah pertanyaan yang masuk akal mengingat kasus penggunaan yang jauh lebih kompleks, tetapi itu sangat jarang.
Peter Lawrey
0

Aneh! Anda mengatakan bahwa Anda ingin membuat rekursi 1 << 15 kedalaman ??? !!!!

Saya sarankan JANGAN mencobanya. Ukuran tumpukan akan menjadi 2^15 * sizeof(stack-frame). Saya tidak tahu apa ukuran stack-frame itu, tetapi 2 ^ 15 adalah 32,768. Cukup banyak ... Nah, jika berhenti pada 1024 (2 ^ 10) Anda harus membuatnya 2 ^ 5 kali lebih besar, itu, 32 kali lebih besar daripada dengan pengaturan Anda yang sebenarnya.

helios
sumber
0

Poster lain telah menunjukkan bagaimana meningkatkan memori dan bahwa Anda dapat mengingat panggilan. Saya menyarankan bahwa untuk banyak aplikasi, Anda dapat menggunakan rumus Stirling untuk mendekati n besar! sangat cepat tanpa jejak memori.

Lihatlah posting ini, yang memiliki beberapa analisis fungsi dan kode:

http://threebrothers.org/brendan/blog/stirlings-approximation-formula-clojure/

pelarian
sumber
0

Saya melakukan latihan Anagram , yang seperti masalah Perubahan Hitung tetapi dengan 50.000 denominasi (koin). Saya tidak yakin itu bisa dilakukan berulang-ulang , saya tidak peduli. Saya hanya tahu bahwa opsi -xss tidak berpengaruh - Saya selalu gagal setelah 1024 stack frame (mungkin scala melakukan pekerjaan yang buruk untuk mengirim ke batasan java atau printStackTrace. Saya tidak tahu). Ini adalah pilihan yang buruk, seperti yang dijelaskan. Anda tidak ingin semua utas yang masuk ke aplikasi menjadi mengerikan. Namun, saya melakukan beberapa eksperimen dengan Thread baru (ukuran tumpukan). Ini memang berhasil,

  def measureStackDepth(ss: Long): Long = {
    var depth: Long = 0
      val thread: Thread = new Thread(null, new Runnable() {
        override def run() {
          try {
          def sum(n: Long): Long = {depth += 1; if (n== 0) 0 else sum(n-1) + 1}
          println("fact = " + sum(ss * 10))
          } catch {
            case e: StackOverflowError => // eat the exception, that is expected
          }
        }
      }, "deep stack for money exchange", ss)
      thread.start()
      thread.join()
    depth
  }                                               //> measureStackDepth: (ss: Long)Long


  for (ss <- (0 to 10)) println("ss = 10^" +  ss + " allows stack of size " -> measureStackDepth((scala.math.pow (10, ss)).toLong) )
                                                  //> fact = 10
                                                  //| (ss = 10^0 allows stack of size ,11)
                                                  //| fact = 100
                                                  //| (ss = 10^1 allows stack of size ,101)
                                                  //| fact = 1000
                                                  //| (ss = 10^2 allows stack of size ,1001)
                                                  //| fact = 10000
                                                  //| (ss = 10^3 allows stack of size ,10001)
                                                  //| (ss = 10^4 allows stack of size ,1336)
                                                  //| (ss = 10^5 allows stack of size ,5456)
                                                  //| (ss = 10^6 allows stack of size ,62736)
                                                  //| (ss = 10^7 allows stack of size ,623876)
                                                  //| (ss = 10^8 allows stack of size ,6247732)
                                                  //| (ss = 10^9 allows stack of size ,62498160)

Anda melihat bahwa tumpukan dapat tumbuh lebih dalam secara eksponensial dengan lebih banyak tumpukan yang dialokasikan ke utas secara eksponensial.

Val
sumber