Hallo Habr!
Im
letzten Artikel habe ich es selbst erwähnt und in den Kommentaren gefragt - ok, nun, mit der wissenschaftlichen Poke-Methode haben wir die Größe des Stapels ausgewählt, es scheint, dass nichts fällt, aber können wir irgendwie zuverlässiger bewerten, was es ist und wer so viel gegessen hat?
Wir antworten kurz: Ja, aber nein.
Nein, mit den Methoden der statischen Analyse ist es unmöglich, die Größe des vom Programm benötigten Stapels genau zu messen - dennoch können diese Methoden nützlich sein.
Die Antwort ist etwas länger - unter dem Schnitt.
Wie einem engen Personenkreis allgemein bekannt ist, wird der Platz auf dem Stapel tatsächlich lokalen Variablen zugewiesen, die von der aktuell ausgeführten Funktion verwendet werden - mit Ausnahme von Variablen mit dem statischen Modifikator, die im statisch zugewiesenen Speicher im bss-Bereich gespeichert sind, weil sie gespeichert werden müssen ihre Bedeutung zwischen Funktionsaufrufen.
Wenn die Funktion ausgeführt wird, fügt der Compiler Speicherplatz für die benötigten Variablen auf dem Stapel hinzu und gibt diesen Speicherplatz nach Abschluss wieder frei. Es scheint, dass alles einfach ist, aber - und das ist sehr mutig,
aber - wir haben mehrere Probleme:
- Funktionen rufen andere Funktionen auf, die ebenfalls einen Stapel benötigen
- Manchmal rufen Funktionen andere Funktionen nicht durch ihre direkte Referenz auf, sondern durch einen Zeiger auf eine Funktion
- Im Prinzip ist ein rekursiver Funktionsaufruf möglich - obwohl dies auf jeden Fall vermieden werden sollte -, wenn A B aufruft, B C aufruft und C in sich selbst wieder A aufruft
- Es kann jederzeit eine Unterbrechung auftreten, deren Handler dieselbe Funktion ist, die ein eigenes Stück des Stapels haben möchte
- Wenn Sie eine Hierarchie von Interrupts haben, kann ein weiterer Interrupt innerhalb des Interrupts auftreten!
Rekursive Funktionsaufrufe sollten eindeutig aus dieser Liste gelöscht werden, da ihre Anwesenheit eine Ausrede ist, die Stapelgröße nicht zu berücksichtigen, sondern dem Autor des Codes Ihre Meinung mitzuteilen. Alles andere kann im allgemeinen Fall leider nicht durchgestrichen werden (obwohl es insbesondere Nuancen geben kann: Beispielsweise können alle Interrupts für Sie die gleiche Priorität haben, z. B. wie bei RIOT OS, und es gibt keine verschachtelten Interrupts).
Stellen Sie sich nun ein Ölgemälde vor:
- Funktion A, die 100 Bytes auf dem Stapel frisst, ruft Funktion B auf, die 50 Bytes benötigt
- Zum Zeitpunkt der Ausführung von B ist A selbst offensichtlich noch nicht fertig, sodass seine 100 Bytes nicht freigegeben werden. Wir haben also bereits 150 Bytes auf dem Stapel
- Funktion B ruft Funktion C auf, und zwar über einen Zeiger, der je nach Programmlogik auf ein halbes Dutzend verschiedener Funktionen verweisen kann, die 5 bis 50 Byte Stapel verbrauchen
- Zur Laufzeit C tritt ein Interrupt auf, wenn ein schwerer Handler relativ lange läuft und 20 Byte Stapel verbraucht
- Während der Interrupt-Verarbeitung tritt ein weiterer Interrupt mit höherer Priorität auf, dessen Handler 10 Bytes Stapel benötigt
In diesem schönen Design haben Sie mit einem besonders erfolgreichen Zusammentreffen aller Umstände
mindestens fünf gleichzeitig aktive Funktionen - A, B, C und zwei Interrupt-Handler. Darüber hinaus hat einer von ihnen keine Stapelverbrauchskonstante, da es sich in verschiedenen Durchgängen nur um eine andere Funktion handeln kann. Um die Möglichkeit oder Unmöglichkeit einer gegenseitigen Unterbrechung zu verstehen, müssen Sie zumindest wissen, ob Sie überhaupt Interrupts mit unterschiedlichen Prioritäten haben und maximal - um zu verstehen, ob sie sich überlappen können.
Offensichtlich ist diese Aufgabe für jeden automatischen statischen Code-Analysator nahezu überwältigend und kann nur in grober Näherung der oberen Schätzung ausgeführt werden:
- Summieren Sie die Stapel aller Interrupt-Handler
- Fassen Sie Stapel von Funktionen zusammen, die in demselben Codezweig ausgeführt werden
- Versuchen Sie, alle Zeiger auf Funktionen und deren Aufrufe zu finden, und nehmen Sie die maximale Stapelgröße unter den Funktionen, auf die diese Zeiger zeigen, als Stapelgröße
In den meisten Fällen erhalten Sie einerseits eine sehr hohe Schätzung und andererseits die Möglichkeit, einen besonders kniffligen Funktionsaufruf über Zeiger zu überspringen.
Daher können wir im allgemeinen Fall einfach sagen:
Diese Aufgabe wird nicht automatisch gelöst . Eine manuelle Lösung - eine Person, die die Logik dieses Programms kennt - erfordert das Graben einiger Zahlen.
Trotzdem kann eine statische Schätzung der Größe des Stapels bei der Optimierung von Software sehr nützlich sein - zumindest, um zu verstehen, wer wie viel isst und nicht zu viel.
Dafür gibt es in der GNU / gcc-Toolchain zwei äußerst nützliche Tools:
- flag -fstack-usage
- cflow-Dienstprogramm
Wenn Sie -fstack-usage zu den gcc-Flags hinzufügen (z. B. zum Makefile in der Zeile mit CFLAGS), erstellt der Compiler für
jede kompilierte Datei% filename% .c die Datei% filename% .su, in der sich einfacher und klarer Text befindet.
Nehmen Sie zum Beispiel target.su für
dieses gigantische Fußtuch :
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
Hier sehen wir den tatsächlichen Verbrauch des Stapels für jede darin erscheinende Funktion, aus dem wir einige Schlussfolgerungen für uns selbst ziehen können - zum Beispiel, dass es sich lohnt, zunächst zu versuchen, zu optimieren, wenn wir auf einen RAM-Mangel stoßen.
Gleichzeitig
liefert diese Datei keine genauen Informationen über den tatsächlichen Verbrauch des Stapels für Funktionen, von denen andere Funktionen aufgerufen werden !
Um den Gesamtverbrauch zu verstehen, müssen wir einen Aufrufbaum erstellen und die Stapel aller Funktionen zusammenfassen, die in jedem seiner Zweige enthalten sind. Dies kann beispielsweise mit dem Dienstprogramm
GNU cflow erfolgen, indem es für eine oder mehrere Dateien festgelegt wird.
Der Auspuff hier wird um eine Größenordnung schwerer, ich werde nur einen Teil davon für das gleiche Ziel geben.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()
Und das ist noch nicht einmal die Hälfte des Baumes.
Um den tatsächlichen Verbrauch des Stapels zu verstehen, müssen wir den Verbrauch für
jede der darin genannten Funktionen nehmen und diese Werte für jeden der Zweige summieren.
Und während wir Funktionsaufrufe durch Zeiger und Interrupts immer noch nicht berücksichtigen, inkl. verschachtelt (und speziell in diesem Code können sie verschachtelt sein).
Wie Sie vielleicht erraten haben, ist es, gelinde gesagt, schwierig, dies jedes Mal zu tun, wenn Sie den Code ändern - deshalb tut dies normalerweise niemand.
Trotzdem ist es notwendig, die Prinzipien der Stapelfüllung zu verstehen - dies kann zu bestimmten Einschränkungen des Projektcodes führen und dessen Zuverlässigkeit unter dem Gesichtspunkt der Verhinderung eines Stapelüberlaufs erhöhen (z. B. Verbot verschachtelter Interrupts oder Funktionsaufrufe durch Zeiger), und insbesondere die Verwendung von Stapeln kann erheblich sein Hilfe bei der Codeoptimierung auf Systemen mit fehlendem RAM.