SimplifyC++ Article
Can Compilers Produce Faster Code Than Assembly on x86-64
Can Compilers Produce Faster Code Than Assembly on x86-64?
It is often claimed that modern C, C++, and Rust compilers can produce code faster than manually written assembly. This statement is sometimes true, but it requires careful qualification.
Assembly is not something the processor executes instead of C++ or Rust. All these languages are eventually translated into x86-64 machine code. The real comparison is therefore between:
Machine code generated by a modern compiler.
Machine code designed manually by an assembly programmer.
Why Can the Compiler Win?
Modern x86-64 processors are extremely complex. They execute instructions out of order and use branch prediction, register renaming, multiple execution units, and several cache levels.
For this reason, having fewer assembly instructions does not necessarily mean having faster code. The instructions must also suit the processor’s internal microarchitecture.
A modern compiler can automatically:
Inline functions and eliminate call overhead.
Remove operations that do not affect the result.
Allocate values efficiently among x86-64 registers.
Reorder instructions to reduce stalls.
Unroll loops and vectorize them.
Use SSE, AVX, AVX2, or AVX-512.
Remove some bounds checks and repeated operations.
Specialize code for a processor using options such as
-march=native.Optimize the whole program using LTO and PGO.
An assembly programmer may write an excellent standalone function, while the compiler may discover that the best solution is to inline the function, calculate its result during compilation, or remove it entirely.
For this reason, compilers often outperform average or insufficiently analyzed assembly code, especially in large programs.
Why Is the Compiler Not Always Best?
A compiler only knows what it can infer or prove from the source code.
The programmer may know that:
The data is aligned to 32 or 64 bytes.
Two pointers never overlap.
The array size is fixed.
The number of elements is a multiple of the AVX vector width.
Input values remain within a specific range.
The program targets one particular processor.
If the compiler cannot prove these facts, it may generate additional checks or multiple execution paths. An assembly programmer who knows the full contract can instead write one direct and highly specialized path.
Compilers also rely on approximate cost models. They may make suboptimal decisions about:
Choosing a particular instruction.
Unrolling a loop too much or too little.
Using SIMD when it is not profitable.
Allocating registers.
Scheduling loads and calculations.
Estimating the effect of code size on the instruction cache.
The Special Nature of x86-64
There is no single assembly implementation that is ideal for every x86-64 processor.
The sequence that performs best on an AMD Zen processor may not be best on an Intel Core processor. Performance may even differ significantly between generations from the same manufacturer because of differences in:
Execution-port count and capabilities.
Instruction latency and throughput.
Instruction decoding width.
Micro-op cache size and behavior.
Branch prediction.
Cache sizes.
Load and store performance.
AVX2 and AVX-512 support.
The effect of wide vector instructions on processor frequency.
A single x86-64 instruction may appear shorter than several simpler instructions, but internally it may be decoded into several micro-operations and perform worse.
Performance therefore cannot be judged merely by looking at the assembly listing or counting instructions. It must be measured on the target processor.
When Can Handwritten Assembly Win?
Assembly can outperform the compiler when the code is:
Small and clearly defined.
Executed an enormous number of times.
Responsible for a large percentage of total runtime.
Written for a specific processor or microarchitecture.
Based on information unavailable to the compiler.
Carefully measured with profiling and benchmarking tools.
This is common in areas such as:
Cryptography.
Data compression.
Image and video processing.
Encoding and decoding.
FFT and matrix multiplication.
Multiprecision arithmetic.
Memory and string routines.
Operating systems and boot code.
Context switching and interrupt handling.
Access to special processor registers and instructions.
In these cases, an expert can design a specialized kernel that uses x86-64 registers and AVX2 or AVX-512 instructions efficiently, reduces dependency chains, and keeps execution units busy.
However, this advantage is not free. Assembly code is harder to write, test, maintain, and port. It may also require separate implementations for different processor families.
Are Intrinsics Better Than Assembly?
In many cases, intrinsics provide a better compromise.
They allow the programmer to use SIMD facilities such as AVX2 and AVX-512 directly from C or C++, while leaving the compiler responsible for:
Register allocation.
Integration with surrounding code.
Instruction scheduling.
ABI handling.
Elimination of unnecessary operations.
A practical optimization process should therefore usually follow this order:
Write clear code in C, C++, or Rust.
Enable appropriate compiler optimizations.
Inspect the generated machine code.
Improve the algorithm and data layout.
Use intrinsics when necessary.
Write assembly only after measurements prove that it provides a real benefit.
Algorithms and Memory Often Matter More Than Instructions
In many x86-64 programs, the processor is not waiting for arithmetic operations. It is waiting for data to arrive from memory.
In such cases, replacing five instructions with three may provide little benefit if the real bottleneck is:
Cache misses.
Poor data organization.
Repeated copying.
Dynamic allocation.
Interference between threads.
Weak memory locality.
A data structure unsuitable for SIMD processing.
Changing the data layout may make a program several times faster, while handwritten assembly may provide only a small improvement.
Is There an Absolutely Best Machine-Code Implementation?
There is no universally best implementation without first defining the objective.
The goal may be:
Minimum latency for one operation.
Maximum throughput.
Minimum code size.
Minimum power consumption.
Best performance on Intel.
Best performance on AMD.
Best performance for small or large inputs.
Predictable worst-case execution time.
An implementation may win according to one criterion and lose according to another.
It is therefore only meaningful to speak about the best code for a specific processor, specific input characteristics, specific runtime conditions, and a specific performance objective.
Conclusion
Modern compilers can often generate better x86-64 machine code than ordinary handwritten assembly because they analyze large portions of the program, manage registers, schedule instructions, use SIMD, and eliminate unnecessary work.
However, an assembly expert can still outperform the compiler when working on a small and critical kernel for a specific processor, using information unavailable to the compiler and validating every decision on real hardware.
The real skill is therefore not writing everything in assembly, nor blindly trusting the compiler. It is knowing:
When to let the compiler work, when to guide it, and when measurements prove that manual intervention in x86-64 machine code is worth the cost.