Praktik terbaik untuk mengurangi aktivitas Pengumpul Sampah di Javascript

95

Saya memiliki aplikasi Javascript yang cukup kompleks, yang memiliki loop utama yang dipanggil 60 kali per detik. Sepertinya ada banyak pengumpulan sampah yang terjadi (berdasarkan keluaran 'gigi gergaji' dari timeline Memori di alat pengembang Chrome) - dan ini sering memengaruhi kinerja aplikasi.

Jadi, saya mencoba meneliti praktik terbaik untuk mengurangi jumlah pekerjaan yang harus dilakukan pemulung. (Sebagian besar informasi yang dapat saya temukan di web berkenaan dengan menghindari kebocoran memori, yang merupakan pertanyaan yang sedikit berbeda - ingatan saya semakin dibebaskan, hanya saja ada terlalu banyak pengumpulan sampah yang terjadi.) Saya berasumsi bahwa ini sebagian besar bermuara pada penggunaan kembali objek sebanyak mungkin, tetapi tentu saja iblis ada dalam detailnya.

Aplikasi ini disusun dalam 'kelas' di sepanjang garis Warisan JavaScript Sederhana John Resig .

Saya pikir satu masalah adalah bahwa beberapa fungsi dapat dipanggil ribuan kali per detik (karena digunakan ratusan kali selama setiap iterasi loop utama), dan mungkin variabel kerja lokal dalam fungsi ini (string, array, dll.) mungkin masalahnya.

Saya mengetahui pengumpulan objek untuk objek yang lebih besar / lebih berat (dan kami menggunakan ini sampai tingkat tertentu), tetapi saya mencari teknik yang dapat diterapkan di seluruh papan, terutama yang berkaitan dengan fungsi yang sering dipanggil dalam loop ketat .

Teknik apa yang dapat saya gunakan untuk mengurangi jumlah pekerjaan yang harus dilakukan oleh pengumpul sampah?

Dan, mungkin juga - teknik apa yang dapat digunakan untuk mengidentifikasi objek mana yang paling banyak dikumpulkan sampah? (Ini adalah basis kode yang sangat besar, jadi membandingkan snapshot dari heap belum terlalu membuahkan hasil)

UpTheCreek
sumber
2
Apakah Anda memiliki contoh kode yang dapat Anda tunjukkan kepada kami? Pertanyaannya akan lebih mudah dijawab (tetapi juga berpotensi kurang umum, jadi saya tidak yakin di sini)
John Dvorak
2
Bagaimana jika berhenti menjalankan fungsi ribuan kali per detik? Benarkah itu satu-satunya cara untuk melakukan ini? Pertanyaan ini sepertinya masalah XY. Anda mendeskripsikan X tetapi yang sebenarnya Anda cari adalah solusi untuk Y.
Travis J
2
@TravisJ: Dia menjalankannya hanya 60 kali per detik, yang merupakan kecepatan animasi yang cukup umum. Dia tidak meminta untuk melakukan lebih sedikit pekerjaan, tetapi bagaimana melakukannya dengan lebih hemat pengumpulan sampah.
Bergi
1
@Bergi - "beberapa fungsi dapat dipanggil ribuan kali per detik". Itu sekali per milidetik (mungkin lebih buruk!). Itu sama sekali tidak umum. 60 kali per detik seharusnya tidak menjadi masalah. Pertanyaan ini terlalu kabur dan hanya akan menghasilkan opini atau tebakan.
Travis J
4
@TravisJ - Sama sekali tidak biasa dalam kerangka kerja game.
UpTheCreek

Jawaban:

128

Banyak hal yang perlu Anda lakukan untuk meminimalkan churn GC bertentangan dengan apa yang dianggap JS idiomatik di sebagian besar skenario lainnya, jadi harap perhatikan konteksnya saat menilai saran yang saya berikan.

Alokasi terjadi pada penafsir modern di beberapa tempat:

  1. Saat Anda membuat objek melalui newatau melalui sintaks literal [...], atau {}.
  2. Saat Anda menggabungkan string.
  3. Saat Anda memasuki lingkup yang berisi deklarasi fungsi.
  4. Saat Anda melakukan tindakan yang memicu pengecualian.
  5. Ketika Anda mengevaluasi ekspresi fungsi: (function (...) { ... }).
  6. Saat Anda melakukan operasi yang memaksa ke Objek seperti Object(myNumber)atauNumber.prototype.toString.call(42)
  7. Ketika Anda memanggil builtin yang melakukan semua ini di bawah tenda, seperti Array.prototype.slice.
  8. Saat Anda menggunakan argumentsuntuk merefleksikan daftar parameter.
  9. Saat Anda memisahkan string atau mencocokkan dengan ekspresi reguler.

Hindari melakukan itu, dan kumpulkan serta gunakan kembali objek jika memungkinkan.

Secara khusus, perhatikan peluang untuk:

  1. Tarik fungsi dalam yang tidak memiliki atau sedikit ketergantungan pada keadaan tertutup ke dalam cakupan yang lebih tinggi dan berumur lebih lama. (Beberapa pengurang kode seperti kompilator Penutupan dapat menyebariskan fungsi dalam dan dapat meningkatkan kinerja GC Anda.)
  2. Hindari menggunakan string untuk merepresentasikan data terstruktur atau untuk pengalamatan dinamis. Terutama hindari penguraian berulang-ulang menggunakan splitatau pencocokan ekspresi reguler karena masing-masing memerlukan beberapa alokasi objek. Ini sering terjadi dengan kunci ke dalam tabel pemeta dan ID simpul DOM dinamis. Misalnya, lookupTable['foo-' + x]dan document.getElementById('foo-' + x)keduanya melibatkan alokasi karena ada rangkaian string. Seringkali Anda dapat melampirkan kunci ke objek berumur panjang alih-alih menggabungkan kembali. Bergantung pada browser yang perlu Anda dukung, Anda mungkin dapat Mapmenggunakan objek sebagai kunci secara langsung.
  3. Hindari menangkap pengecualian pada jalur kode normal. Alih-alih try { op(x) } catch (e) { ... }, lakukan if (!opCouldFailOn(x)) { op(x); } else { ... }.
  4. Ketika Anda tidak dapat menghindari pembuatan string, misalnya untuk meneruskan pesan ke server, gunakan builtin seperti JSON.stringifyyang menggunakan buffer asli internal untuk mengakumulasi konten daripada mengalokasikan beberapa objek.
  5. Hindari penggunaan callback untuk peristiwa frekuensi tinggi, dan jika Anda bisa, teruskan sebagai callback fungsi berumur panjang (lihat 1) yang membuat ulang status dari konten pesan.
  6. Hindari penggunaan argumentskarena fungsi yang digunakan harus membuat objek seperti array saat dipanggil.

Saya menyarankan menggunakan JSON.stringifyuntuk membuat pesan jaringan keluar. Mengurai pesan input menggunakan JSON.parsejelas melibatkan alokasi, dan banyak untuk pesan besar. Jika Anda dapat merepresentasikan pesan masuk Anda sebagai array primitif, maka Anda dapat menghemat banyak alokasi. Satu-satunya bawaan lain yang bisa Anda gunakan untuk membuat parser yang tidak mengalokasikan adalah String.prototype.charCodeAt. Parser untuk format kompleks yang hanya menggunakan itu akan sangat sulit untuk dibaca.

Mike Samuel
sumber
Tidakkah menurut Anda JSON.parseobjek d mengalokasikan lebih sedikit (atau sama) ruang daripada string pesan?
Bergi
@Bergi, Itu tergantung pada apakah nama properti memerlukan alokasi terpisah, tetapi parser yang menghasilkan kejadian daripada pohon parse tidak melakukan alokasi asing.
Mike Samuel
Jawaban yang fantastis, terima kasih! Banyak permintaan maaf atas bounty yang kedaluwarsa - Saya sedang bepergian pada saat itu, dan untuk beberapa alasan saya tidak dapat masuk ke SO dengan akun gmail saya di ponsel saya ....: /
UpTheCreek
Untuk menebus waktu buruk saya dengan hadiah, saya telah menambahkan satu tambahan untuk menambahnya (200 adalah jumlah minimum yang bisa saya berikan;) - Untuk beberapa alasan meskipun itu mengharuskan saya menunggu 24 jam sebelum saya memberikannya (meskipun Saya memilih 'hadiahi jawaban yang ada'). Akan menjadi milikmu besok ...
UpTheCreek
@UpTheCreek, jangan khawatir. Saya senang Anda menganggapnya berguna.
Mike Samuel
12

Alat pengembang Chrome memiliki fitur yang sangat bagus untuk melacak alokasi memori. Ini disebut Memory Timeline. Artikel ini menjelaskan beberapa detail. Saya kira ini yang Anda bicarakan tentang "gigi gergaji"? Ini adalah perilaku normal untuk sebagian besar runtime yang di-GC. Alokasi berlanjut hingga ambang penggunaan tercapai yang memicu pengumpulan. Biasanya ada berbagai jenis koleksi di ambang yang berbeda.

Garis Waktu Memori di Chrome

Koleksi sampah termasuk dalam daftar acara yang terkait dengan jejak beserta durasinya. Di buku catatan saya yang agak lama, koleksi singkat terjadi pada sekitar 4 MB dan memakan waktu 30 md. Ini adalah 2 dari iterasi loop 60Hz Anda. Jika ini adalah animasi, koleksi 30 md mungkin menyebabkan gagap. Anda harus mulai di sini untuk melihat apa yang terjadi di lingkungan Anda: di mana ambang batas pengumpulan dan berapa lama waktu pengambilan koleksi Anda. Ini memberi Anda titik referensi untuk menilai pengoptimalan. Tetapi Anda mungkin tidak akan melakukan lebih baik daripada mengurangi frekuensi gagap dengan memperlambat tingkat alokasi, memperpanjang interval antar koleksi.

Langkah selanjutnya adalah menggunakan Profiles | Fitur Record Heap Allocations untuk menghasilkan katalog alokasi berdasarkan jenis record. Ini akan segera menunjukkan tipe objek mana yang paling banyak mengonsumsi memori selama periode pelacakan, yang setara dengan tingkat alokasi. Fokus pada ini dalam urutan suku bunga menurun.

Tekniknya bukanlah ilmu roket. Hindari objek dalam kotak jika Anda dapat melakukannya dengan yang tidak dikotakkan. Gunakan variabel global untuk menahan dan menggunakan kembali objek kotak tunggal daripada mengalokasikan yang baru di setiap iterasi. Gabungkan jenis objek umum dalam daftar gratis daripada mengabaikannya. Hasil penggabungan string cache yang kemungkinan dapat digunakan kembali di iterasi mendatang. Hindari alokasi hanya untuk mengembalikan hasil fungsi dengan mengatur variabel dalam lingkup tertutup sebagai gantinya. Anda harus mempertimbangkan setiap jenis objek dalam konteksnya sendiri untuk menemukan strategi terbaik. Jika Anda membutuhkan bantuan dengan spesifik, posting suntingan yang menjelaskan rincian tantangan yang Anda lihat.

Saya menyarankan agar tidak mengubah gaya pengkodean normal Anda di seluruh aplikasi dalam upaya senapan untuk menghasilkan lebih sedikit sampah. Ini untuk alasan yang sama Anda tidak harus mengoptimalkan kecepatan sebelum waktunya. Sebagian besar usaha Anda ditambah banyak kerumitan tambahan dan ketidakjelasan kode tidak akan berarti.

Gen
sumber
Benar, itulah yang saya maksud dengan gigi gergaji. Saya tahu akan selalu ada pola gigi gergaji, tetapi kekhawatiran saya adalah bahwa dengan aplikasi saya frekuensi gigi gergaji dan 'tebing' cukup tinggi. Menariknya, acara GC tidak muncul pada timeline saya - satu-satunya peristiwa yang muncul di panel 'catatan' (yang di tengah) adalah: request animation frame, animation frame fired, dan composite layers. Saya tidak tahu mengapa saya tidak melihat GC Eventseperti Anda (ini ada di versi terbaru chrome, dan juga kenari).
UpTheCreek
4
Saya sudah mencoba menggunakan profiler dengan 'catatan alokasi tumpukan' tetapi sejauh ini belum terlalu berguna. Mungkin ini karena saya tidak tahu bagaimana menggunakannya dengan benar. Sepertinya penuh dengan referensi yang tidak ada artinya bagi saya, seperti @342342dan code relocation info.
UpTheCreek
Mengenai "pengoptimalan prematur adalah akar dari segala kejahatan": Pahami. Jangan ikuti begitu saja. Dalam skenario tertentu, seperti permainan dan pemrograman multimedia, kinerja adalah yang terpenting dan Anda akan memiliki banyak kode "panas". Jadi ya, Anda harus menyesuaikan gaya pemrograman Anda.
snarf
9

Sebagai prinsip umum, Anda ingin menyimpan sebanyak mungkin dan melakukan sesedikit mungkin pembuatan dan penghancuran untuk setiap putaran loop Anda.

Hal pertama yang muncul di kepala saya adalah mengurangi penggunaan fungsi anonim (jika ada) di dalam loop utama Anda. Juga akan mudah untuk jatuh ke dalam perangkap membuat dan menghancurkan objek yang diteruskan ke fungsi lain. Saya sama sekali bukan ahli javascript, tetapi saya akan membayangkan bahwa ini:

var options = {var1: value1, var2: value2, ChangingVariable: value3};
function loopfunc()
{
    //do something
}

while(true)
{
    $.each(listofthings, loopfunc);

    options.ChangingVariable = newvalue;
    someOtherFunction(options);
}

akan berjalan lebih cepat dari ini:

while(true)
{
    $.each(listofthings, function(){
        //do something on the list
    });

    someOtherFunction({
        var1: value1,
        var2: value2,
        ChangingVariable: newvalue
    });
}

Apakah ada waktu henti untuk program Anda? Mungkin Anda membutuhkannya agar berjalan lancar selama satu atau dua detik (misalnya untuk animasi) dan kemudian memiliki lebih banyak waktu untuk memproses? Jika ini kasusnya, saya dapat melihat mengambil objek yang biasanya akan menjadi sampah yang dikumpulkan selama animasi dan menyimpan referensi ke objek tersebut di beberapa objek global. Kemudian saat animasi berakhir Anda dapat menghapus semua referensi dan membiarkan pengumpul sampah melakukan tugasnya.

Maaf jika ini semua agak sepele dibandingkan dengan apa yang sudah Anda coba dan pikirkan.

Chris B
sumber
Ini. Ditambah juga fungsi yang disebutkan di dalam fungsi lain (yang bukan IIFE) juga merupakan penyalahgunaan umum yang menghabiskan banyak memori dan mudah terlewat.
Esailija
Terima kasih Chris! Sayangnya, saya tidak memiliki waktu henti: /
UpTheCreek
4

Saya akan membuat satu atau beberapa objek di global scope(di mana saya yakin pengumpul sampah tidak diizinkan untuk menyentuhnya), lalu saya akan mencoba memfaktor ulang solusi saya untuk menggunakan objek tersebut untuk menyelesaikan pekerjaan, daripada menggunakan variabel lokal .

Tentu saja ini tidak dapat dilakukan di semua tempat dalam kode, tetapi secara umum itulah cara saya untuk menghindari pengumpul sampah.

PS Ini mungkin membuat bagian tertentu dari kode sedikit kurang dapat dipelihara.

Mahdi
sumber
GC mengeluarkan variabel cakupan global saya secara konsisten.
VectorVortec