Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assign (generic) struct to default causes overzealous CORINFO_HELP_ASSIGN_BYREF #12060

Closed
benaadams opened this issue Feb 16, 2019 · 10 comments
Closed
Labels
area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Comments

@benaadams
Copy link
Member

Assigning references to null shouldn't need Jit_ByRefWriteBarrier however assigning a struct with GC references to default causes lots of write barrier calls; whereas I'd have thought it would just assign them to null?

e.g. in ASP.NET Core there is a feature cache that gets set to default in Initialize

FeatureReferences

public struct FeatureReferences<TCache>
{
    public FeatureReferences(IFeatureCollection collection)
    {
        Collection = collection;
        Cache = default(TCache); // zero the cache
        Revision = collection.Revision;
    }

    public IFeatureCollection Collection { get; private set; }
    public int Revision { get; private set; }
    
    // ...
}

DefaultHttpContext

public sealed class DefaultHttpContext : HttpContext
{
    // ...

    public void Initialize(IFeatureCollection features)
    {
        _features = new FeatureReferences<FeatureInterfaces>(features);
        _request.Initialize();
        _response.Initialize();
        _connection?.Initialize(features);
        _websockets?.Initialize(features);
    }
        
    struct FeatureInterfaces // Cache is 6 references
    {
        public IItemsFeature Items;
        public IServiceProvidersFeature ServiceProviders;
        public IHttpAuthenticationFeature Authentication;
        public IHttpRequestLifetimeFeature Lifetime;
        public ISessionFeature Session;
        public IHttpRequestIdentifierFeature RequestIdentifier;
    }
}

What it seems to do is blank out enough stack; then assign via CORINFO_HELP_ASSIGN_BYREF

; Assembly listing for method DefaultHttpContext:Initialize(ref):this
; Emitting BLENDED_CODE for X64 CPU with AVX - Windows
; optimized code
; rsp based frame
; partially interruptible
; Final local variable assignments
;
;  V00 this         [V00,T00] (  7,  7   )     ref  ->  rbx         this class-hnd
;  V01 arg1         [V01,T01] (  8,  6   )     ref  ->  rbp         class-hnd
;  V02 OutArgs      [V02    ] (  1,  1   )  lclBlk (32) [rsp+0x00]   "OutgoingArgSpace"
;  V03 tmp1         [V03,T02] (  4,  8   )  struct (64) [rsp+0x68]   do-not-enreg[SFB] must-init "NewObj constructor temp"
;  V04 tmp2         [V04,T05] (  2,  4   )     ref  ->  rsi         class-hnd "dup spill"
;  V05 tmp3         [V05,T08] (  3,  2.50)     ref  ->  rdi        
;  V06 tmp4         [V06,T06] (  2,  4   )     ref  ->  rsi         class-hnd "dup spill"
;  V07 tmp5         [V07,T09] (  3,  2.50)     ref  ->  rdi        
;  V08 tmp6         [V08,T07] (  2,  4   )     int  ->  rax         "Inlining Arg"
;  V09 tmp7         [V09,T03] (  5,  5   )  struct (32) [rsp+0x48]   do-not-enreg[SFB] must-init "NewObj constructor temp"
;  V10 tmp8         [V10,T10] (  2,  2   )     int  ->  rax         "Inlining Arg"
;  V11 tmp9         [V11,T04] (  5,  5   )  struct (32) [rsp+0x28]   do-not-enreg[SFB] must-init "NewObj constructor temp"
;  V12 tmp10        [V12,T11] (  2,  2   )     int  ->  rax         "Inlining Arg"
;
; Lcl frame size = 168

G_M56063_IG01:
       57                   push     rdi
       56                   push     rsi
       55                   push     rbp
       53                   push     rbx
       4881ECA8000000       sub      rsp, 168
       C5F877               vzeroupper 
       488BF1               mov      rsi, rcx
       488D7C2428           lea      rdi, [rsp+28H]
       B920000000           mov      ecx, 32
       33C0                 xor      rax, rax
       F3AB                 rep stosd 
       488BCE               mov      rcx, rsi
       488BD9               mov      rbx, rcx
       488BEA               mov      rbp, rdx

G_M56063_IG02:
       48896C2468           mov      gword ptr [rsp+68H], rbp
       488D4C2478           lea      rcx, bword ptr [rsp+78H]
       C5F857C0             vxorps   xmm0, xmm0
       C5FA7F01             vmovdqu  qword ptr [rcx], xmm0
       C5FA7F4110           vmovdqu  qword ptr [rcx+16], xmm0
       C5FA7F4120           vmovdqu  qword ptr [rcx+32], xmm0
       488BCD               mov      rcx, rbp
       49BBD811C8D9F87F0000 mov      r11, 0x7FF8D9C811D8
       3909                 cmp      dword ptr [rcx], ecx
       FF15CF52A3FF         call     [IFeatureCollection:get_Revision():int:this]
       89442470             mov      dword ptr [rsp+70H], eax
       488D7B38             lea      rdi, bword ptr [rbx+56]
       488D742468           lea      rsi, bword ptr [rsp+68H]
       E8E5FB675F           call     CORINFO_HELP_ASSIGN_BYREF
       48A5                 movsq    

                            ; // typeof(TCache) == typeof(DefaultHttpContext.FeatureInterfaces)
                            ; Cache = default(TCache);
       E8DEFB675F           call     CORINFO_HELP_ASSIGN_BYREF ; Items = null
       E8D9FB675F           call     CORINFO_HELP_ASSIGN_BYREF ; ServiceProviders = null
       E8D4FB675F           call     CORINFO_HELP_ASSIGN_BYREF ; Authentication = null 
       E8CFFB675F           call     CORINFO_HELP_ASSIGN_BYREF ; Lifetime = null
       E8CAFB675F           call     CORINFO_HELP_ASSIGN_BYREF ; Session = null
       E8C5FB675F           call     CORINFO_HELP_ASSIGN_BYREF ; RequestIdentifier = null

       488B4B08             mov      rcx, gword ptr [rbx+8]
       3909                 cmp      dword ptr [rcx], ecx
       E85A1AFFFF           call     DefaultHttpRequest:Initialize():this
       488B4B10             mov      rcx, gword ptr [rbx+16]
       3909                 cmp      dword ptr [rcx], ecx
       E87F1DFFFF           call     DefaultHttpResponse:Initialize():this
       488B7318             mov      rsi, gword ptr [rbx+24]
       488BFE               mov      rdi, rsi
       4885FF               test     rdi, rdi
       7460                 je       SHORT G_M56063_IG06

G_M56063_IG03:

This shows up at around 2% of total run time in the TE benchmarks:

image

/cc @AndyAyersMS @davidfowl @jkotas @mikedn

@benaadams benaadams changed the title Assign struct to default causes overzealous CORINFO_HELP_ASSIGN_BYREF Assign (generic) struct to default causes overzealous CORINFO_HELP_ASSIGN_BYREF Feb 16, 2019
@benaadams
Copy link
Member Author

.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <LangVersion>latest</LangVersion>
    <TieredCompilation>false</TieredCompilation>
  </PropertyGroup>

</Project>

Program.cs

using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;

public class Startup
{
    private static readonly byte[] _helloWorldBytes = Encoding.UTF8.GetBytes("Hello, World!");

    public void Configure(IApplicationBuilder app)
    {
        app.Run((httpContext) =>
        {
            var response = httpContext.Response;
            response.StatusCode = 200;
            response.ContentType = "text/plain";

            var helloWorld = _helloWorldBytes;
            response.ContentLength = helloWorld.Length;
            return response.Body.WriteAsync(helloWorld, 0, helloWorld.Length);
        });
    }

    public static Task Main(string[] args)
    {
        var host = new WebHostBuilder()
            .UseKestrel(options =>
            {
                options.Listen(IPAddress.Loopback, 5001);
            })
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseStartup<Startup>()
            .Build();

        return host.RunAsync();
    }
}

Dasm filter

set COMPlus_JitDisasm=DefaultHttpContext:*
set COMPlus_TieredCompilation=0
set COMPlus_ReadyToRun=0

Make 2 HTTP requests to http://127.0.0.1:5001/ when running

First will inline Initialize into the DefaultHttpContext:.ctor and second will output DefaultHttpContext:Initialize individually.

@benaadams
Copy link
Member Author

benaadams commented Feb 16, 2019

il is initobj !TCache

.method public hidebysig specialname rtspecialname 
        instance void  .ctor(class Microsoft.AspNetCore.Http.Features.IFeatureCollection collection) cil managed
{
  // Code size       32 (0x20)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldarg.1
  IL_0002:  call       instance void valuetype Microsoft.AspNetCore.Http.Features.FeatureReferences`1<!TCache>::set_Collection(class Microsoft.AspNetCore.Http.Features.IFeatureCollection)
  IL_0007:  ldarg.0
  IL_0008:  ldflda     !0 valuetype Microsoft.AspNetCore.Http.Features.FeatureReferences`1<!TCache>::Cache
  IL_000d:  initobj    !TCache
  IL_0013:  ldarg.0
  IL_0014:  ldarg.1
  IL_0015:  callvirt   instance int32 Microsoft.AspNetCore.Http.Features.IFeatureCollection::get_Revision()
  IL_001a:  call       instance void valuetype Microsoft.AspNetCore.Http.Features.FeatureReferences`1<!TCache>::set_Revision(int32)
  IL_001f:  ret
} // end of method FeatureReferences`1::.ctor

@jkotas
Copy link
Member

jkotas commented Feb 16, 2019

cc @CarolEidt

@benaadams
Copy link
Member Author

As a counter point Uninitialize which sets the top level to default (vs going via new which then sets a sub element to default) doesn't use CORINFO_HELP_ASSIGN_BYREF

public void Uninitialize()
{
    _features = default;
    _request.Uninitialize();
    _response.Uninitialize();
    _connection?.Uninitialize();
    _websockets?.Uninitialize();
}

It produces

; Assembly listing for method DefaultHttpContext:Uninitialize():this
; Emitting BLENDED_CODE for X64 CPU with AVX - Windows
; optimized code
; rsp based frame
; partially interruptible
; Final local variable assignments
;
;  V00 this         [V00,T00] (  7,  7   )     ref  ->  rcx         this class-hnd
;# V01 OutArgs      [V01    ] (  1,  1   )  lclBlk ( 0) [rsp+0x00]   "OutgoingArgSpace"
;  V02 tmp1         [V02,T03] (  2,  4   )     ref  ->  rax         class-hnd "dup spill"
;  V03 tmp2         [V03,T05] (  3,  2.50)     ref  ->  rax        
;  V04 tmp3         [V04,T04] (  2,  4   )     ref  ->  rax         class-hnd "dup spill"
;  V05 tmp4         [V05,T06] (  3,  2.50)     ref  ->  rax        
;  V06 tmp5         [V06,T01] (  3,  6   )     ref  ->  rax         class-hnd "Inlining Arg"
;  V07 tmp6         [V07,T02] (  3,  6   )     ref  ->  rax         class-hnd "Inlining Arg"
;
; Lcl frame size = 0

G_M29359_IG01:
       C5F877               vzeroupper 
       6690                 nop      

G_M29359_IG02:
       488D4138             lea      rax, bword ptr [rcx+56]
       C5F857C0             vxorps   xmm0, xmm0
       C5FA7F00             vmovdqu  qword ptr [rax], xmm0
       C5FA7F4010           vmovdqu  qword ptr [rax+16], xmm0
       C5FA7F4020           vmovdqu  qword ptr [rax+32], xmm0
       C5FA7F4030           vmovdqu  qword ptr [rax+48], xmm0
       488B4108             mov      rax, gword ptr [rcx+8]
       8B10                 mov      edx, dword ptr [rax]
       4883C018             add      rax, 24
       C5F857C0             vxorps   xmm0, xmm0
       C5FA7F00             vmovdqu  qword ptr [rax], xmm0
       C5FA7F4010           vmovdqu  qword ptr [rax+16], xmm0
       C5FA7F4020           vmovdqu  qword ptr [rax+32], xmm0
       C5FA7F4030           vmovdqu  qword ptr [rax+48], xmm0
       488B4110             mov      rax, gword ptr [rcx+16]
       8B10                 mov      edx, dword ptr [rax]
       4883C010             add      rax, 16
       C5F857C0             vxorps   xmm0, xmm0
       C5FA7F00             vmovdqu  qword ptr [rax], xmm0
       C5FA7F4010           vmovdqu  qword ptr [rax+16], xmm0
       C5FA7F4020           vmovdqu  qword ptr [rax+32], xmm0
       488B4118             mov      rax, gword ptr [rcx+24]
       4885C0               test     rax, rax
       7422                 je       SHORT G_M29363_IG04

G_M29363_IG03:
       4883C008             add      rax, 8
       C5F857C0             vxorps   xmm0, xmm0
       C5FA7F00             vmovdqu  qword ptr [rax], xmm0
       C5FA7F4010           vmovdqu  qword ptr [rax+16], xmm0

G_M29365_IG04:
       488B4120             mov      rax, gword ptr [rcx+32]
       4885C0               test     rax, rax
       7501                 jne      SHORT G_M29365_IG06

G_M29365_IG05:
       C3                   ret      

G_M29365_IG06:
       4883C008             add      rax, 8
       C5F857C0             vxorps   xmm0, xmm0
       C5FA7F00             vmovdqu  qword ptr [rax], xmm0
       C5FA7F4010           vmovdqu  qword ptr [rax+16], xmm0

G_M29365_IG07:
       C3                   ret      

; Total bytes of code 147, prolog size 5 for method DefaultHttpContext:Uninitialize():this

Work around might be to set to default, then initalize the 2 other items via method rather than using the struct .ctor?

@benaadams
Copy link
Member Author

Workaround would be dotnet/aspnetcore#7659; however would also be an api breaking change so not sure it would be acceptable

@benaadams
Copy link
Member Author

Which would mean I'd identified the wrong cause; as it would be stfld valuetype after newobj instance void valuetype; so its the copy that does the assignment

.method public hidebysig instance void  Initialize(class [Microsoft.AspNetCore.Http.Features]Microsoft.AspNetCore.Http.Features.IFeatureCollection features) cil managed
{
  // Code size       70 (0x46)
  .maxstack  2
  IL_0000:  ldarg.0
  IL_0001:  ldarg.1
  IL_0002:  newobj     instance void valuetype [Microsoft.AspNetCore.Http.Features]Microsoft.AspNetCore.Http.Features.FeatureReferences`1<valuetype Microsoft.AspNetCore.Http.DefaultHttpContext/FeatureInterfaces>::.ctor(class [Microsoft.AspNetCore.Http.Features]Microsoft.AspNetCore.Http.Features.IFeatureCollection)
  IL_0007:  stfld      valuetype [Microsoft.AspNetCore.Http.Features]Microsoft.AspNetCore.Http.Features.FeatureReferences`1<valuetype Microsoft.AspNetCore.Http.DefaultHttpContext/FeatureInterfaces> Microsoft.AspNetCore.Http.DefaultHttpContext::_features
  IL_000c:  ldarg.0
  IL_000d:  ldfld      class Microsoft.AspNetCore.Http.Internal.DefaultHttpRequest Microsoft.AspNetCore.Http.DefaultHttpContext::_request
  IL_0012:  callvirt   instance void Microsoft.AspNetCore.Http.Internal.DefaultHttpRequest::Initialize()
  IL_0017:  ldarg.0
  IL_0018:  ldfld      class Microsoft.AspNetCore.Http.Internal.DefaultHttpResponse Microsoft.AspNetCore.Http.DefaultHttpContext::_response
  IL_001d:  callvirt   instance void Microsoft.AspNetCore.Http.Internal.DefaultHttpResponse::Initialize()
  IL_0022:  ldarg.0
  IL_0023:  ldfld      class Microsoft.AspNetCore.Http.Internal.DefaultConnectionInfo Microsoft.AspNetCore.Http.DefaultHttpContext::_connection
  IL_0028:  dup
  IL_0029:  brtrue.s   IL_002e
  IL_002b:  pop
  IL_002c:  br.s       IL_0034
  IL_002e:  ldarg.1
  IL_002f:  call       instance void Microsoft.AspNetCore.Http.Internal.DefaultConnectionInfo::Initialize(class [Microsoft.AspNetCore.Http.Features]Microsoft.AspNetCore.Http.Features.IFeatureCollection)
  IL_0034:  ldarg.0
  IL_0035:  ldfld      class Microsoft.AspNetCore.Http.Internal.DefaultWebSocketManager Microsoft.AspNetCore.Http.DefaultHttpContext::_websockets
  IL_003a:  dup
  IL_003b:  brtrue.s   IL_003f
  IL_003d:  pop
  IL_003e:  ret
  IL_003f:  ldarg.1
  IL_0040:  call       instance void Microsoft.AspNetCore.Http.Internal.DefaultWebSocketManager::Initialize(class [Microsoft.AspNetCore.Http.Features]Microsoft.AspNetCore.Http.Features.IFeatureCollection)
  IL_0045:  ret
} // end of method DefaultHttpContext::Initialize

Might be by design then?

@jkotas
Copy link
Member

jkotas commented Feb 16, 2019

Right, constructors tend to not work well for big structs by design.

@benaadams
Copy link
Member Author

And if I add second method Initialize to the struct and leave the .ctor rather than replacing it; then its not a breaking change... 😸

@benaadams
Copy link
Member Author

The upstream change produces far better asm (as also don't have to set to default as its already default) dotnet/aspnetcore#7659 (comment)

So the question is, should this be closed ("by design") or left open as something that could be improved post inline (recognising the fields are null assigns)?

As it only becomes a potential optimization after inlining the .ctor (which is why it shows in the asm); without inlining the .ctor it couldn't be determined that the copy could use lighter assignments.

@benaadams
Copy link
Member Author

Closing, looks like its covered by this issue? https://github.com/dotnet/coreclr/issues/12812

@msftgits msftgits transferred this issue from dotnet/coreclr Jan 31, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 14, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
Projects
None yet
Development

No branches or pull requests

2 participants