This article explores the differences between the compiler optimization levels <span><span>-O0</span></span> and <span><span>-O1</span></span> under the ARMv8 architecture. While the core concepts are consistent with architectures like x86, there are ARM-specific behaviors in terms of register usage, instruction set features, and more.
Overview of ARMv8 Architecture
-
Rich Register Set: ARMv8 has 31 general-purpose registers
<span><span>X0-X30</span></span>(64-bit) or<span><span>W0-W30</span></span>(32-bit), providing a solid foundation for register optimization. -
Load/Store Architecture: Data operations must be performed in registers and cannot be done directly in memory. Therefore, memory access optimization is crucial.
-
Calling Conventions: For example,
<span><span>X0-X7</span></span>are used for passing parameters and return values, while<span><span>X19-X29</span></span>are callee-saved registers, etc. Optimization will affect the usage strategy of these registers.
Core Differences Comparison (ARMv8 Perspective)
| Feature | <span><span>-O0</span></span> (Debug Mode) |
<span><span>-O1</span></span> (Optimization Mode) |
Specific Performance in ARMv8 |
|---|---|---|---|
| Register Usage | Conservative, frequent memory exchanges | Aggressive, maximizing register utilization | <span><span>-O0</span></span> frequently uses <span><span>str</span></span>/<span><span>ldr</span></span>; <span><span>-O1</span></span> allows variables to reside longer in <span><span>X0-X15</span></span> and other temporary registers. |
| Stack Frame Management | Complete, each function has a clear stack frame | Simplified or Omitted | <span><span>-O0</span></span> strictly executes <span><span>stp x29, x30, [sp, #-48]!</span></span> and other instructions to set up the stack frame; <span><span>-O1</span></span> may omit the stack frame for leaf functions. |
| Instruction Selection | Direct, Redundant | Efficient, Streamlined | <span><span>-O0</span></span> may use multiple simple instructions; <span><span>-O1</span></span> will use more powerful composite instructions (e.g., <span><span>stp</span></span>/<span><span>ldp</span></span> to access two registers simultaneously). |
| Function Calls | Strictly follows calling conventions | Tail call optimization, limited inlining | <span><span>-O0</span></span> uses <span><span>bl</span></span> for all calls; <span><span>-O1</span></span> may optimize tail calls to <span><span>b</span></span> instructions or inline small functions. |
| Debug Support | Perfect | Fundamentally Unavailable | <span><span>-O0</span></span> ensures every C/C++ variable has a memory location; <span><span>-O1</span></span> variables are only in registers, with disordered sequences. |
Technical Analysis and ARM Assembly Example
Let’s compare with a simple C code example.
C Code (test.c):
int calculate(int a, int b) {
int c = a + b;
int d = c * 2;
return d - 1;
}
Compiled with <span><span>-O0</span></span> (GCC)
Using the command <span><span>aarch64-linux-gnu-gcc -S -O0 test.c</span></span> to generate assembly.
Characteristics of the Generated Assembly Code:
calculate:
// 1. Complete stack frame setup
stp x29, x30, [sp, -32]! // Push frame pointer (x29) and return address (x30) onto the stack, allocating 32 bytes of stack space
mov x29, sp // Set new frame pointer
// 2. Save all parameters from registers to stack memory (for debugger inspection)
str w0, [x29, 28] // Store parameter 'a' (w0) to stack [sp+28]
str w1, [x29, 24] // Store parameter 'b' (w1) to stack [sp+24]
// 3. Perform calculations: frequently exchanging data between registers and memory
ldr w0, [x29, 28] // Load 'a' from stack to w0
ldr w1, [x29, 24] // Load 'b' from stack to w1
add w0, w0, w1 // w0 = a + b
str w0, [x29, 20] // Store result 'c' to stack [sp+20]
// 4. Similarly redundant loading to calculate d
ldr w0, [x29, 20] // Load 'c' from stack to w0
lsl w0, w0, 1 // w0 = c * 2 (logical left shift by 1)
str w0, [x29, 16] // Store result 'd' to stack [sp+16]
// 5. Prepare return value
ldr w0, [x29, 16] // Load 'd' from stack to w0
sub w0, w0, 1 // w0 = d - 1
// 6. Complete stack frame recovery
ldp x29, x30, [sp], 32 // Restore frame pointer and return address, reclaim stack space
ret // Return
<span><span>-O0</span></span> Summary:
-
Memory Operation Intensive: Even the simplest calculations involve a lot of
<span><span>str</span></span>/<span><span>ldr</span></span>instructions. -
Complete Stack Frame: Facilitates stack backtracking at any point in the function.
-
Logical Clarity: The sequence of assembly instructions corresponds closely to the lines of C code.
Compiled with <span><span>-O1</span></span>
Using the command <span><span>aarch64-linux-gnu-gcc -S -O1 test.c</span></span> to generate assembly.
Characteristics of the Generated Assembly Code:
calculate:
// 1. No stack frame operations! Because this is a simple leaf function, no extra stack space is needed, nor does it call other functions.
// 2. All calculations are done in registers in one go
add w0, w0, w1 // w0 = a + b (parameters are already in w0, w1)
add w0, w0, w0 // w0 = c * 2 (using addition w0 + w0 instead of shifting)
sub w0, w0, 1 // w0 = d - 1
// 3. Directly return. The return value is already in w0.
ret
<span><span>-O1</span></span> Summary:
-
Registers Rule: The entire function has no memory accesses. Parameters
<span><span>a</span></span>and<span><span>b</span></span>remain in<span><span>w0</span></span>and<span><span>w1</span></span>throughout, with all intermediate results also circulating in<span><span>w0</span></span>. -
Stack Frame Omitted: As this is a leaf function and does not need to save state, stack frame operations are directly omitted, saving instructions and execution time.
-
Instruction Fusion and Simplification: The compiler recognizes that
<span><span>c * 2</span></span>can be efficiently completed using<span><span>add w0, w0, w0</span></span>. -
Code Extremely Streamlined: From 12 instructions (
<span><span>-O0</span></span>) optimized to only 3 instructions (<span><span>-O1</span></span>), resulting in significant improvements in performance and code size.
Other Common Optimizations in ARMv8 <span><span>-O1</span></span>
-
Dead Code Elimination: Similar to other architectures, code that will never be executed is removed.
-
Constant Propagation and Folding:
// C code int x = 5; int y = x * 10;<span><span>-O1</span></span>will directly compute to<span><span>int y = 50;</span></span>, and in assembly, it may be implemented with a single<span><span>mov w0, 50</span></span>instruction. -
Tail Call Optimization:
int tail_function(int n) { if (n == 0) return 1; return tail_function(n - 1); // Tail call }<span><span>-O1</span></span>may optimize this into a loop, avoiding the stack overhead of recursive calls. In assembly, the recursive call<span><span>bl tail_function</span></span>will be replaced with a branch instruction after adjusting parameters<span><span>b tail_function</span></span>. -
Limited Function Inlining: Very small functions (like
<span><span>getter</span></span>) will be inlined at the call site, eliminating the overhead of<span><span>bl</span></span>instructions.
Conclusion
In the ARMv8 architecture, the transition from <span><span>-O0</span></span> to <span><span>-O1</span></span> is a Qualitative Change:
-
<span><span>-O0</span></span>produces code for programmers and debuggers to view, ensuring absolute logical transparency at the cost of poor performance. Its ARM assembly is filled with structured but redundant stack operations and memory accesses. -
<span><span>-O1</span></span>produces code for the CPU to execute, fully utilizing the rich register resources and streamlined instruction set of ARMv8. By eliminating redundant memory accesses, simplifying stack frames, and optimizing instruction selection, it greatly enhances execution efficiency while maintaining compilation speed.
Therefore, in ARM embedded or server development, the development phase must use <span><span>-O0</span></span> to ensure debuggability, while at release, at least <span><span>-O1</span></span> must be used to achieve basic performance guarantees. For performance-sensitive applications, <span><span>-O2</span></span> or <span><span>-Os</span></span> (size optimization) is often adopted.
Finally, I recommend an online C language to assembly code conversion website: https://godbolt.org/. You can compare any C code you think of online and see the differences between C and assembly.
Here is a comparison of the differences between O0 and O1:
O0 C Language and Assembly Comparison
O1 C Language and Assembly Comparison