Skip to content

Avoid unnecessary defensive copy for constrained calls on readonly struct receivers#82432

Open
stevenelliottjr wants to merge 1 commit intodotnet:mainfrom
stevenelliottjr:fix/readonly-struct-defensive-copy
Open

Avoid unnecessary defensive copy for constrained calls on readonly struct receivers#82432
stevenelliottjr wants to merge 1 commit intodotnet:mainfrom
stevenelliottjr:fix/readonly-struct-defensive-copy

Conversation

@stevenelliottjr
Copy link

Summary

  • When calling base class virtual methods (e.g., ToString(), GetHashCode(), Equals()) on readonly struct receivers via constrained. callvirt, the compiler previously used AddressKind.Writeable for the receiver address
  • This caused unnecessary defensive copies when the receiver was in a readonly context (e.g., a field accessed through a readonly method's this, or a direct this reference in a readonly struct method)
  • Since readonly structs guarantee non-mutation for all their methods, and constrained calls either resolve to a non-mutating method or box the value (which copies it), the original receiver is never mutated
  • Changed to use AddressKind.ReadOnly for readonly value type receivers, eliminating the defensive copy

Example (from #76288):

struct X
{
    private Y a;
    public readonly string W() => a.ToString();
}
readonly struct Y { }

Before (21 bytes, 1 local):

ldarg.0
ldfld      "Y X.a"    // load value
stloc.0                // store to temp
ldloca.s   V_0         // address of temp
constrained. "Y"
callvirt   "string object.ToString()"
ret

After (18 bytes, 0 locals):

ldarg.0
ldflda     "Y X.a"    // direct field address
constrained. "Y"
callvirt   "string object.ToString()"
ret

Fixes #76288
Also resolves the optimization noted in #66365

Test plan

…ruct receivers

When calling a base class virtual method (like ToString, GetHashCode, Equals)
on a readonly struct receiver via constrained callvirt, the compiler previously
used AddressKind.Writeable for the receiver address. This caused unnecessary
defensive copies when the receiver was accessed through a readonly context
(e.g., a readonly method's 'this' or a readonly field).

Since readonly structs guarantee non-mutation for all their methods, and the
constrained call either resolves to a non-mutating method or boxes the value
(which copies it anyway), the original receiver is never mutated. This change
uses AddressKind.ReadOnly for readonly value type receivers, eliminating the
defensive copy.

Fixes dotnet#76288
Also resolves the optimization noted in dotnet#66365
@stevenelliottjr stevenelliottjr requested a review from a team as a code owner February 17, 2026 16:22
@dotnet-policy-service dotnet-policy-service bot added the Community The pull request was submitted by a contributor who is not a Microsoft employee. label Feb 17, 2026
";
var comp = CreateCompilation(text, options: TestOptions.ReleaseDll);
comp.VerifyDiagnostics(
// (4,15): warning CS0649: Field 'X.a' is never assigned to, and will always have its default value
Copy link
Contributor

Choose a reason for hiding this comment

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

// (4,15): warning CS0649: Field 'X.a' is never assigned to, and will always have its default value

Consider suppressing the warning with #pragma

}
";
var comp = CreateCompilation(text, options: TestOptions.ReleaseDll);
comp.VerifyDiagnostics(
Copy link
Contributor

Choose a reason for hiding this comment

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

VerifyDiagnostics

We generally prefer combining VerifyDiagnostics with CompileAndVerify since it collects diagnostics anyway and collects all diagnostics.

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 1)

@AlekseyTs AlekseyTs requested a review from a team February 18, 2026 16:20
@AlekseyTs
Copy link
Contributor

@dotnet/roslyn-compiler For a second review

var comp = CompileAndVerify(text, parseOptions: TestOptions.Regular, verify: Verification.Passes, expectedOutput: @"Program+S1Program+S1");

// We may be able to optimize this case (ie. avoid defensive copy) since the struct and the caller are in the same module
// Tracked by https://github.com/dotnet/roslyn/issues/66365
Copy link
Member

Choose a reason for hiding this comment

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

Consider marking the issue as closed by the PR (writing literally closes #66365 in the PR's description)

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

Labels

Area-Compilers Community The pull request was submitted by a contributor who is not a Microsoft employee.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unnecessary defensive copy on readonly struct when performing a constrained call

4 participants

Comments