Inlining is a technique used to optimize the execution speed of a program. It involves replacing a function call with the actual code of the function at the call site. In other words, the compiler copies the entire body of the function into the location where the function is called, eliminating the overhead of the call itself.
Calling a function is not free. When a function is invoked, the program typically has to perform a set of operations: pushing parameters onto the stack, jumping to the function's code, and then returning to the original location once the function finishes executing. These operations incur a fixed amount of overhead, paid on every call regardless of what the function actually does. For small functions, or for functions called very frequently, that overhead can become a meaningful fraction of the total runtime.
By inlining a function, the compiler eliminates that overhead, because the code is inserted directly at the call site. This can improve performance by reducing the number of instructions executed and removing the need for stack operations and jumps. It also has a second effect that usually matters more than the call overhead itself: the optimizer now sees the caller and the callee as a single piece of code, which opens up further optimizations that were previously impossible across the call boundary.
Inlining is usually performed by the compiler based on certain criteria. In C and C++, functions marked with the inline keyword are considered for inlining, though compilers still make their own decisions based on heuristics such as the size of the function, the frequency of calls, and the wider optimization goals. The Go compiler generally does a good job of deciding automatically. It leverages escape analysis, function size, and other factors to make informed decisions, and in most cases relying on that automatic behaviour is sufficient.
It is important to note that inlining is not always beneficial. Inlining large or complex functions can increase the size of the generated code, leading to cache inefficiencies and reduced performance. It can also hinder modularity and maintainability, since changes to the function's logic have to propagate to every location where it was inlined.
Overall, inlining is a trade-off between reducing function call overhead and increasing code size. Its effectiveness depends on the specific context and the optimization goals of the program. The interesting question is therefore not whether it helps, but when the saving is large enough to notice.
When to use inlining
When deciding whether inlining is worth pursuing, several factors matter.
- Function size. Inlining is generally more beneficial for small functions. There is less code to inline, which means reduced overhead and potentially improved performance. Inlining larger functions can lead to code bloat and hurt cache efficiency, which may outweigh any benefit.
- Function complexity. Inlining simple functions provides performance benefits because they are easier for the compiler to optimize once inlined. Complex functions increase code size and hinder maintainability. Functions with complicated control flow, recursion, or extensive computation are usually poor candidates.
- Function call frequency. If a function is called frequently, inlining is more likely to pay off, since the per-call overhead adds up across many invocations. Functions that are rarely called will not provide a significant benefit no matter how cheap they are.
- Performance profiling. Before deciding to inline anything, profile your code and identify the real bottlenecks. Focus on the sections that consume a significant amount of execution time. In many cases inlining is not the most effective optimization available, and algorithmic improvements or better I/O handling will matter far more.
- Code modularity and maintainability. Inlining affects both. When a function is inlined, changes to its code require the compiler to regenerate every site where it appears, and if you are hand-inlining, you have to update each one yourself. Weigh the performance gain against the ability to maintain the code easily.
- Compiler and architecture considerations. Different compilers and target architectures have different inlining strategies and limits. Compiler flags may give you some control over the behaviour. Understanding the capabilities and limitations of your toolchain is part of making an informed decision.
In summary, inlining is worth it when small, simple functions are called frequently and profiling indicates a potential gain. The goal is to strike a balance between performance optimization and maintainability, and analysing the specific characteristics of your code against the factors above will tell you which side of that balance you are on.
How Go decides
In Go, inlining is handled entirely by the compiler as part of its optimization strategy. Unlike C++, Go has no inline keyword and does not require explicit marking of functions. Instead the compiler uses a cost budget: it walks the function body, assigns a cost to each node, and inlines the function if the total falls below a threshold. Small leaf functions qualify; large ones, and historically anything containing certain control flow constructs, do not.
Alongside this, the compiler employs a technique called escape analysis. Escape analysis helps the compiler identify the lifetime of objects and where they should be allocated. By analysing object lifetimes, it can determine whether an object can live on the stack rather than the heap, which improves performance by reducing allocation and garbage collection pressure. During escape analysis the compiler also evaluates function size, complexity, and frequency of use, all of which feed into the inlining decision.
It is worth remembering that the compiler makes these decisions autonomously based on its own optimization goals, and that the criteria vary across compiler versions. What is inlined today may not be inlined after the next release, and vice versa.
To observe what the compiler decided, rather than guessing:
go build -gcflags="-m" ./..../main.go:5:6: can inline AddInlined
./main.go:12:20: inlining call to AddInlinedThis is the part worth knowing, because it turns a guess into an observation. Passing -m -m gives the full reasoning, including why a particular function was rejected.
To force the issue in the other direction, the //go:noinline directive on a function excludes it from inlining entirely. That is a benchmarking and debugging tool, not something to ship. The -gcflags flag more generally lets you specify optimization-related options at compile time, including several that affect inlining behaviour.
The Go compiler optimizations wiki (opens in a new tab) documents the current rules in detail.
A benchmark where it matters
Let us examine a sample to demonstrate inlining in practice. Two identical functions, one explicitly excluded from inlining:
package main
//go:noinline
func AddNonInlined(a, b int) int {
return a + b
}
func AddInlined(a, b int) int {
return a + b
}The AddNonInlined function carries the //go:noinline directive, telling the compiler to leave it alone. AddInlined is identical in every other respect and is free to be inlined.
To evaluate the impact, we use the following benchmark:
package main
import "testing"
func BenchmarkAddNonInlined(b *testing.B) {
x := 10
y := 5
for i := 0; i < b.N; i++ {
_ = AddNonInlined(x, y)
}
}
func BenchmarkAddInlined(b *testing.B) {
x := 10
y := 5
for i := 0; i < b.N; i++ {
_ = AddInlined(x, y)
}
}Each benchmark performs a number of iterations calling its respective function. Running it gives:
➜ go test -bench=.
goos: darwin
goarch: amd64
pkg: testp
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkAddNonInlined-12 970689716 1.234 ns/op
BenchmarkAddInlined-12 1000000000 0.2503 ns/op
PASS
ok testp 1.721sThe inlined function is roughly five times faster — approximately 0.2503 nanoseconds per operation against 1.234 for the non-inlined version. The difference is attributable to the elimination of call overhead. The function body here is a single addition, about one cycle of work, so the call overhead is not a tax on the work; it is the work.
NOTE
Be suspicious of your own microbenchmarks at this scale. 0.25 ns/op is under one cycle on a 2.6 GHz machine, which is a strong hint that the optimiser eliminated part of the loop once it could see through the call. That is a real effect of inlining, but it means the number measures "code the compiler deleted" as much as "call overhead removed".
At sub-nanosecond resolution, use -gcflags="-m" to confirm what was inlined, and read the assembly if the answer matters.
Benchmark results also vary with hardware, compiler version, and optimization settings, so interpret them within the context of your own environment rather than treating these numbers as universal.
When inlining is not needed
Inlining can improve performance by eliminating call overhead, but there are situations where it is unnecessary or actively unhelpful.
- Large or complex functions. Inlining these increases code size and can hurt cache locality. It also makes generated code harder to reason about. In such cases the compiler will usually decline to inline anyway.
- Frequently changing functions. If a function is modified often, inlining hinders maintainability by propagating redundant code throughout the codebase, making updates harder to apply consistently.
- Code size considerations. Sometimes reducing binary size matters more than shaving call overhead. Inlining increases the size of the generated binary, which is undesirable in resource-constrained environments or when a small binary is itself a goal.
- Virtual function calls. In object-oriented languages, virtual calls enable polymorphism and dynamic dispatch. Inlining them prematurely can defeat the dynamic behaviour that polymorphism depends on, and in the general case the target is not known at compile time at all.
- Debugging and profiling. Inlining makes both harder. When functions are inlined, setting breakpoints becomes awkward and attributing execution time to individual functions becomes unreliable, because the function no longer exists as a distinct entity in the generated code.
In these cases it is better to let the compiler make its own decisions based on the characteristics of the code and its optimization goals. The balance to strike is between inlining for performance and preserving readability, maintainability, and debuggability.
A benchmark where it does not
Let us take the previous example and modify it so the functions do real work, demonstrating the impact of inlining a larger or more complex function:
package main
//go:noinline
func AddNonInlined(a, b int) int {
sum := a + b
for i := 0; i < 100000; i++ {
sum += i
}
return sum
}
func AddInlined(a, b int) int {
sum := a + b
for i := 0; i < 100000; i++ {
sum += i
}
return sum
}Both functions add two integers and then run a loop accumulating the numbers from 0 to 99,999. They are identical apart from the //go:noinline directive. The same benchmark code applies:
package main
import "testing"
func BenchmarkAddNonInlined(b *testing.B) {
x := 10
y := 5
for i := 0; i < b.N; i++ {
_ = AddNonInlined(x, y)
}
}
func BenchmarkAddInlined(b *testing.B) {
x := 10
y := 5
for i := 0; i < b.N; i++ {
_ = AddInlined(x, y)
}
}Running it:
➜ go test -bench=.
goos: darwin
goarch: amd64
pkg: testp
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkAddNonInlined-12 43389 26175 ns/op
BenchmarkAddInlined-12 48938 23937 ns/op
PASS
ok testp 3.297sThe gap has essentially vanished. Both functions have similar execution times, with the inlined version very slightly ahead — well inside the noise of two benchmark runs.
The arithmetic explains why. Call overhead is around one nanosecond. The body now costs about 26,000 nanoseconds. The overhead is roughly 0.004% of the runtime, which is not a measurable quantity in this context.
Nothing about inlining got worse. The fixed cost is exactly what it was; it is simply no longer a meaningful fraction of the total. This example demonstrates that inlining large or complex functions does not reliably improve performance, and that factors such as code size, cache efficiency, and the specific characteristics of the function all bear on the outcome.
This is the whole story of inlining in one comparison. It removes a constant, so it matters in inverse proportion to what the function costs.
What this means in practice
Inlining pays for small functions called often — getters, comparators, the tiny helper inside a hot loop. Those are also the functions Go's inliner is most likely to handle without being asked.
It does not pay for functions that do real work, and it costs something: more code means more instruction-cache pressure and a larger binary. Compilers weigh this, which is precisely why the cost budget exists.
The practical advice is narrow, because there is not much to do:
- Profile before caring. If inlining shows up as your bottleneck, you have already fixed the real problems.
- Keep hot-path helpers small if you want them inlined. That is the one lever you actually control — the budget is about function size, so a function that does one thing stays eligible.
- Check with
-gcflags="-m"rather than assuming. - Do not restructure code for it. Hand-inlining hurts readability and duplicates logic to buy a nanosecond you are unlikely to be able to measure.
The general shape
Inlining belongs to a family of optimizations that trade space for time by removing indirection. It is close to free when the thing being removed is comparable in size to the thing doing the removing, and pointless when it is not.
Recognising which case you are in is a matter of comparing two numbers: the fixed cost, and the cost of the work. The second benchmark above is not a demonstration that inlining fails. It is a demonstration of arithmetic.
Conclusion
Inlining optimizes execution speed by eliminating the overhead of function calls, replacing the call with the body of the function at the call site. It can deliver real performance improvements, particularly for small functions that are called frequently.
However, it is not free, and it is not universally beneficial. Function size, complexity, code modularity, and maintainability all bear on whether it is the right choice, and the decision is typically best left to the compiler, which has more information about the target architecture than you do. The job is to strike a balance between performance optimization and the other software engineering considerations that keep a codebase workable — and to measure, rather than assume, which side of that balance any particular function falls on.