Skip to content

compiler: always inline runtime/volatile functions - #6064

Draft
davecheney wants to merge 1 commit into
tinygo-org:devfrom
davecheney:volatile-alwaysinline
Draft

davecheney wants to merge 1 commit into
tinygo-org:devfrom
davecheney:volatile-alwaysinline

Conversation

@davecheney

@davecheney davecheney commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

At -opt=z every function has minsize, and LLVM ignores inlinehint in a minsize caller. A register write through a pointer, such as a Register32 held in a driver struct, then becomes a call to Set. The inlined store is also smaller than the call.

Measured with the ili9325 Lorenz example from tinygo-org/drivers#926 on an Arduino M0 (SAMD21, 48 MHz). The flash sizes include timing code. The speeds are the mean of three readings.

opt flash before flash after FillScreen before FillScreen after Lorenz steps/s before Lorenz steps/s after
z 24104 24024 137.9 ms 34.2 ms 3125 3667
s 24196 24148 29.2 ms 29.2 ms 3972 3943
1 33620 33620 29.3 ms 29.3 ms 4117 4134
2 33092 33092 29.2 ms 29.5 ms 4156 4116

The z before values are from the same dev commit. The s, 1 and 2 before speeds are from an earlier dev commit.

At -opt=z, FillScreen is still 5 ms slower than at -opt=s, with no calls to runtime/volatile left. That difference comes from minsize in other code and is not part of this change.

Fixes #6063

@davecheney davecheney added this to the 0.45 milestone Oct 28, 2026
@davecheney
davecheney force-pushed the volatile-alwaysinline branch 2 times, most recently from db46182 to b9a4cfa Compare October 28, 2026 04:29
At -opt=z every function has minsize, and LLVM ignores inlinehint in a minsize caller. A register write through a pointer, such as a Register32 held in a driver struct, then becomes a call to Set. The inlined store is also smaller than the call.

Measured with the ili9325 Lorenz example from tinygo-org/drivers#926 on an Arduino M0 (SAMD21, 48 MHz). The flash sizes include timing code. The speeds are the mean of three readings.

| opt | flash before | flash after | FillScreen before | FillScreen after | Lorenz steps/s before | Lorenz steps/s after |
|---|---|---|---|---|---|---|
| z | 24104 | 24024 | 137.9 ms | 34.2 ms | 3125 | 3667 |
| s | 24196 | 24148 | 29.2 ms | 29.2 ms | 3972 | 3943 |
| 1 | 33620 | 33620 | 29.3 ms | 29.3 ms | 4117 | 4134 |
| 2 | 33092 | 33092 | 29.2 ms | 29.5 ms | 4156 | 4116 |

The z before values are from the same dev commit. The s, 1 and 2 before speeds are from an earlier dev commit.

At -opt=z, FillScreen is still 5 ms slower than at -opt=s, with no calls to runtime/volatile left. That difference comes from minsize in other code and is not part of this change.

Fixes tinygo-org#6063
@davecheney
davecheney force-pushed the volatile-alwaysinline branch from b9a4cfa to 6f797f4 Compare October 28, 2026 05:16

@dgryski dgryski left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.. Question for a follow-up PR (just while we're here looking at runtime and inlining...)

Comment thread compiler/compiler.go
switch b.info.inline {
case inlineHint:
// Add LLVM inline hint to functions with //go:inline pragma.
inline := b.ctx.CreateEnumAttribute(llvm.AttributeKindID("inlinehint"), 0)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should always inline runtime functions that have been tagged with go:inline

diff --git a/compiler/compiler.go b/compiler/compiler.go
index 421a9b76..9504a7b4 100644
--- a/compiler/compiler.go
+++ b/compiler/compiler.go
@@ -1245,7 +1245,11 @@ func (b *builder) createFunctionStart(intrinsic bool) {
 	switch b.info.inline {
 	case inlineHint:
 		// Add LLVM inline hint to functions with //go:inline pragma.
-		inline := b.ctx.CreateEnumAttribute(llvm.AttributeKindID("inlinehint"), 0)
+		kind := "inlinehint"
+		if b.fn.Pkg != nil && b.fn.Pkg.Pkg.Path() == "runtime" {
+			kind = "alwaysinline"
+		}
+		inline := b.ctx.CreateEnumAttribute(llvm.AttributeKindID(kind), 0)
 		b.llvmFn.AddFunctionAttr(inline)
 	case inlineNone:
 		// Add LLVM attribute to always avoid inlining this function.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmarked map operations with this patch on native, wasip1, arduino r4, and a pico 2. Consistently faster for on -opt=z but slower for -opt=2. So maybe if we could guard this on optimization level?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'll look into that. In my testing opt=2 is never smaller or faster than opt=1, I know we use opt=2 for size tests, and I'm kind of wondering if that is the right choice, kinda of the way that -O2 was the default choice in gcc back in the day not -O3. See also #6083 as a proposal.

Once I'm ready to move this out of draft I can make this opt level specific as it only really applies to opt=z and the magic minsize attribute. But I also want to think a little bit more about encoding this magic into the compiler vs another of my new found passions, pragams

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runtime/volatile: Register32.Set is not inlined at -opt=z

2 participants