From ce99182022cb3864faa961d414ac923b30b20c77 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Fri, 10 Oct 2025 14:17:34 +0200 Subject: [PATCH] compiler: mark string parameters as readonly Strings are readonly, but the compiler doesn't always know this. Marking them as readonly in the frontend allows the compiler to optimize based on this knowledge. This provides some small code size benefits. I didn't measure running speed. --- compiler/calls.go | 8 ++++++++ compiler/symbol.go | 5 +++++ compiler/testdata/go1.20.ll | 2 +- compiler/testdata/go1.21.ll | 6 +++--- compiler/testdata/string.ll | 16 ++++++++-------- 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/compiler/calls.go b/compiler/calls.go index a44ac38a87..08952c4db6 100644 --- a/compiler/calls.go +++ b/compiler/calls.go @@ -32,6 +32,9 @@ const ( // Whether this is a full or partial Go parameter (int, slice, etc). // The extra context parameter is not a Go parameter. paramIsGoParam = 1 << iota + + // Whether this is a readonly parameter (for example, a string pointer). + paramIsReadonly ) // createRuntimeCallCommon creates a runtime call. Use createRuntimeCall or @@ -167,6 +170,7 @@ func (c *compilerContext) flattenAggregateType(t llvm.Type, name string, goType continue } suffix := strconv.Itoa(i) + isString := false if goType != nil { // Try to come up with a good suffix for this struct field, // depending on which Go type it's based on. @@ -183,12 +187,16 @@ func (c *compilerContext) flattenAggregateType(t llvm.Type, name string, goType suffix = []string{"r", "i"}[i] case types.String: suffix = []string{"data", "len"}[i] + isString = true } case *types.Signature: suffix = []string{"context", "funcptr"}[i] } } subInfos := c.flattenAggregateType(subfield, name+"."+suffix, extractSubfield(goType, i)) + if isString { + subInfos[0].flags |= paramIsReadonly + } paramInfos = append(paramInfos, subInfos...) } return paramInfos diff --git a/compiler/symbol.go b/compiler/symbol.go index 749ad8f1b5..47bc0f4844 100644 --- a/compiler/symbol.go +++ b/compiler/symbol.go @@ -142,6 +142,11 @@ func (c *compilerContext) getFunction(fn *ssa.Function) (llvm.Type, llvm.Value) nocapture := c.ctx.CreateEnumAttribute(llvm.AttributeKindID("nocapture"), 0) llvmFn.AddAttributeAtIndex(i+1, nocapture) } + if paramInfo.flags¶mIsReadonly != 0 && paramInfo.llvmType.TypeKind() == llvm.PointerTypeKind { + // Readonly pointer parameters (like strings) benefit from being marked as readonly. + readonly := c.ctx.CreateEnumAttribute(llvm.AttributeKindID("readonly"), 0) + llvmFn.AddAttributeAtIndex(i+1, readonly) + } } // Set a number of function or parameter attributes, depending on the diff --git a/compiler/testdata/go1.20.ll b/compiler/testdata/go1.20.ll index b1c5bd48f2..dff746667d 100644 --- a/compiler/testdata/go1.20.ll +++ b/compiler/testdata/go1.20.ll @@ -50,7 +50,7 @@ unsafe.String.throw: ; preds = %entry declare void @runtime.unsafeSlicePanic(ptr) #1 ; Function Attrs: nounwind -define hidden ptr @main.unsafeStringData(ptr %s.data, i32 %s.len, ptr %context) unnamed_addr #2 { +define hidden ptr @main.unsafeStringData(ptr readonly %s.data, i32 %s.len, ptr %context) unnamed_addr #2 { entry: %stackalloc = alloca i8, align 1 call void @runtime.trackPointer(ptr %s.data, ptr nonnull %stackalloc, ptr undef) #3 diff --git a/compiler/testdata/go1.21.ll b/compiler/testdata/go1.21.ll index 0e8182538c..6af9776bc3 100644 --- a/compiler/testdata/go1.21.ll +++ b/compiler/testdata/go1.21.ll @@ -77,7 +77,7 @@ entry: } ; Function Attrs: nounwind -define hidden %runtime._string @main.minString(ptr %a.data, i32 %a.len, ptr %b.data, i32 %b.len, ptr %context) unnamed_addr #2 { +define hidden %runtime._string @main.minString(ptr readonly %a.data, i32 %a.len, ptr readonly %b.data, i32 %b.len, ptr %context) unnamed_addr #2 { entry: %0 = insertvalue %runtime._string zeroinitializer, ptr %a.data, 0 %1 = insertvalue %runtime._string %0, i32 %a.len, 1 @@ -91,7 +91,7 @@ entry: ret %runtime._string %5 } -declare i1 @runtime.stringLess(ptr, i32, ptr, i32, ptr) #1 +declare i1 @runtime.stringLess(ptr readonly, i32, ptr readonly, i32, ptr) #1 ; Function Attrs: nounwind define hidden i32 @main.maxInt(i32 %a, i32 %b, ptr %context) unnamed_addr #2 { @@ -116,7 +116,7 @@ entry: } ; Function Attrs: nounwind -define hidden %runtime._string @main.maxString(ptr %a.data, i32 %a.len, ptr %b.data, i32 %b.len, ptr %context) unnamed_addr #2 { +define hidden %runtime._string @main.maxString(ptr readonly %a.data, i32 %a.len, ptr readonly %b.data, i32 %b.len, ptr %context) unnamed_addr #2 { entry: %0 = insertvalue %runtime._string zeroinitializer, ptr %a.data, 0 %1 = insertvalue %runtime._string %0, i32 %a.len, 1 diff --git a/compiler/testdata/string.ll b/compiler/testdata/string.ll index ce2bf35066..8c95323ccf 100644 --- a/compiler/testdata/string.ll +++ b/compiler/testdata/string.ll @@ -31,13 +31,13 @@ entry: } ; Function Attrs: nounwind -define hidden i32 @main.stringLen(ptr %s.data, i32 %s.len, ptr %context) unnamed_addr #2 { +define hidden i32 @main.stringLen(ptr readonly %s.data, i32 %s.len, ptr %context) unnamed_addr #2 { entry: ret i32 %s.len } ; Function Attrs: nounwind -define hidden i8 @main.stringIndex(ptr %s.data, i32 %s.len, i32 %index, ptr %context) unnamed_addr #2 { +define hidden i8 @main.stringIndex(ptr readonly %s.data, i32 %s.len, i32 %index, ptr %context) unnamed_addr #2 { entry: %.not = icmp ult i32 %index, %s.len br i1 %.not, label %lookup.next, label %lookup.throw @@ -55,16 +55,16 @@ lookup.throw: ; preds = %entry declare void @runtime.lookupPanic(ptr) #1 ; Function Attrs: nounwind -define hidden i1 @main.stringCompareEqual(ptr %s1.data, i32 %s1.len, ptr %s2.data, i32 %s2.len, ptr %context) unnamed_addr #2 { +define hidden i1 @main.stringCompareEqual(ptr readonly %s1.data, i32 %s1.len, ptr readonly %s2.data, i32 %s2.len, ptr %context) unnamed_addr #2 { entry: %0 = call i1 @runtime.stringEqual(ptr %s1.data, i32 %s1.len, ptr %s2.data, i32 %s2.len, ptr undef) #3 ret i1 %0 } -declare i1 @runtime.stringEqual(ptr, i32, ptr, i32, ptr) #1 +declare i1 @runtime.stringEqual(ptr readonly, i32, ptr readonly, i32, ptr) #1 ; Function Attrs: nounwind -define hidden i1 @main.stringCompareUnequal(ptr %s1.data, i32 %s1.len, ptr %s2.data, i32 %s2.len, ptr %context) unnamed_addr #2 { +define hidden i1 @main.stringCompareUnequal(ptr readonly %s1.data, i32 %s1.len, ptr readonly %s2.data, i32 %s2.len, ptr %context) unnamed_addr #2 { entry: %0 = call i1 @runtime.stringEqual(ptr %s1.data, i32 %s1.len, ptr %s2.data, i32 %s2.len, ptr undef) #3 %1 = xor i1 %0, true @@ -72,16 +72,16 @@ entry: } ; Function Attrs: nounwind -define hidden i1 @main.stringCompareLarger(ptr %s1.data, i32 %s1.len, ptr %s2.data, i32 %s2.len, ptr %context) unnamed_addr #2 { +define hidden i1 @main.stringCompareLarger(ptr readonly %s1.data, i32 %s1.len, ptr readonly %s2.data, i32 %s2.len, ptr %context) unnamed_addr #2 { entry: %0 = call i1 @runtime.stringLess(ptr %s2.data, i32 %s2.len, ptr %s1.data, i32 %s1.len, ptr undef) #3 ret i1 %0 } -declare i1 @runtime.stringLess(ptr, i32, ptr, i32, ptr) #1 +declare i1 @runtime.stringLess(ptr readonly, i32, ptr readonly, i32, ptr) #1 ; Function Attrs: nounwind -define hidden i8 @main.stringLookup(ptr %s.data, i32 %s.len, i8 %x, ptr %context) unnamed_addr #2 { +define hidden i8 @main.stringLookup(ptr readonly %s.data, i32 %s.len, i8 %x, ptr %context) unnamed_addr #2 { entry: %0 = zext i8 %x to i32 %.not = icmp ugt i32 %s.len, %0