Skip to content

Undoing boxing doesn't work with C# 7 pattern matching #10195

Description

@svick

Short version: I was reading @stephentoub's article Performance Improvements in .NET Core 2.1. I noticed that his example for avoiding boxing allocations thanks to dotnet/coreclr#14698 uses is followed by a cast, when in C# 7, the same code could be simplified using pattern matching. So I was wondering if using C# 7 features also results in the same efficient code. It turns out it doesn't and I think this should be improved.

More details:

Consider this code:

usingSystem.Runtime.CompilerServices;classProgram{staticvoidMain(){Cast(newDog());Pattern(newDog());}[MethodImpl(MethodImplOptions.NoInlining)]staticvoidCast<T>(Tthing){if(thingisIAnimal)((IAnimal)thing).MakeSound();}[MethodImpl(MethodImplOptions.NoInlining)]staticvoidPattern<T>(Tthing){if(thingisIAnimalanimal)animal.MakeSound();}}structDog:IAnimal{publicvoidBark(){}voidIAnimal.MakeSound()=>Bark();}interfaceIAnimal{voidMakeSound();}

The IL for the relevant methods is:

.method private hidebysig static void Cast<T>(!!T thing) cil managed noinlining
{
// Code size 30 (0x1e)
.maxstack 8
IL_0000: ldarg.0
IL_0001: box !!T
IL_0006: isinst IAnimal
IL_000b: brfalse.s IL_001d
IL_000d: ldarg.0
IL_000e: box !!T
IL_0013: castclass IAnimal
IL_0018: callvirt instance void IAnimal::MakeSound()
IL_001d: ret
}
.method private hidebysig static void Pattern<T>(!!T thing) cil managed noinlining
{
// Code size 22 (0x16)
.maxstack 2
.locals init (class IAnimal V_0)
IL_0000: ldarg.0
IL_0001: box !!T
IL_0006: isinst IAnimal
IL_000b: dup
IL_000c: stloc.0
IL_000d: brfalse.s IL_0015
IL_000f: ldloc.0
IL_0010: callvirt instance void IAnimal::MakeSound()
IL_0015: ret
}

Notice how in Pattern, the boxed object is saved to a local variable (typed as the interface).

The disassembly from .Net Core 2.1.0-preview2-26406-04 win10-x64 is:

; Assembly listing for method Program:Cast(struct); Emitting BLENDED_CODE for X64 CPU with AVX; optimized code; rsp based frame; partially interruptible; Final local variable assignments;; V00 arg0 [V00 ] ( 2, 2 ) struct ( 8) [rsp+0x08] do-not-enreg[XS] addr-exposed;* V01 tmp0 [V01 ] ( 0, 0 ) ref -> zero-ref class-hnd exact;* V02 tmp1 [V02 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] class-hnd exact;# V03 OutArgs [V03 ] ( 1, 1 ) lclBlk ( 0) [rsp+0x00] ;; Lcl frame size = 0G_M19994_IG01: 48894C2408 mov qword ptr [rsp+08H],rcxG_M19994_IG02: C3 ret; Total bytes of code 6, prolog size 0 for method Program:Cast(struct); ============================================================; Assembly listing for method Program:Pattern(struct); Emitting BLENDED_CODE for X64 CPU with AVX; optimized code; rsp based frame; partially interruptible; Final local variable assignments;; V00 arg0 [V00 ] ( 4, 4 ) struct ( 8) [rsp+0x30] do-not-enreg[XSF] addr-exposed; V01 loc0 [V01,T02] ( 3, 2 ) ref -> rax class-hnd exact; V02 tmp0 [V02,T00] ( 4, 8 ) ref -> rax class-hnd exact; V03 tmp1 [V03,T01] ( 2, 4 ) ref -> rax class-hnd exact; V04 OutArgs [V04 ] ( 1, 1 ) lclBlk (32) [rsp+0x00] ;; Lcl frame size = 40G_M22101_IG01: 4883EC28 subrsp,40 48894C2430 mov qword ptr [rsp+30H],rcxG_M22101_IG02: 48B9005F64B2F87F0000 movrcx,0x7FF8B2645F00 E8A86B0F5F call CORINFO_HELP_NEWSFAST 480FBE4C2430 movsxrcx, byte ptr [rsp+30H]884808mov byte ptr [rax+8],cl 488BC8 movrcx,rax E897FBFFFF call Dog:IAnimal.MakeSound():this90nopG_M22101_IG03: 4883C428 addrsp,40 C3 ret; Total bytes of code 47, prolog size 4 for method Program:Pattern(struct); ============================================================

Notice how for Cast, almost all the code, including the boxing allocation, is optimized away (the remaining mov seems to be unnecessary, but that's not really relevant here). But for Pattern, all the code is still there, including an allocation and a non-inlined call to Dog.IAnimal.MakeSound.

The two versions of the code do the same thing, so I think they should have comparable performance. Especially since the pattern matching version is more readable and I suspect it's also going to be more common in new code than the other version.

How hard would it be to make this optimization work even in the pattern matching version?

If it would be too hard to perform this optimization in the JIT, is there a reasonable way for the C# compiler to emit IL that would be optmized?

cc (?): @AndyAyersMS, @benaadams, @justinvp

category:cq
theme:importer
skill-level:expert
cost:medium

Metadata

Metadata

Labels

Priority:2Work that is important, but not critical for the releasearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIoptimization

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Undoing boxing doesn't work with C# 7 pattern matching · Issue #10195 · dotnet/runtime · GitHub
Skip to content

Undoing boxing doesn't work with C# 7 pattern matching #10195

Description

@svick

Short version: I was reading @stephentoub's article Performance Improvements in .NET Core 2.1. I noticed that his example for avoiding boxing allocations thanks to dotnet/coreclr#14698 uses is followed by a cast, when in C# 7, the same code could be simplified using pattern matching. So I was wondering if using C# 7 features also results in the same efficient code. It turns out it doesn't and I think this should be improved.

More details:

Consider this code:

usingSystem.Runtime.CompilerServices;classProgram{staticvoidMain(){Cast(newDog());Pattern(newDog());}[MethodImpl(MethodImplOptions.NoInlining)]staticvoidCast<T>(Tthing){if(thingisIAnimal)((IAnimal)thing).MakeSound();}[MethodImpl(MethodImplOptions.NoInlining)]staticvoidPattern<T>(Tthing){if(thingisIAnimalanimal)animal.MakeSound();}}structDog:IAnimal{publicvoidBark(){}voidIAnimal.MakeSound()=>Bark();}interfaceIAnimal{voidMakeSound();}

The IL for the relevant methods is:

.method private hidebysig static void Cast<T>(!!T thing) cil managed noinlining
{
// Code size 30 (0x1e)
.maxstack 8
IL_0000: ldarg.0
IL_0001: box !!T
IL_0006: isinst IAnimal
IL_000b: brfalse.s IL_001d
IL_000d: ldarg.0
IL_000e: box !!T
IL_0013: castclass IAnimal
IL_0018: callvirt instance void IAnimal::MakeSound()
IL_001d: ret
}
.method private hidebysig static void Pattern<T>(!!T thing) cil managed noinlining
{
// Code size 22 (0x16)
.maxstack 2
.locals init (class IAnimal V_0)
IL_0000: ldarg.0
IL_0001: box !!T
IL_0006: isinst IAnimal
IL_000b: dup
IL_000c: stloc.0
IL_000d: brfalse.s IL_0015
IL_000f: ldloc.0
IL_0010: callvirt instance void IAnimal::MakeSound()
IL_0015: ret
}

Notice how in Pattern, the boxed object is saved to a local variable (typed as the interface).

The disassembly from .Net Core 2.1.0-preview2-26406-04 win10-x64 is:

; Assembly listing for method Program:Cast(struct); Emitting BLENDED_CODE for X64 CPU with AVX; optimized code; rsp based frame; partially interruptible; Final local variable assignments;; V00 arg0 [V00 ] ( 2, 2 ) struct ( 8) [rsp+0x08] do-not-enreg[XS] addr-exposed;* V01 tmp0 [V01 ] ( 0, 0 ) ref -> zero-ref class-hnd exact;* V02 tmp1 [V02 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] class-hnd exact;# V03 OutArgs [V03 ] ( 1, 1 ) lclBlk ( 0) [rsp+0x00] ;; Lcl frame size = 0G_M19994_IG01: 48894C2408 mov qword ptr [rsp+08H],rcxG_M19994_IG02: C3 ret; Total bytes of code 6, prolog size 0 for method Program:Cast(struct); ============================================================; Assembly listing for method Program:Pattern(struct); Emitting BLENDED_CODE for X64 CPU with AVX; optimized code; rsp based frame; partially interruptible; Final local variable assignments;; V00 arg0 [V00 ] ( 4, 4 ) struct ( 8) [rsp+0x30] do-not-enreg[XSF] addr-exposed; V01 loc0 [V01,T02] ( 3, 2 ) ref -> rax class-hnd exact; V02 tmp0 [V02,T00] ( 4, 8 ) ref -> rax class-hnd exact; V03 tmp1 [V03,T01] ( 2, 4 ) ref -> rax class-hnd exact; V04 OutArgs [V04 ] ( 1, 1 ) lclBlk (32) [rsp+0x00] ;; Lcl frame size = 40G_M22101_IG01: 4883EC28 subrsp,40 48894C2430 mov qword ptr [rsp+30H],rcxG_M22101_IG02: 48B9005F64B2F87F0000 movrcx,0x7FF8B2645F00 E8A86B0F5F call CORINFO_HELP_NEWSFAST 480FBE4C2430 movsxrcx, byte ptr [rsp+30H]884808mov byte ptr [rax+8],cl 488BC8 movrcx,rax E897FBFFFF call Dog:IAnimal.MakeSound():this90nopG_M22101_IG03: 4883C428 addrsp,40 C3 ret; Total bytes of code 47, prolog size 4 for method Program:Pattern(struct); ============================================================

Notice how for Cast, almost all the code, including the boxing allocation, is optimized away (the remaining mov seems to be unnecessary, but that's not really relevant here). But for Pattern, all the code is still there, including an allocation and a non-inlined call to Dog.IAnimal.MakeSound.

The two versions of the code do the same thing, so I think they should have comparable performance. Especially since the pattern matching version is more readable and I suspect it's also going to be more common in new code than the other version.

How hard would it be to make this optimization work even in the pattern matching version?

If it would be too hard to perform this optimization in the JIT, is there a reasonable way for the C# compiler to emit IL that would be optmized?

cc (?): @AndyAyersMS, @benaadams, @justinvp

category:cq
theme:importer
skill-level:expert
cost:medium

Metadata

Metadata

Labels

Priority:2Work that is important, but not critical for the releasearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIoptimization

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Undoing boxing doesn't work with C# 7 pattern matching · Issue #10195 · dotnet/runtime · GitHub
Skip to content

Undoing boxing doesn't work with C# 7 pattern matching #10195

Description

@svick

Short version: I was reading @stephentoub's article Performance Improvements in .NET Core 2.1. I noticed that his example for avoiding boxing allocations thanks to dotnet/coreclr#14698 uses is followed by a cast, when in C# 7, the same code could be simplified using pattern matching. So I was wondering if using C# 7 features also results in the same efficient code. It turns out it doesn't and I think this should be improved.

More details:

Consider this code:

usingSystem.Runtime.CompilerServices;classProgram{staticvoidMain(){Cast(newDog());Pattern(newDog());}[MethodImpl(MethodImplOptions.NoInlining)]staticvoidCast<T>(Tthing){if(thingisIAnimal)((IAnimal)thing).MakeSound();}[MethodImpl(MethodImplOptions.NoInlining)]staticvoidPattern<T>(Tthing){if(thingisIAnimalanimal)animal.MakeSound();}}structDog:IAnimal{publicvoidBark(){}voidIAnimal.MakeSound()=>Bark();}interfaceIAnimal{voidMakeSound();}

The IL for the relevant methods is:

.method private hidebysig static void Cast<T>(!!T thing) cil managed noinlining
{
// Code size 30 (0x1e)
.maxstack 8
IL_0000: ldarg.0
IL_0001: box !!T
IL_0006: isinst IAnimal
IL_000b: brfalse.s IL_001d
IL_000d: ldarg.0
IL_000e: box !!T
IL_0013: castclass IAnimal
IL_0018: callvirt instance void IAnimal::MakeSound()
IL_001d: ret
}
.method private hidebysig static void Pattern<T>(!!T thing) cil managed noinlining
{
// Code size 22 (0x16)
.maxstack 2
.locals init (class IAnimal V_0)
IL_0000: ldarg.0
IL_0001: box !!T
IL_0006: isinst IAnimal
IL_000b: dup
IL_000c: stloc.0
IL_000d: brfalse.s IL_0015
IL_000f: ldloc.0
IL_0010: callvirt instance void IAnimal::MakeSound()
IL_0015: ret
}

Notice how in Pattern, the boxed object is saved to a local variable (typed as the interface).

The disassembly from .Net Core 2.1.0-preview2-26406-04 win10-x64 is:

; Assembly listing for method Program:Cast(struct); Emitting BLENDED_CODE for X64 CPU with AVX; optimized code; rsp based frame; partially interruptible; Final local variable assignments;; V00 arg0 [V00 ] ( 2, 2 ) struct ( 8) [rsp+0x08] do-not-enreg[XS] addr-exposed;* V01 tmp0 [V01 ] ( 0, 0 ) ref -> zero-ref class-hnd exact;* V02 tmp1 [V02 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] class-hnd exact;# V03 OutArgs [V03 ] ( 1, 1 ) lclBlk ( 0) [rsp+0x00] ;; Lcl frame size = 0G_M19994_IG01: 48894C2408 mov qword ptr [rsp+08H],rcxG_M19994_IG02: C3 ret; Total bytes of code 6, prolog size 0 for method Program:Cast(struct); ============================================================; Assembly listing for method Program:Pattern(struct); Emitting BLENDED_CODE for X64 CPU with AVX; optimized code; rsp based frame; partially interruptible; Final local variable assignments;; V00 arg0 [V00 ] ( 4, 4 ) struct ( 8) [rsp+0x30] do-not-enreg[XSF] addr-exposed; V01 loc0 [V01,T02] ( 3, 2 ) ref -> rax class-hnd exact; V02 tmp0 [V02,T00] ( 4, 8 ) ref -> rax class-hnd exact; V03 tmp1 [V03,T01] ( 2, 4 ) ref -> rax class-hnd exact; V04 OutArgs [V04 ] ( 1, 1 ) lclBlk (32) [rsp+0x00] ;; Lcl frame size = 40G_M22101_IG01: 4883EC28 subrsp,40 48894C2430 mov qword ptr [rsp+30H],rcxG_M22101_IG02: 48B9005F64B2F87F0000 movrcx,0x7FF8B2645F00 E8A86B0F5F call CORINFO_HELP_NEWSFAST 480FBE4C2430 movsxrcx, byte ptr [rsp+30H]884808mov byte ptr [rax+8],cl 488BC8 movrcx,rax E897FBFFFF call Dog:IAnimal.MakeSound():this90nopG_M22101_IG03: 4883C428 addrsp,40 C3 ret; Total bytes of code 47, prolog size 4 for method Program:Pattern(struct); ============================================================

Notice how for Cast, almost all the code, including the boxing allocation, is optimized away (the remaining mov seems to be unnecessary, but that's not really relevant here). But for Pattern, all the code is still there, including an allocation and a non-inlined call to Dog.IAnimal.MakeSound.

The two versions of the code do the same thing, so I think they should have comparable performance. Especially since the pattern matching version is more readable and I suspect it's also going to be more common in new code than the other version.

How hard would it be to make this optimization work even in the pattern matching version?

If it would be too hard to perform this optimization in the JIT, is there a reasonable way for the C# compiler to emit IL that would be optmized?

cc (?): @AndyAyersMS, @benaadams, @justinvp

category:cq
theme:importer
skill-level:expert
cost:medium

Metadata

Metadata

Labels

Priority:2Work that is important, but not critical for the releasearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIoptimization

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Undoing boxing doesn't work with C# 7 pattern matching · Issue #10195 · dotnet/runtime · GitHub
Skip to content

Undoing boxing doesn't work with C# 7 pattern matching #10195

Description

@svick

Short version: I was reading @stephentoub's article Performance Improvements in .NET Core 2.1. I noticed that his example for avoiding boxing allocations thanks to dotnet/coreclr#14698 uses is followed by a cast, when in C# 7, the same code could be simplified using pattern matching. So I was wondering if using C# 7 features also results in the same efficient code. It turns out it doesn't and I think this should be improved.

More details:

Consider this code:

usingSystem.Runtime.CompilerServices;classProgram{staticvoidMain(){Cast(newDog());Pattern(newDog());}[MethodImpl(MethodImplOptions.NoInlining)]staticvoidCast<T>(Tthing){if(thingisIAnimal)((IAnimal)thing).MakeSound();}[MethodImpl(MethodImplOptions.NoInlining)]staticvoidPattern<T>(Tthing){if(thingisIAnimalanimal)animal.MakeSound();}}structDog:IAnimal{publicvoidBark(){}voidIAnimal.MakeSound()=>Bark();}interfaceIAnimal{voidMakeSound();}

The IL for the relevant methods is:

.method private hidebysig static void Cast<T>(!!T thing) cil managed noinlining
{
// Code size 30 (0x1e)
.maxstack 8
IL_0000: ldarg.0
IL_0001: box !!T
IL_0006: isinst IAnimal
IL_000b: brfalse.s IL_001d
IL_000d: ldarg.0
IL_000e: box !!T
IL_0013: castclass IAnimal
IL_0018: callvirt instance void IAnimal::MakeSound()
IL_001d: ret
}
.method private hidebysig static void Pattern<T>(!!T thing) cil managed noinlining
{
// Code size 22 (0x16)
.maxstack 2
.locals init (class IAnimal V_0)
IL_0000: ldarg.0
IL_0001: box !!T
IL_0006: isinst IAnimal
IL_000b: dup
IL_000c: stloc.0
IL_000d: brfalse.s IL_0015
IL_000f: ldloc.0
IL_0010: callvirt instance void IAnimal::MakeSound()
IL_0015: ret
}

Notice how in Pattern, the boxed object is saved to a local variable (typed as the interface).

The disassembly from .Net Core 2.1.0-preview2-26406-04 win10-x64 is:

; Assembly listing for method Program:Cast(struct); Emitting BLENDED_CODE for X64 CPU with AVX; optimized code; rsp based frame; partially interruptible; Final local variable assignments;; V00 arg0 [V00 ] ( 2, 2 ) struct ( 8) [rsp+0x08] do-not-enreg[XS] addr-exposed;* V01 tmp0 [V01 ] ( 0, 0 ) ref -> zero-ref class-hnd exact;* V02 tmp1 [V02 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] class-hnd exact;# V03 OutArgs [V03 ] ( 1, 1 ) lclBlk ( 0) [rsp+0x00] ;; Lcl frame size = 0G_M19994_IG01: 48894C2408 mov qword ptr [rsp+08H],rcxG_M19994_IG02: C3 ret; Total bytes of code 6, prolog size 0 for method Program:Cast(struct); ============================================================; Assembly listing for method Program:Pattern(struct); Emitting BLENDED_CODE for X64 CPU with AVX; optimized code; rsp based frame; partially interruptible; Final local variable assignments;; V00 arg0 [V00 ] ( 4, 4 ) struct ( 8) [rsp+0x30] do-not-enreg[XSF] addr-exposed; V01 loc0 [V01,T02] ( 3, 2 ) ref -> rax class-hnd exact; V02 tmp0 [V02,T00] ( 4, 8 ) ref -> rax class-hnd exact; V03 tmp1 [V03,T01] ( 2, 4 ) ref -> rax class-hnd exact; V04 OutArgs [V04 ] ( 1, 1 ) lclBlk (32) [rsp+0x00] ;; Lcl frame size = 40G_M22101_IG01: 4883EC28 subrsp,40 48894C2430 mov qword ptr [rsp+30H],rcxG_M22101_IG02: 48B9005F64B2F87F0000 movrcx,0x7FF8B2645F00 E8A86B0F5F call CORINFO_HELP_NEWSFAST 480FBE4C2430 movsxrcx, byte ptr [rsp+30H]884808mov byte ptr [rax+8],cl 488BC8 movrcx,rax E897FBFFFF call Dog:IAnimal.MakeSound():this90nopG_M22101_IG03: 4883C428 addrsp,40 C3 ret; Total bytes of code 47, prolog size 4 for method Program:Pattern(struct); ============================================================

Notice how for Cast, almost all the code, including the boxing allocation, is optimized away (the remaining mov seems to be unnecessary, but that's not really relevant here). But for Pattern, all the code is still there, including an allocation and a non-inlined call to Dog.IAnimal.MakeSound.

The two versions of the code do the same thing, so I think they should have comparable performance. Especially since the pattern matching version is more readable and I suspect it's also going to be more common in new code than the other version.

How hard would it be to make this optimization work even in the pattern matching version?

If it would be too hard to perform this optimization in the JIT, is there a reasonable way for the C# compiler to emit IL that would be optmized?

cc (?): @AndyAyersMS, @benaadams, @justinvp

category:cq
theme:importer
skill-level:expert
cost:medium

Metadata

Metadata

Labels

Priority:2Work that is important, but not critical for the releasearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIoptimization

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Undoing boxing doesn't work with C# 7 pattern matching · Issue #10195 · dotnet/runtime · GitHub
Skip to content

Undoing boxing doesn't work with C# 7 pattern matching #10195

Description

@svick

Short version: I was reading @stephentoub's article Performance Improvements in .NET Core 2.1. I noticed that his example for avoiding boxing allocations thanks to dotnet/coreclr#14698 uses is followed by a cast, when in C# 7, the same code could be simplified using pattern matching. So I was wondering if using C# 7 features also results in the same efficient code. It turns out it doesn't and I think this should be improved.

More details:

Consider this code:

usingSystem.Runtime.CompilerServices;classProgram{staticvoidMain(){Cast(newDog());Pattern(newDog());}[MethodImpl(MethodImplOptions.NoInlining)]staticvoidCast<T>(Tthing){if(thingisIAnimal)((IAnimal)thing).MakeSound();}[MethodImpl(MethodImplOptions.NoInlining)]staticvoidPattern<T>(Tthing){if(thingisIAnimalanimal)animal.MakeSound();}}structDog:IAnimal{publicvoidBark(){}voidIAnimal.MakeSound()=>Bark();}interfaceIAnimal{voidMakeSound();}

The IL for the relevant methods is:

.method private hidebysig static void Cast<T>(!!T thing) cil managed noinlining
{
// Code size 30 (0x1e)
.maxstack 8
IL_0000: ldarg.0
IL_0001: box !!T
IL_0006: isinst IAnimal
IL_000b: brfalse.s IL_001d
IL_000d: ldarg.0
IL_000e: box !!T
IL_0013: castclass IAnimal
IL_0018: callvirt instance void IAnimal::MakeSound()
IL_001d: ret
}
.method private hidebysig static void Pattern<T>(!!T thing) cil managed noinlining
{
// Code size 22 (0x16)
.maxstack 2
.locals init (class IAnimal V_0)
IL_0000: ldarg.0
IL_0001: box !!T
IL_0006: isinst IAnimal
IL_000b: dup
IL_000c: stloc.0
IL_000d: brfalse.s IL_0015
IL_000f: ldloc.0
IL_0010: callvirt instance void IAnimal::MakeSound()
IL_0015: ret
}

Notice how in Pattern, the boxed object is saved to a local variable (typed as the interface).

The disassembly from .Net Core 2.1.0-preview2-26406-04 win10-x64 is:

; Assembly listing for method Program:Cast(struct); Emitting BLENDED_CODE for X64 CPU with AVX; optimized code; rsp based frame; partially interruptible; Final local variable assignments;; V00 arg0 [V00 ] ( 2, 2 ) struct ( 8) [rsp+0x08] do-not-enreg[XS] addr-exposed;* V01 tmp0 [V01 ] ( 0, 0 ) ref -> zero-ref class-hnd exact;* V02 tmp1 [V02 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] class-hnd exact;# V03 OutArgs [V03 ] ( 1, 1 ) lclBlk ( 0) [rsp+0x00] ;; Lcl frame size = 0G_M19994_IG01: 48894C2408 mov qword ptr [rsp+08H],rcxG_M19994_IG02: C3 ret; Total bytes of code 6, prolog size 0 for method Program:Cast(struct); ============================================================; Assembly listing for method Program:Pattern(struct); Emitting BLENDED_CODE for X64 CPU with AVX; optimized code; rsp based frame; partially interruptible; Final local variable assignments;; V00 arg0 [V00 ] ( 4, 4 ) struct ( 8) [rsp+0x30] do-not-enreg[XSF] addr-exposed; V01 loc0 [V01,T02] ( 3, 2 ) ref -> rax class-hnd exact; V02 tmp0 [V02,T00] ( 4, 8 ) ref -> rax class-hnd exact; V03 tmp1 [V03,T01] ( 2, 4 ) ref -> rax class-hnd exact; V04 OutArgs [V04 ] ( 1, 1 ) lclBlk (32) [rsp+0x00] ;; Lcl frame size = 40G_M22101_IG01: 4883EC28 subrsp,40 48894C2430 mov qword ptr [rsp+30H],rcxG_M22101_IG02: 48B9005F64B2F87F0000 movrcx,0x7FF8B2645F00 E8A86B0F5F call CORINFO_HELP_NEWSFAST 480FBE4C2430 movsxrcx, byte ptr [rsp+30H]884808mov byte ptr [rax+8],cl 488BC8 movrcx,rax E897FBFFFF call Dog:IAnimal.MakeSound():this90nopG_M22101_IG03: 4883C428 addrsp,40 C3 ret; Total bytes of code 47, prolog size 4 for method Program:Pattern(struct); ============================================================

Notice how for Cast, almost all the code, including the boxing allocation, is optimized away (the remaining mov seems to be unnecessary, but that's not really relevant here). But for Pattern, all the code is still there, including an allocation and a non-inlined call to Dog.IAnimal.MakeSound.

The two versions of the code do the same thing, so I think they should have comparable performance. Especially since the pattern matching version is more readable and I suspect it's also going to be more common in new code than the other version.

How hard would it be to make this optimization work even in the pattern matching version?

If it would be too hard to perform this optimization in the JIT, is there a reasonable way for the C# compiler to emit IL that would be optmized?

cc (?): @AndyAyersMS, @benaadams, @justinvp

category:cq
theme:importer
skill-level:expert
cost:medium

Metadata

Metadata

Labels

Priority:2Work that is important, but not critical for the releasearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIoptimization

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Undoing boxing doesn't work with C# 7 pattern matching · Issue #10195 · dotnet/runtime · GitHub
Skip to content

Undoing boxing doesn't work with C# 7 pattern matching #10195

Description

@svick

Short version: I was reading @stephentoub's article Performance Improvements in .NET Core 2.1. I noticed that his example for avoiding boxing allocations thanks to dotnet/coreclr#14698 uses is followed by a cast, when in C# 7, the same code could be simplified using pattern matching. So I was wondering if using C# 7 features also results in the same efficient code. It turns out it doesn't and I think this should be improved.

More details:

Consider this code:

usingSystem.Runtime.CompilerServices;classProgram{staticvoidMain(){Cast(newDog());Pattern(newDog());}[MethodImpl(MethodImplOptions.NoInlining)]staticvoidCast<T>(Tthing){if(thingisIAnimal)((IAnimal)thing).MakeSound();}[MethodImpl(MethodImplOptions.NoInlining)]staticvoidPattern<T>(Tthing){if(thingisIAnimalanimal)animal.MakeSound();}}structDog:IAnimal{publicvoidBark(){}voidIAnimal.MakeSound()=>Bark();}interfaceIAnimal{voidMakeSound();}

The IL for the relevant methods is:

.method private hidebysig static void Cast<T>(!!T thing) cil managed noinlining
{
// Code size 30 (0x1e)
.maxstack 8
IL_0000: ldarg.0
IL_0001: box !!T
IL_0006: isinst IAnimal
IL_000b: brfalse.s IL_001d
IL_000d: ldarg.0
IL_000e: box !!T
IL_0013: castclass IAnimal
IL_0018: callvirt instance void IAnimal::MakeSound()
IL_001d: ret
}
.method private hidebysig static void Pattern<T>(!!T thing) cil managed noinlining
{
// Code size 22 (0x16)
.maxstack 2
.locals init (class IAnimal V_0)
IL_0000: ldarg.0
IL_0001: box !!T
IL_0006: isinst IAnimal
IL_000b: dup
IL_000c: stloc.0
IL_000d: brfalse.s IL_0015
IL_000f: ldloc.0
IL_0010: callvirt instance void IAnimal::MakeSound()
IL_0015: ret
}

Notice how in Pattern, the boxed object is saved to a local variable (typed as the interface).

The disassembly from .Net Core 2.1.0-preview2-26406-04 win10-x64 is:

; Assembly listing for method Program:Cast(struct); Emitting BLENDED_CODE for X64 CPU with AVX; optimized code; rsp based frame; partially interruptible; Final local variable assignments;; V00 arg0 [V00 ] ( 2, 2 ) struct ( 8) [rsp+0x08] do-not-enreg[XS] addr-exposed;* V01 tmp0 [V01 ] ( 0, 0 ) ref -> zero-ref class-hnd exact;* V02 tmp1 [V02 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] class-hnd exact;# V03 OutArgs [V03 ] ( 1, 1 ) lclBlk ( 0) [rsp+0x00] ;; Lcl frame size = 0G_M19994_IG01: 48894C2408 mov qword ptr [rsp+08H],rcxG_M19994_IG02: C3 ret; Total bytes of code 6, prolog size 0 for method Program:Cast(struct); ============================================================; Assembly listing for method Program:Pattern(struct); Emitting BLENDED_CODE for X64 CPU with AVX; optimized code; rsp based frame; partially interruptible; Final local variable assignments;; V00 arg0 [V00 ] ( 4, 4 ) struct ( 8) [rsp+0x30] do-not-enreg[XSF] addr-exposed; V01 loc0 [V01,T02] ( 3, 2 ) ref -> rax class-hnd exact; V02 tmp0 [V02,T00] ( 4, 8 ) ref -> rax class-hnd exact; V03 tmp1 [V03,T01] ( 2, 4 ) ref -> rax class-hnd exact; V04 OutArgs [V04 ] ( 1, 1 ) lclBlk (32) [rsp+0x00] ;; Lcl frame size = 40G_M22101_IG01: 4883EC28 subrsp,40 48894C2430 mov qword ptr [rsp+30H],rcxG_M22101_IG02: 48B9005F64B2F87F0000 movrcx,0x7FF8B2645F00 E8A86B0F5F call CORINFO_HELP_NEWSFAST 480FBE4C2430 movsxrcx, byte ptr [rsp+30H]884808mov byte ptr [rax+8],cl 488BC8 movrcx,rax E897FBFFFF call Dog:IAnimal.MakeSound():this90nopG_M22101_IG03: 4883C428 addrsp,40 C3 ret; Total bytes of code 47, prolog size 4 for method Program:Pattern(struct); ============================================================

Notice how for Cast, almost all the code, including the boxing allocation, is optimized away (the remaining mov seems to be unnecessary, but that's not really relevant here). But for Pattern, all the code is still there, including an allocation and a non-inlined call to Dog.IAnimal.MakeSound.

The two versions of the code do the same thing, so I think they should have comparable performance. Especially since the pattern matching version is more readable and I suspect it's also going to be more common in new code than the other version.

How hard would it be to make this optimization work even in the pattern matching version?

If it would be too hard to perform this optimization in the JIT, is there a reasonable way for the C# compiler to emit IL that would be optmized?

cc (?): @AndyAyersMS, @benaadams, @justinvp

category:cq
theme:importer
skill-level:expert
cost:medium

Metadata

Metadata

Labels

Priority:2Work that is important, but not critical for the releasearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIoptimization

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Undoing boxing doesn't work with C# 7 pattern matching · Issue #10195 · dotnet/runtime · GitHub
Skip to content

Undoing boxing doesn't work with C# 7 pattern matching #10195

Description

@svick

Short version: I was reading @stephentoub's article Performance Improvements in .NET Core 2.1. I noticed that his example for avoiding boxing allocations thanks to dotnet/coreclr#14698 uses is followed by a cast, when in C# 7, the same code could be simplified using pattern matching. So I was wondering if using C# 7 features also results in the same efficient code. It turns out it doesn't and I think this should be improved.

More details:

Consider this code:

usingSystem.Runtime.CompilerServices;classProgram{staticvoidMain(){Cast(newDog());Pattern(newDog());}[MethodImpl(MethodImplOptions.NoInlining)]staticvoidCast<T>(Tthing){if(thingisIAnimal)((IAnimal)thing).MakeSound();}[MethodImpl(MethodImplOptions.NoInlining)]staticvoidPattern<T>(Tthing){if(thingisIAnimalanimal)animal.MakeSound();}}structDog:IAnimal{publicvoidBark(){}voidIAnimal.MakeSound()=>Bark();}interfaceIAnimal{voidMakeSound();}

The IL for the relevant methods is:

.method private hidebysig static void Cast<T>(!!T thing) cil managed noinlining
{
// Code size 30 (0x1e)
.maxstack 8
IL_0000: ldarg.0
IL_0001: box !!T
IL_0006: isinst IAnimal
IL_000b: brfalse.s IL_001d
IL_000d: ldarg.0
IL_000e: box !!T
IL_0013: castclass IAnimal
IL_0018: callvirt instance void IAnimal::MakeSound()
IL_001d: ret
}
.method private hidebysig static void Pattern<T>(!!T thing) cil managed noinlining
{
// Code size 22 (0x16)
.maxstack 2
.locals init (class IAnimal V_0)
IL_0000: ldarg.0
IL_0001: box !!T
IL_0006: isinst IAnimal
IL_000b: dup
IL_000c: stloc.0
IL_000d: brfalse.s IL_0015
IL_000f: ldloc.0
IL_0010: callvirt instance void IAnimal::MakeSound()
IL_0015: ret
}

Notice how in Pattern, the boxed object is saved to a local variable (typed as the interface).

The disassembly from .Net Core 2.1.0-preview2-26406-04 win10-x64 is:

; Assembly listing for method Program:Cast(struct); Emitting BLENDED_CODE for X64 CPU with AVX; optimized code; rsp based frame; partially interruptible; Final local variable assignments;; V00 arg0 [V00 ] ( 2, 2 ) struct ( 8) [rsp+0x08] do-not-enreg[XS] addr-exposed;* V01 tmp0 [V01 ] ( 0, 0 ) ref -> zero-ref class-hnd exact;* V02 tmp1 [V02 ] ( 0, 0 ) struct ( 8) zero-ref do-not-enreg[SF] class-hnd exact;# V03 OutArgs [V03 ] ( 1, 1 ) lclBlk ( 0) [rsp+0x00] ;; Lcl frame size = 0G_M19994_IG01: 48894C2408 mov qword ptr [rsp+08H],rcxG_M19994_IG02: C3 ret; Total bytes of code 6, prolog size 0 for method Program:Cast(struct); ============================================================; Assembly listing for method Program:Pattern(struct); Emitting BLENDED_CODE for X64 CPU with AVX; optimized code; rsp based frame; partially interruptible; Final local variable assignments;; V00 arg0 [V00 ] ( 4, 4 ) struct ( 8) [rsp+0x30] do-not-enreg[XSF] addr-exposed; V01 loc0 [V01,T02] ( 3, 2 ) ref -> rax class-hnd exact; V02 tmp0 [V02,T00] ( 4, 8 ) ref -> rax class-hnd exact; V03 tmp1 [V03,T01] ( 2, 4 ) ref -> rax class-hnd exact; V04 OutArgs [V04 ] ( 1, 1 ) lclBlk (32) [rsp+0x00] ;; Lcl frame size = 40G_M22101_IG01: 4883EC28 subrsp,40 48894C2430 mov qword ptr [rsp+30H],rcxG_M22101_IG02: 48B9005F64B2F87F0000 movrcx,0x7FF8B2645F00 E8A86B0F5F call CORINFO_HELP_NEWSFAST 480FBE4C2430 movsxrcx, byte ptr [rsp+30H]884808mov byte ptr [rax+8],cl 488BC8 movrcx,rax E897FBFFFF call Dog:IAnimal.MakeSound():this90nopG_M22101_IG03: 4883C428 addrsp,40 C3 ret; Total bytes of code 47, prolog size 4 for method Program:Pattern(struct); ============================================================

Notice how for Cast, almost all the code, including the boxing allocation, is optimized away (the remaining mov seems to be unnecessary, but that's not really relevant here). But for Pattern, all the code is still there, including an allocation and a non-inlined call to Dog.IAnimal.MakeSound.

The two versions of the code do the same thing, so I think they should have comparable performance. Especially since the pattern matching version is more readable and I suspect it's also going to be more common in new code than the other version.

How hard would it be to make this optimization work even in the pattern matching version?

If it would be too hard to perform this optimization in the JIT, is there a reasonable way for the C# compiler to emit IL that would be optmized?

cc (?): @AndyAyersMS, @benaadams, @justinvp

category:cq
theme:importer
skill-level:expert
cost:medium

Metadata

Metadata

Labels

Priority:2Work that is important, but not critical for the releasearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIoptimization

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions