Understanding and optimizing stack usage is an important part of developing efficient software for Arm-based systems. The stack is a region of memory used to store temporary variables created by functions and invoked during program execution. Keeping stack usage low improves performance and helps prevent stack overflows that can cause crashes or undefined behavior.
Arm’s toolchain provides powerful options to analyze and report on stack usage at both compile-time and runtime. These tools give developers visibility into stack requirements and can help identify areas to optimize stack allocation and usage.
Compile-Time Stack Usage Analysis
The Arm compiler can estimate static stack usage for each function in your program and generate reports detailing the stack requirements. This compile-time analysis helps provide an estimate of the stack footprint for optimization.
--fstack-usage option to the Arm compiler command will output a stack usage report after compilation. For each function, it shows the estimated static stack usage in bytes. You can quickly identify functions allocating large amounts of stack space and focus optimization efforts there.
--analyze=stack option provides a more detailed stack usage report in HTML format. This report includes per-function stack usage as well as call graphs showing the cumulative stack usage along different call paths. Identifying the call sequences with the highest stack requirements can help guide optimization strategies.
Runtime Stack Analysis
While compile-time analysis provides estimates, measuring actual stack usage during execution often gives the most accurate picture. Arm’s toolchain contains features to track and report runtime stack usage.
Stack Unwinding Libraries
The Arm unwinder libraries can track stack usage during program execution. By integrating these libraries and examining the unwinding information, you can determine peak stack requirements in different functions or code blocks. This helps identify optimization opportunities at a granular level.
Stack Depth Probing
Probing the current stack depth at strategic points in your program is another method to measure runtime usage. Simple macros like
GET_CURRENT_STACK make this probing easy to add. Tracing stack depth over time can reveal usage patterns and spikes.
Stack Limit Checking
To detect stack overflows, you can periodically check the stack pointer against a defined stack limit. This can uncover situations where usage exceeds expected limits. Upon hitting the limit, you can trigger a handler to record details or gracefully abort execution.
Stack Usage Optimization Techniques
Once you’ve analyzed stack usage, there are optimization techniques to reduce your stack footprint:
- Minimize variables allocated on the stack in large functions by switching to dynamic memory or optimizing algorithms.
- Reduce recursion depth to lower required stack space.
- Split functions into smaller sub-functions that can reuse stack space.
- Use link-time optimization to eliminate dead stack variables.
- Tune stack sizes with linker configuration options.
Variable Size Reduction
Reducing the size of stack variables provides one of the simplest ways to cut stack usage. Techniques include:
- Using smaller data types – for example, short ints instead of long ints.
- Declaring variables with only the required scope.
- Eliminating unused variables.
For large data structures or buffers, using dynamic memory allocation from the heap instead of stack allocation can significantly reduce stack footprint:
- Use malloc()/free() instead of declaring large automatic arrays.
- Allocate linked list nodes, tree nodes, etc. dynamically instead of declaring on stack.
- Move large buffers used temporarily during execution to the heap.
Recursion can result in large stack allocations as each call adds stack frame. Optimization strategies include:
- Converting recursive algorithms to iterative.
- Tail call elimination.
- Splitting into smaller recursive sub-functions.
- Rewriting with loop unrolling to reduce depth.
Stack Frame Reuse
Analysis may show multiple smaller functions called in sequence that could reuse the same section of stack. By carefully ordering functions and sharing stack frames, overall usage can reduced.
Tuning linker options controlling stack sizes can also yield savings:
- Reducing default stack size for threads.
- Configuring exact stack requirements for each thread.
- Adjusting stack area placement in memory.
Optimizing stack usage is a key aspect of creating efficient Arm applications. Arm’s toolchain provides powerful options to analyze and report on stack usage at compile-time and runtime. Using stack analysis to guide optimization techniques such as reducing variables, adding dynamic allocation, optimizing recursion, reusing stack frames, and configuring the linker can significantly reduce your stack footprint and overhead.