Yang lebih cepat: alokasi tumpukan atau alokasi tumpukan

Pertanyaan ini mungkin terdengar sangat mendasar, tetapi ini adalah diskusi dengan mana saya bekerja dengan pengembang lain dengan siapa saya bekerja.

Saya mencoba untuk menumpuk hal-hal di mana saya bisa, bukannya tumpukan, menyoroti mereka. Dia berbicara dengan saya dan memperhatikan bahu saya dan berkomentar bahwa ini tidak perlu karena mereka sama untuk kinerja.

Saya selalu mendapat kesan bahwa pertumbuhan tumpukan konstan, dan kinerja distribusi tumpukan bergantung pada kompleksitas tumpukan saat ini, baik untuk distribusi (menemukan lubang berukuran tepat) dan untuk memilih (melipat lubang untuk mengurangi fragmentasi, karena banyak implementasi perpustakaan standar membutuhkan waktu untuk melakukan ini selama penghapusan, jika saya tidak salah).

Ini menurut saya sesuatu yang cenderung sangat tergantung pada kompiler. Untuk proyek ini, khususnya, saya menggunakan kompiler Metrowerks untuk PPC . Ketajaman dalam kombinasi ini akan sangat berguna, tetapi, secara umum, untuk GCC dan MSVC ++, ada apa? Apakah distribusi timbunan tidak setinggi alokasi tumpukan? Apakah tidak ada perbedaan? Atau apakah itu perbedaan, sehingga menit menjadi mikro-optimasi tidak berarti.

426
02 окт. Adam diatur 02 Oktober. 2008-10-02 09:06 '08 pada jam 9:06 2008-10-02 09:06
@ 23 jawaban

Alokasi tumpukan jauh lebih cepat, karena yang dia lakukan hanyalah memindahkan penunjuk tumpukan. Menggunakan kumpulan memori, Anda bisa mendapatkan kinerja yang sebanding dari distribusi tumpukan, tetapi ini disebabkan oleh kompleksitas kecil dan sakit kepala.

Selain itu, tumpukan terhadap tumpukan tidak hanya memperhitungkan kinerja akun; dia juga mengatakan banyak tentang kehidupan objek yang diharapkan.

426
02 окт. jawabannya diberikan kepada Torbjörn Gyllebring 02 Oktober. 2008-10-02 09:09 '08 pada 9:09 2008-10-02 09:09

Tumpukannya jauh lebih cepat. Secara harfiah hanya menggunakan satu instruksi untuk sebagian besar arsitektur, dalam banyak kasus, misalnya. pada x86:

 sub esp, 0x10 

(Ini menggerakkan penunjuk tumpukan turun 0x10 byte dan dengan demikian "mendistribusikan" byte ini untuk menggunakan variabel.)

Tentu saja, ukuran tumpukan sangat, sangat terbatas, karena Anda dengan cepat mengetahui apakah Anda menyalahgunakan alokasi tumpukan atau mencoba melakukan rekursi: -)

Selain itu, ada alasan kecil untuk mengoptimalkan kinerja kode yang tidak perlu diverifikasi, misalnya, dengan menggunakan profil. "Optimalisasi prematur" seringkali menyebabkan lebih banyak masalah daripada biayanya.

Aturan praktis saya adalah: jika saya tahu bahwa saya akan memerlukan beberapa data pada waktu kompilasi, dan ukurannya beberapa ratus byte, saya akan meletakkannya di tumpukan. Kalau tidak, aku akan menumpuknya.

145
02 окт. balasan yang diberikan oleh Dan Lenski 02 Okt 2008-10-02 09:16 '08 pada jam 09:16 2008-10-02 09:16

Jujur saja, sepele untuk menulis sebuah program untuk membandingkan kinerja:

 #include <ctime> #include <iostream> namespace { class empty { }; // even empty classes take up 1 byte of space, minimum } int main() { std::clock_t start = std::clock(); for (int i = 0; i < 100000; ++i) empty e; std::clock_t duration = std::clock() - start; std::cout << "stack allocation took " << duration << " clock ticks\n"; start = std::clock(); for (int i = 0; i < 100000; ++i) { empty* e = new empty; delete e; }; duration = std::clock() - start; std::cout << "heap allocation took " << duration << " clock ticks\n"; } 

Dikatakan bahwa konsistensi konyol adalah hobgoblin pikiran kecil . Rupanya, optimisasi kompiler adalah pikiran hobi banyak programmer. Diskusi ini adalah inti dari jawabannya, tetapi orang-orang tampaknya tidak dapat bekerja untuk membacanya, jadi saya beralih ke sini untuk menghindari pertanyaan yang sudah saya jawab.

Kompiler pengoptimal mungkin memperhatikan bahwa kode ini tidak melakukan apa-apa dan dapat mengoptimalkan semuanya. Ini adalah tugas pengoptimal untuk melakukan hal-hal seperti itu, dan perjuangan dengan pengoptimal adalah tugas gila.

Saya akan merekomendasikan mengkompilasi kode ini dengan optimasi yang dimatikan, karena tidak ada cara yang baik untuk mengelabui setiap optimizer yang sedang digunakan atau akan digunakan di masa depan.

Siapa pun yang menyalakan pengoptimal, dan kemudian mengeluh tentang melawannya, harus dihadapkan pada ejekan publik.

Jika saya khawatir dengan presisi nanosecond, saya tidak akan menggunakan std::clock() . Jika saya ingin mempublikasikan hasilnya sebagai disertasi doktoral, saya akan membuat masalah besar tentang ini, dan saya mungkin akan membandingkan GCC, Tendra / Ten15, LLVM, Watcom, Borland, Visual C ++, Digital Mars, ICC dan lain-lain kompiler Pokoknya, heap distribusi memakan waktu ratusan kali lebih lama dari alokasi tumpukan, dan saya tidak melihat sesuatu yang berguna dalam mengeksplorasi lebih lanjut masalah ini.

Pengoptimal memiliki tugas untuk menyingkirkan kode yang saya uji. Saya tidak melihat alasan untuk mengatakan bahwa pengoptimal mulai, dan kemudian mencoba menipu pengoptimal tanpa benar-benar mengoptimalkan. Tetapi jika saya melihat manfaatnya, saya akan melakukan satu atau lebih hal berikut ini:

  • Tambahkan item data untuk empty dan akses item data ini dalam satu lingkaran; tetapi jika saya hanya pernah membaca dari elemen data, optimizer dapat melakukan pelipatan konstan dan menghapus loop; jika saya pernah menulis ke anggota data, pengoptimal dapat melewati segalanya kecuali iterasi terbaru dari loop. Juga, pertanyaannya bukan "alokasi tumpukan dan akses data versus distribusi tumpukan dan akses data."

  • Menyatakan volatile , tetapi volatile sering dikompilasi secara salah (PDF).

  • Ambil alamat e di dalam loop (dan mungkin tetapkan ke variabel yang dinyatakan extern dan didefinisikan dalam file lain). Tetapi bahkan dalam kasus ini, kompiler mungkin memperhatikan bahwa - setidaknya - pada stack, e akan selalu dialokasikan ke alamat memori yang sama, dan kemudian membuat lipatan permanen, seperti pada (1) di atas. Saya mendapatkan semua iterasi dari loop, tetapi objeknya tidak pernah menonjol.

Selain yang jelas, tes ini keliru karena mengukur distribusi dan rilis, dan pertanyaan awal tidak meminta rilis. Tentu saja, variabel yang dialokasikan dalam tumpukan secara otomatis dibebaskan pada akhir area mereka, sehingga tidak menyebabkan delete kehendak (1) angka miring (pelepasan tumpukan dimasukkan dalam angka pada distribusi tumpukan, oleh karena itu, adil untuk mengevaluasi pelepasan tumpukan) dan (2) menyebabkan kebocoran memori yang agak buruk jika kita tidak menyimpan tautan ke pointer baru dan tidak memanggil delete setelah kita memiliki dimensi waktu.

Di komputer saya, menggunakan g ++ 3.4.4 di Windows, saya mendapatkan "0 jam" untuk mengalokasikan tumpukan dan tumpukan untuk sesuatu yang kurang dari 100.000 distribusi, dan bahkan kemudian saya mendapatkan "0 jam waktu" untuk mendistribusikan tumpukan dan "15 jam" untuk distribusi tumpukan. Ketika saya mengukur 10.000.000 distribusi, alokasi tumpukan mengambil 31 kutu, dan distribusi tumpukan mengambil 1.562 kutu.


Ya, kompiler yang mengoptimalkan dapat mempercepat pembuatan objek kosong. Jika saya mengerti dengan benar, ia bahkan dapat melebihi seluruh siklus pertama. Ketika saya mengalami iterasi hingga 10.000.000 alokasi tumpukan, butuh 31 jam, dan distribusi tumpukan mengambil 1.562 jam. Saya dapat mengatakan dengan keyakinan bahwa, tanpa menentukan g ++ untuk mengoptimalkan file yang dapat dieksekusi, g ++ tidak mengecualikan konstruktor.


Selama bertahun-tahun sejak saya menulis ini, preferensi encoreci.net adalah untuk mempublikasikan kinerja dari build yang dioptimalkan. Secara umum, saya pikir ini benar. Namun, saya masih merasa konyol untuk meminta kompiler mengoptimalkan kode ketika Anda tidak ingin kode ini dioptimalkan. Sepertinya saya sangat mirip dengan membayar parkir tambahan, tetapi saya menolak untuk menyerahkan kunci. Dalam kasus khusus ini, saya tidak ingin pengoptimal bekerja.

Menggunakan versi standar yang sedikit dimodifikasi (untuk mengatasi titik yang valid di mana program sumber tidak mengalokasikan apa pun di tumpukan setiap kali melalui siklus) dan mengkompilasi tanpa optimasi, tetapi dengan menautkan ke rilis perpustakaan (untuk mengakses titik aktual yang tidak kita inginkan termasuk perlambatan yang disebabkan oleh pengikatan ke pustaka debug):

 #include <cstdio> #include <chrono> namespace { void on_stack() { int i; } void on_heap() { int* i = new int; delete i; } } int main() { auto begin = std::chrono::system_clock::now(); for (int i = 0; i < 1000000000; ++i) on_stack(); auto end = std::chrono::system_clock::now(); std::printf("on_stack took %f seconds\n", std::chrono::duration<double>(end - begin).count()); begin = std::chrono::system_clock::now(); for (int i = 0; i < 1000000000; ++i) on_heap(); end = std::chrono::system_clock::now(); std::printf("on_heap took %f seconds\n", std::chrono::duration<double>(end - begin).count()); return 0; } 

ditampilkan:

 on_stack took 2.070003 seconds on_heap took 57.980081 seconds 

pada sistem saya ketika mengkompilasi dengan baris perintah cl foo.cc /Od /MT /EHsc .

Anda mungkin tidak setuju dengan pendekatan saya untuk mendapatkan perakitan yang tidak dioptimalkan. Itu hebat: jangan ragu untuk memodifikasi tolok ukur sebanyak yang Anda inginkan. Ketika saya mengaktifkan pengoptimalan, saya mendapatkan:

 on_stack took 0.000000 seconds on_heap took 51.608723 seconds 

Bukan karena distribusi tumpukan hampir instan, tetapi karena setiap kompiler semi-tahan lama mungkin memperhatikan bahwa on_stack tidak berguna dan dapat dioptimalkan. GCC pada laptop Linux saya juga mencatat bahwa on_heap tidak berguna dan mengoptimalkannya:

 on_stack took 0.000003 seconds on_heap took 0.000002 seconds 
 on_stack took 0.000003 seconds on_heap took 0.000002 seconds 
104
02 окт. jawaban yang diberikan oleh Max Lybbert 02 Oktober. 2008-10-02 21:11 '08 pada jam 9:11 malam 2008-10-02 21:11

Suatu hal yang menarik yang saya pelajari tentang Stack vs. Heap Allocation pada prosesor Xbox 360 Xenon, yang juga dapat diterapkan pada sistem multi-core lainnya, adalah heaping yang menyebabkan bagian kritis untuk menghentikan semua core lainnya, jadi ini tidak bertentangan. Dengan demikian, dalam loop tertutup, Alokasi Tumpukan adalah cara untuk pergi untuk array ukuran tetap, karena ini mencegah warung.

Ini mungkin akselerasi lain yang perlu dipertimbangkan jika Anda mengkodekan multicore / multiproc, karena alokasi stack hanya akan tersedia untuk kernel menggunakan fungsi terbatas Anda dan itu tidak akan mempengaruhi core / prosesor lainnya.

26
02 марта '09 в 4:55 2009-03-02 04:55 jawabannya diberikan oleh Furious Coder pada 02 Maret 2009 di 04:55 2009-03-02 04:55

Anda dapat menulis dispenser tumpukan khusus untuk ukuran objek tertentu yang sangat efisien. Namun, distributor tumpukan umum tidak terlalu efisien.

Saya juga setuju dengan Torbjörn Gyllebring tentang kehidupan objek yang diharapkan. Poin bagus!

16
02 окт. Balas diberikan oleh Chris Jester-Young 02 Okt 2008-10-02 09:08 '08 pada jam 9:08 2008-10-02 09:08

Selain keunggulan kinerja dalam urutan besarnya dibandingkan dengan distribusi tumpukan, alokasi tumpukan lebih disukai untuk aplikasi server lama. Bahkan tumpukan terbaik yang dikelola akhirnya menjadi sangat terfragmentasi sehingga kinerja aplikasi menjadi lebih buruk.

6
26 окт. Balas oleh Jay 26 Okt 2009-10-26 20:36 '09 pada 20:36 2009-10-26 20:36

Biasanya, alokasi stack hanya terdiri dari pengurangan pointer stack dari register. Ini lebih dari sekadar pencarian tumpukan.

Terkadang untuk alokasi tumpukan diperlukan untuk menambahkan halaman (s) dari memori virtual. Menambahkan halaman baru ke memori zeroed tidak perlu membaca halaman dari disk, jadi biasanya akan lebih cepat beberapa ton daripada mencari heap (terutama jika bagian dari heap juga diturunkan). Dalam situasi yang jarang terjadi, dan Anda dapat membuat contoh seperti itu, cukup ruang yang ternyata tersedia di bagian tumpukan yang sudah ada dalam RAM, tetapi mengalokasikan halaman baru untuk tumpukan harus menunggu beberapa halaman lain ditulis ke disk. Dalam situasi >

5
02 окт. jawabannya diberikan oleh programmer Windows 02 Oktober. 2008-10-02 09:18 '08 pada 9:18 2008-10-02 09:18

Saya tidak berpikir alokasi tumpukan dan distribusi tumpukan biasanya dipertukarkan. Saya juga berharap kinerja keduanya cukup untuk penggunaan umum.

Saya akan sangat merekomendasikan untuk barang-barang kecil, tergantung mana yang lebih cocok untuk area distribusi. Untuk barang-barang besar, sekelompok mungkin diperlukan.

Pada sistem operasi 32-bit yang memiliki banyak utas, tumpukan sering kali sangat terbatas (walaupun biasanya setidaknya beberapa MB), karena ruang alamat harus dipotong, dan cepat atau lambat satu tumpukan benang akan dimulai pada yang lain, Pada sistem satu-utas Keterbatasan (Linux glibc single-threaded) jauh lebih kecil, karena tumpukan hanya dapat tumbuh dan tumbuh.

Pada sistem operasi 64-bit, ruang alamat cukup untuk membuat tumpukan benang cukup besar.

5
02 окт. jawabannya diberikan oleh MarkR 02 Oktober. 2008-10-02 09:12 '08 pada jam 9:12 2008-10-02 09:12

Ini bukan alokasi tumpukan yang lebih cepat. Anda juga akan mendapat banyak manfaat dari menggunakan variabel stack. Mereka memiliki lokalitas tautan terbaik. Dan akhirnya, rilisnya jauh lebih murah.

3
03 окт. balasan yang diberikan oleh MSalters pada 03 Oktober 2008-10-03 18:35 '08 pada 18:35 2008-10-03 18:35

Tumpukan memiliki kapasitas terbatas, tetapi tumpukan tidak. Tumpukan khas untuk proses atau utas sekitar 8K. Anda tidak dapat mengubah ukuran setelah memilihnya.

Variabel stack mengikuti aturan untuk coverage, tetapi heaps tidak. Jika penunjuk instruksi melampaui fungsi, semua variabel baru yang terkait dengan fungsi ini hi>

Yang paling penting, Anda tidak dapat memprediksi sebelumnya panggilan umum fungsi. Dengan demikian, mengalokasikan hanya 200 byte pada bagian Anda dapat menyebabkan stack overflow. Ini sangat penting jika Anda menulis perpustakaan, bukan aplikasi.

3
02 окт. jawabannya diberikan oleh yogman 02 Oktober. 2008-10-02 19:57 '08 pada 19:57 2008-10-02 19:57

Alokasi tumpukan adalah sepasang instruksi, sedangkan rtos distributor tumpukan tercepat (TLSF) yang saya tahu rata-rata menggunakan sekitar 150 instruksi. Selain itu, pemblokiran tidak diperlukan untuk alokasi tumpukan, karena mereka menggunakan penyimpanan thread lokal, yang merupakan keuntungan kinerja besar lainnya. Dengan demikian, distribusi tumpukan bisa 2-3 kali lipat lebih cepat, tergantung pada seberapa kuat lingkungan multithreaded.

Secara umum, alokasi tumpukan adalah pilihan terakhir Anda jika Anda peduli dengan kinerja. Opsi menengah yang layak dapat menjadi pengalokasi kumpulan tetap, yang juga merupakan instruksi pasangan dan memiliki sumber daya distribusi yang sangat sedikit, sehingga sangat bagus untuk objek kecil dengan ukuran tetap. Di sisi lain, ini hanya bekerja dengan objek dengan ukuran tetap, secara inheren tidak aman untuk benang dan memiliki masalah fragmentasi blok.

3
17 авг. Balas diberikan oleh Andrei Pokrovsky 17 Agu. 2010-08-17 23:22 '10 pada 11:22 PM 2010-08-17 23:22

Alokasi tumpukan hampir selalu secepat atau secepat distribusi heap, walaupun untuk distributor heap, tentu saja, dimungkinkan untuk menggunakan teknik alokasi berbasis stack.

Namun, ada masalah besar ketika bekerja dengan keseluruhan stack dan kinerja berbasis heap (atau dalam beberapa kondisi yang lebih baik, distribusi lokal dan eksternal). Biasanya, distribusi heap (eksternal) lambat, karena berurusan dengan berbagai jenis pola distribusi dan distribusi. Mengurangi volume pengalokasi yang Anda gunakan (yang membuatnya lokal untuk algoritma / kode) akan membantu meningkatkan kinerja tanpa perubahan besar. Menambahkan struktur yang lebih baik ke pola distribusi Anda, misalnya, memberlakukan LIFO pada pasangan distribusi dan rilis juga dapat meningkatkan kinerja distributor dengan menggunakan distributor dengan cara yang lebih sederhana dan lebih terstruktur. Atau Anda dapat menggunakan atau menulis dispenser yang disesuaikan untuk pola distribusi khusus Anda; Sebagian besar program sering mengalokasikan beberapa ukuran diskrit, jadi tumpukan berdasarkan buffer tampilan beberapa ukuran tetap (lebih disukai dikenal) akan bekerja dengan sangat baik. Untuk alasan ini, Windows menggunakan tumpukan rendah-destruktif.

Di sisi lain, alokasi berbasis tumpukan di pita memori 32-bit juga penuh bahaya jika Anda memiliki terlalu banyak utas. Tumpukan membutuhkan jangkauan memori terus menerus, sehingga semakin banyak utas yang Anda miliki, semakin banyak ruang alamat virtual yang harus Anda jalankan tanpa. Ini tidak akan menjadi masalah (untuk saat ini) dengan versi 64-bit, tetapi dapat menyebabkan kekacauan dalam program yang panjang dengan sejumlah besar utas. Meluncurkan ruang alamat virtual karena fragmentasi selalu menyebalkan.

3
10 авг. balasan diberikan MSN 10 Agustus 2010-08-10 19:27 '10 pada 19:27 2010-08-10 19:27

Mungkin masalah terbesar dengan alokasi tumpukan dibandingkan dengan alokasi tumpukan adalah bahwa distribusi tumpukan umumnya merupakan operasi tanpa batas, dan karena itu Anda tidak dapat menggunakannya di mana waktu merupakan masalah.

Untuk aplikasi lain di mana waktu tidak menjadi masalah, ini mungkin tidak begitu penting, tetapi jika Anda mengalokasikan banyak, itu akan mempengaruhi kecepatan eksekusi. Selalu mencoba menggunakan tumpukan untuk waktu yang singkat dan sering mengalokasikan memori (misalnya, dalam siklus) dan, sejauh mungkin, untuk mendistribusikan tumpukan selama peluncuran aplikasi.

3
02 окт. jawabannya diberikan larsivi 02 Oktober. 2008-10-02 11:34 '08 pada 11:34 2008-10-02 11:34

Saya pikir waktu hidup itu penting, dan apakah Anda perlu membangun hal yang kompleks. Misalnya, dalam pemodelan berbasis transaksi, Anda biasanya perlu mengisi dan mentransfer struktur transaksi dengan sekelompok bidang untuk fungsi pekerjaan. Lihatlah standar OSCI SystemC TLM-2.0 untuk contoh.

Mengalokasikannya dalam tumpukan yang dekat dengan panggilan operasi menghasilkan overhead yang sangat besar, karena konstruksinya mahal. Cara yang baik adalah dengan mengalokasikan tumpukan dan menggunakan kembali objek transaksi dengan menggabungkan atau kebijakan sederhana, misalnya, "modul ini hanya membutuhkan satu objek transaksi".

Ini jauh lebih cepat daripada memilih objek dengan setiap permintaan operasi.

Alasannya adalah bahwa objek tersebut memiliki konstruksi yang mahal dan masa manfaat yang agak lama.