Halo, Habr!
Dalam
artikel terakhir , saya menyebutkannya sendiri dan bertanya dalam komentar - ok, well, menggunakan metode poke ilmiah, kami memilih ukuran tumpukan, sepertinya tidak ada yang jatuh, tetapi bisakah kita dengan lebih baik mengevaluasi apa yang setara dan siapa yang makan begitu banyak?
Kami menjawab singkat: ya, tapi tidak.
Tidak, dengan menggunakan metode analisis statis tidak mungkin untuk secara akurat mengukur ukuran tumpukan yang diperlukan oleh program - tetapi, bagaimanapun, metode ini dapat berguna.
Jawabannya sedikit lebih lama - di bawah potongan.
Seperti diketahui secara luas oleh sekelompok kecil orang, tempat di stack dialokasikan, pada kenyataannya, untuk variabel lokal yang digunakan fungsi saat ini - dengan pengecualian variabel dengan pengubah statis, yang disimpan dalam memori yang dialokasikan secara statis, di area bss, karena mereka harus menyimpan artinya antara panggilan fungsi.
Ketika fungsi dieksekusi, kompiler menambahkan ruang pada stack untuk variabel yang dibutuhkannya, dan setelah selesai, ia membebaskan ruang ini kembali. Tampaknya semuanya sederhana, tetapi - dan ini sangat berani
tetapi - kami memiliki beberapa masalah:
- fungsi memanggil fungsi-fungsi lain yang juga membutuhkan stack
- terkadang fungsi memanggil fungsi lain bukan dengan referensi langsung mereka, tetapi dengan pointer ke suatu fungsi
- pada prinsipnya, dimungkinkan - meskipun harus dihindari dengan segala cara - fungsi rekursif memanggil ketika A memanggil B, B memanggil C, dan C di dalam dirinya sendiri memanggil A lagi
- kapan saja gangguan dapat terjadi, pawang yang fungsinya sama dengan yang ingin bagian tumpukannya sendiri
- jika Anda memiliki hierarki interupsi, interupsi lain dapat terjadi di dalam interupsi!
Jelas, panggilan fungsi rekursif harus dihapus dari daftar ini, karena kehadiran mereka adalah alasan untuk tidak mempertimbangkan ukuran tumpukan, tetapi untuk menyampaikan pendapat Anda kepada pembuat kode. Segala sesuatu yang lain, sayangnya, tidak dapat dicoret dalam kasus umum (meskipun secara khusus mungkin ada nuansa: misalnya, semua interupsi untuk Anda dapat memiliki prioritas yang sama dengan desain, misalnya, seperti pada RIOT OS, dan tidak akan ada interupsi bersarang).
Sekarang bayangkan sebuah lukisan cat minyak:
- fungsi A, memakan 100 byte pada stack, memanggil fungsi B, yang membutuhkan 50 byte
- pada saat eksekusi B, A sendiri, jelas, belum selesai, jadi 100 byte tidak dibebaskan, jadi kita sudah memiliki 150 byte pada stack
- fungsi B memanggil fungsi C, dan melakukan ini sesuai dengan sebuah penunjuk yang, tergantung pada logika program, dapat menunjuk ke setengah lusin fungsi yang berbeda yang mengkonsumsi dari 5 hingga 50 byte tumpukan
- pada saat runtime C, terjadi interupsi dengan handler berat berjalan relatif lama dan menghabiskan 20 byte stack
- selama pemrosesan interupsi, interupsi prioritas tinggi lainnya terjadi, pawang yang menginginkan 10 byte stack
Dalam desain yang indah ini, dengan kebetulan yang sukses dari semua keadaan, Anda akan memiliki
setidaknya lima fungsi aktif secara bersamaan - A, B, C dan dua penangan interupsi. Selain itu, salah satu dari mereka tidak memiliki konstanta konsumsi tumpukan, karena itu bisa saja merupakan fungsi yang berbeda dalam lintasan yang berbeda, dan untuk memahami kemungkinan atau ketidakmungkinan saling mengganggu, Anda setidaknya harus tahu apakah Anda memiliki interupsi dengan prioritas yang berbeda sama sekali , dan sebagai maksimum - untuk memahami apakah mereka dapat saling tumpang tindih.
Jelas, untuk penganalisa kode statis otomatis tugas ini sangat dekat dengan berlebihan, dan hanya dapat dilakukan dalam perkiraan kasar dari estimasi atas:
- jumlah tumpukan semua penangan interrupt
- jumlah tumpukan fungsi yang berjalan di cabang kode yang sama
- coba temukan semua pointer ke fungsi dan panggilannya, dan ambil ukuran stack maksimum di antara fungsi yang ditunjuk pointer ini sebagai ukuran stack
Dalam kebanyakan kasus, Anda mendapatkan, di satu sisi, perkiraan yang sangat tinggi, dan di sisi lain, kesempatan untuk melewati beberapa pemanggilan fungsi yang sangat rumit melalui pointer.
Oleh karena itu, dalam kasus umum, kita dapat mengatakan:
tugas ini tidak diselesaikan secara otomatis . Solusi manual - seseorang yang tahu logika program ini - membutuhkan penggalian beberapa angka.
Namun demikian, perkiraan statis ukuran tumpukan dapat sangat berguna dalam mengoptimalkan perangkat lunak - setidaknya untuk tujuan sederhana memahami siapa yang makan banyak, dan tidak terlalu banyak.
Ada dua alat yang sangat berguna untuk ini di GNU / gcc toolchain:
- flag -fstack-use
- utilitas cflow
Jika Anda menambahkan -fstack-use ke flag gcc (misalnya, ke Makefile sejalan dengan CFLAGS), maka untuk
setiap file yang dikompilasi% nama file% .c kompiler akan membuat file% nama file% .su, di dalamnya akan terdapat teks yang sederhana dan jelas.
Ambil, misalnya, target.su untuk
alas kaki raksasa ini :
target.c:159:13:save_settings 8 static target.c:172:13:disable_power 8 static target.c:291:13:adc_measure_vdda 32 static target.c:255:13:adc_measure_current 24 static target.c:76:6:cpu_setup 0 static target.c:81:6:clock_setup 8 static target.c:404:6:dma1_channel1_isr 24 static target.c:434:6:adc_comp_isr 40 static target.c:767:6:systick_activity 56 static target.c:1045:6:user_activity 104 static target.c:1215:6:gpio_setup 24 static target.c:1323:6:target_console_init 8 static target.c:1332:6:led_bit 8 static target.c:1362:6:led_num 8 static
Di sini kita melihat konsumsi sebenarnya dari stack untuk setiap fungsi yang muncul di dalamnya, yang darinya kita dapat menarik beberapa kesimpulan untuk diri kita sendiri - contohnya, bahwa ada baiknya mencoba mengoptimalkannya sejak awal, jika kita mengalami kekurangan RAM.
Pada saat yang sama, perhatian,
file ini sebenarnya tidak memberikan informasi yang akurat tentang konsumsi aktual dari stack untuk fungsi-fungsi dari mana fungsi-fungsi lain dipanggil !
Untuk memahami total konsumsi, kita perlu membangun pohon panggilan dan merangkum tumpukan semua fungsi yang termasuk dalam setiap cabangnya. Ini dapat dilakukan, misalnya, dengan utilitas
GNU cflow dengan mengaturnya pada satu atau lebih file.
Knalpot di sini kita mendapatkan urutan besarnya lebih berat, saya hanya akan memberikan sebagian untuk target yang sama. C:
olegart@oleg-npc /mnt/c/Users/oleg/Documents/Git/dap42 (umdk-emb) $ cflow src/stm32f042/umdk-emb/target.c adc_comp_isr() <void adc_comp_isr (void) at src/stm32f042/umdk-emb/target.c:434>: TIM_CR1() ADC_DR() ADC_ISR() DMA_CCR() GPIO_BSRR() GPIO_BRR() ADC_TR1() ADC_TR1_HT_VAL() ADC_TR1_LT_VAL() TIM_CNT() DMA_CNDTR() DIV_ROUND_CLOSEST() NVIC_ICPR() clock_setup() <void clock_setup (void) at src/stm32f042/umdk-emb/target.c:81>: rcc_clock_setup_in_hsi48_out_48mhz() crs_autotrim_usb_enable() rcc_set_usbclk_source() dma1_channel1_isr() <void dma1_channel1_isr (void) at src/stm32f042/umdk-emb/target.c:404>: DIV_ROUND_CLOSEST() gpio_setup() <void gpio_setup (void) at src/stm32f042/umdk-emb/target.c:1215>: rcc_periph_clock_enable() button_setup() <void button_setup (void) at src/stm32f042/umdk-emb/target.c:1208>: gpio_mode_setup() gpio_set_output_options() gpio_mode_setup() gpio_set() gpio_clear() rcc_peripheral_enable_clock() tim2_setup() <void tim2_setup (void) at src/stm32f042/umdk-emb/target.c:1194>: rcc_periph_clock_enable() rcc_periph_reset_pulse() timer_set_mode() timer_set_period() timer_set_prescaler() timer_set_clock_division() timer_set_master_mode() adc_setup_common() <void adc_setup_common (void) at src/stm32f042/umdk-emb/target.c:198>: rcc_periph_clock_enable() gpio_mode_setup() adc_set_clk_source() adc_calibrate() adc_set_operation_mode() adc_disable_discontinuous_mode() adc_enable_external_trigger_regular() ADC_CFGR1_EXTSEL_VAL() adc_set_right_aligned() adc_disable_temperature_sensor() adc_disable_dma() adc_set_resolution() adc_disable_eoc_interrupt() nvic_set_priority() nvic_enable_irq() dma_channel_reset() dma_set_priority() dma_set_memory_size() dma_set_peripheral_size() dma_enable_memory_increment_mode() dma_disable_peripheral_increment_mode() dma_enable_transfer_complete_interrupt() dma_enable_half_transfer_interrupt() dma_set_read_from_peripheral() dma_set_peripheral_address() dma_set_memory_address() dma_enable_circular_mode() ADC_CFGR1() memcpy() console_reconfigure() tic33m_init() strlen() tic33m_display_string()
Dan itu bahkan tidak setengah pohon.
Untuk memahami konsumsi sebenarnya dari stack, kita perlu mengambil konsumsi untuk
masing -
masing fungsi yang disebutkan di dalamnya dan menjumlahkan nilai-nilai ini untuk masing-masing cabang.
Dan sementara kita masih tidak memperhitungkan panggilan fungsi akun oleh pointer dan interupsi, termasuk. bersarang (dan secara khusus dalam kode ini, mereka dapat disarangkan).
Seperti yang Anda tebak, melakukan ini setiap kali Anda mengubah kode, untuk membuatnya lebih ringan, sulit - itulah sebabnya biasanya tidak ada yang melakukannya.
Namun demikian, perlu untuk memahami prinsip-prinsip pengisian tumpukan - ini dapat menyebabkan pembatasan tertentu pada kode proyek, meningkatkan keandalannya dari sudut pandang mencegah tumpukan meluap (misalnya, larangan interupsi bersarang atau pemanggilan fungsi oleh pointer), dan khususnya -penggunaan simpanan sangat bisa membantu dengan optimasi kode pada sistem dengan kekurangan RAM.