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

Reduce asm size of CollectionsMarshal.AsSpan #96773

Merged
merged 4 commits into from
Jan 11, 2024

Conversation

stephentoub
Copy link
Member

Using the span constructor brings with it some branches and code we don't need due to knowledge of List<T>'s implementation, in particular that a) it always has an array (even if empty), and b) its array is invariant.

Erroneous use of AsSpan while the list is being mutated could have also led to an ArgumentOutOfRangeException from the span's constructor, which doesn't make much sense in this context. I've changed it to be an InvalidOperationException about the invalid concurrent use.

Method Toolchain Code Size
AsSpan \main\corerun.exe 99 B
AsSpan \pr\corerun.exe 58 B
Sum \main\corerun.exe 112 B
Sum \pr\corerun.exe 75 B
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Runtime.InteropServices;

BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);

[DisassemblyDiagnoser]
public class Tests
{
    private readonly List<string> _list = new() { "a", "b", "c" };

    [Benchmark]
    public Span<string> AsSpan() => CollectionsMarshal.AsSpan(_list);

    [Benchmark]
    public int Sum()
    {
        int total = 0;
        foreach (string s in CollectionsMarshal.AsSpan(_list))
        {
            total += s.Length;
        }
        return total;
    }
}

Using the span constructor brings with it some branches and code we don't need due to knowledge of `List<T>`'s implementation, in particular that a) it always has an array (even if empty), and b) its array is invariant.

Erroneous use of AsSpan while the list is being mutated could have also led to an ArgumentOutOfRangeException, which doesn't make much sense in this context. I've changed it to be an InvalidOperationException about the invalid concurrent use.
@ghost
Copy link

ghost commented Jan 10, 2024

Tagging subscribers to this area: @dotnet/interop-contrib
See info in area-owners.md if you want to be subscribed.

Issue Details

Using the span constructor brings with it some branches and code we don't need due to knowledge of List<T>'s implementation, in particular that a) it always has an array (even if empty), and b) its array is invariant.

Erroneous use of AsSpan while the list is being mutated could have also led to an ArgumentOutOfRangeException from the span's constructor, which doesn't make much sense in this context. I've changed it to be an InvalidOperationException about the invalid concurrent use.

Method Toolchain Code Size
AsSpan \main\corerun.exe 99 B
AsSpan \pr\corerun.exe 58 B
Sum \main\corerun.exe 112 B
Sum \pr\corerun.exe 75 B
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Runtime.InteropServices;

BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);

[DisassemblyDiagnoser]
public class Tests
{
    private readonly List<string> _list = new() { "a", "b", "c" };

    [Benchmark]
    public Span<string> AsSpan() => CollectionsMarshal.AsSpan(_list);

    [Benchmark]
    public int Sum()
    {
        int total = 0;
        foreach (string s in CollectionsMarshal.AsSpan(_list))
        {
            total += s.Length;
        }
        return total;
    }
}
Author: stephentoub
Assignees: stephentoub
Labels:

area-System.Runtime.InteropServices

Milestone: -

@jkotas
Copy link
Member

jkotas commented Jan 10, 2024

Why is the JIT not able to optimize the original code well?

cc @dotnet/jit-contrib

@stephentoub
Copy link
Member Author

Why is the JIT not able to optimize the original code well?

For starters I assume because it doesn't know that it can elide the null check on the array and elide the variance check on the array, since it's not privvy to the information about how the list was constructed.

@EgorBo
Copy link
Member

EgorBo commented Jan 10, 2024

For the variance check, minimal repro:

public class MyList<T>
{
    public T[] arr;
}

bool Test(MyList<string> list) => list.arr.GetType() == typeof(string[]);

codegen:

; Method Program:Foo(MyList`1[System.String]):ubyte:this (FullOpts)
G_M55733_IG01:
G_M55733_IG02:
       mov      rax, gword ptr [rdx+0x08]
       mov      rcx, 0xD1FFAB1E      ; System.String[]
       cmp      qword ptr [rax], rcx
       sete     al
       movzx    rax, al
G_M55733_IG03:
       ret      
; Total bytes of code: 24

I bet it lost generic context somewhere during opts..

Querying runtime about current class of field MyList`1[System.__Canon]:arr (declared as System.__Canon[])
Field's current class not available


[000010] ---XG------                         *  RETURN    int   
[000009] ---XG------                         \--*  EQ        int   
[000008] #--XG------                            +--*  IND       long  
[000002] n--XG------                            |  \--*  IND       ref   
[000001] ---X-------                            |     \--*  FIELD_ADDR byref  MyList`1[System.__Canon]:arr
[000000] -----------                            |        \--*  LCL_VAR   ref    V00 arg0         
[000004] H----------                            \--*  CNS_INT(h) long   0xd1ffab1e class

@stephentoub
Copy link
Member Author

For the variance check

Even if that's fixed for string, though, I imagine the JIT would have a lot more trouble doing it for an arbitrary non-sealed reference type.

@EgorBo
Copy link
Member

EgorBo commented Jan 10, 2024

For the variance check

Even if that's fixed for string, though, I imagine the JIT would have a lot more trouble doing it for an arbitrary non-sealed reference type.

Sure, but looks like it's should be a general goodness to improve this in JIT too if possible

@stephentoub
Copy link
Member Author

Sure, but looks like it's should be a general goodness to improve this in JIT too if possible

Agreed. Just commenting that this change still improves upon what I imagine the JIT will be able to safely do, unless we build knowledge of List<T> into it directly.

@jkotas
Copy link
Member

jkotas commented Jan 10, 2024

this change still improves upon what I imagine the JIT will be able to safely

Yea, I agree. LGTM.

@EgorBo
Copy link
Member

EgorBo commented Jan 10, 2024

For the variance check

Even if that's fixed for string, though, I imagine the JIT would have a lot more trouble doing it for an arbitrary non-sealed reference type.

Sure, but looks like it's should be a general goodness to improve this in JIT too if possible

Handled via #96794

stephentoub and others added 2 commits January 10, 2024 13:29
…pServices/CollectionsMarshal.cs

Co-authored-by: Jan Kotas <jkotas@microsoft.com>
@stephentoub stephentoub merged commit 4083523 into dotnet:main Jan 11, 2024
176 of 178 checks passed
@stephentoub stephentoub deleted the shrinkasspan branch January 11, 2024 12:45
tmds pushed a commit to tmds/runtime that referenced this pull request Jan 23, 2024
* Reduce asm size of CollectionsMarshal.AsSpan

Using the span constructor brings with it some branches and code we don't need due to knowledge of `List<T>`'s implementation, in particular that a) it always has an array (even if empty), and b) its array is invariant.

Erroneous use of AsSpan while the list is being mutated could have also led to an ArgumentOutOfRangeException, which doesn't make much sense in this context. I've changed it to be an InvalidOperationException about the invalid concurrent use.

* Update src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/CollectionsMarshal.cs

* Update src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/CollectionsMarshal.cs

Co-authored-by: Jan Kotas <jkotas@microsoft.com>

---------

Co-authored-by: Jan Kotas <jkotas@microsoft.com>
@github-actions github-actions bot locked and limited conversation to collaborators Feb 11, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants