Почему блок C# finally вызывает дополнительный вызов метода? - PullRequest
1 голос
/ 11 февраля 2020

Я смотрел на сгенерированный ассемблерный код при создании небольшого класса трассировщика, который должен быть максимально быстрым. Идея состоит в том, чтобы создать класс-обертку, в который метод трассировки входит в отпуск, а когда метод остается, метод покидает трассировку. Этого можно добиться с помощью операторов

public void DoStruct()
{
    using var tmp = Wrapper.Create(nameof(DoStruct));
}

, которые автоматически создадут экземпляр struct Tracer, который располагается в методе exit. Все идет нормально. Теперь давайте посмотрим на генерируемую сборку:

NetCoreJitStruct.User.DoStruct()
Begin 00007FFC91651860, size 61
00007ffc`91651860 push    rbp
00007ffc`91651861 sub     rsp,30h
00007ffc`91651865 lea     rbp,[rsp+30h]
00007ffc`9165186a xor     eax,eax
00007ffc`9165186c mov     qword ptr [rbp-8],rax
00007ffc`91651870 mov     qword ptr [rbp-10h],rsp
00007ffc`91651874 mov     qword ptr [rbp+10h],rcx
00007ffc`91651878 mov rcx,268F07F30D8h
00007ffc`91651882 mov     rcx,qword ptr [rcx]
00007ffc`91651885 call    00007ffc`91650800 (NetCoreJitStruct.Wrapper.Create(System.String) *** Ctor called
00007ffc`9165188a mov     qword ptr [rbp-8],rax
00007ffc`9165188e jmp     00007ffc`91651890
00007ffc`91651890 mov     rcx,rsp
00007ffc`91651893 call    00007ffc`9165189f (NetCoreJitStruct.User.DoClass() *** Dispose Called via extra method Call!
00007ffc`91651898 nop
00007ffc`91651899 lea     rsp,[rbp]
00007ffc`9165189d pop     rbp
00007ffc`9165189e ret
    00007ffc`9165189f push    rbp  ** Dispose wrapper method
    00007ffc`916518a0 sub     rsp,30h
    00007ffc`916518a4 mov     rbp,qword ptr [rcx+20h]
    00007ffc`916518a8 mov     qword ptr [rsp+20h],rbp
    00007ffc`916518ad lea     rbp,[rbp+30h]
    00007ffc`916518b1 lea     rcx,[rbp-8]
    00007ffc`916518b5 call    00007ffc`91650818 (NetCoreJitStruct.Wrapper.Dispose()
    00007ffc`916518ba nop
    00007ffc`916518bb add     rsp,30h
    00007ffc`916518bf pop     rbp
    00007ffc`916518c0 ret

Что я не понимаю, так это почему JIT-компилятор переводит вызов метода dispose в дополнительный метод-оболочку. Поведение то же самое для. NET 4.8 + еще немного неэффективного кода gen. Я проверил, если это проблема с встраиванием метода структуры, но поведение одинаково для классов, которые расположены.

Это самый быстрый, который я могу получить. NET или я пропустил какой-то шаблон, чтобы сделать его более быстрым / более дружественным к JIT?

Компилятор: C# 7+. NET :. NET 4.8 или. NET Core 3.1

Ниже приведен полный исходный код

с использованием System; using System.Collections.Generic; использование System.Diagnostics; using System.Runtime.CompilerServices;

пространство имен NetCoreJitStruct {class Program {

    [MethodImpl(MethodImplOptions.NoInlining)]
    static void Main(string[] args)
    {
        Queue<string> argList = new Queue<string>(args);
        bool useStruct = true;
        bool nofactory = false;
        bool nop = false;
        bool direct = false;

        while (argList.Count > 0)
        {
            string arg = argList.Dequeue().ToLower();
            switch (arg)
            {
                case "-trace":
                    CustomData.IsEnabled = true;
                    break;
                case "-struct":
                    break;
                case "-class":
                    useStruct = false;
                    break;
                case "-direct":
                    direct = true;
                    break;
                case "-nop":
                    nop = true;
                    break;
                case "-nofactory":
                    nofactory = true;
                    break;
                default:
                    Console.WriteLine("NtCoreJitStruct [-trace] [-struct or -class]");
                    return;
            }
        }

        var user = new User();
        user.DoClass_Factory();
        user.DoStruct_Factory();
        user.DoStructTryFinally();
        user.DoStructNoFinally_Factory();
        user.DoStructNoFinally_NoFactory();

        const int Runs = 1500_000_000;
        var sw = Stopwatch.StartNew();

        if (nop) // measure loop overhead
        {
            for (int i = 0; i < Runs; i++)
            {
            }
        }
        else
        {
            if (useStruct)
            {
                if (direct)
                {
                    if (nofactory)
                    {
                        for (int i = 0; i < Runs; i++)
                        {
                            user.DoStructNoFinally_NoFactory();
                        }
                    }
                    else
                    {
                        for (int i = 0; i < Runs; i++)
                        {
                            user.DoStructNoFinally_Factory();
                        }
                    }
                }
                else
                {
                    if (nofactory)
                    {
                        for (int i = 0; i < Runs; i++)
                        {
                            user.DoStruct_NoFactory();
                        }
                    }
                    else
                    {
                        for (int i = 0; i < Runs; i++)
                        {
                            user.DoStruct_Factory();
                        }
                    }
                }
            }
            else
            {
                for (int i = 0; i < Runs; i++)
                {
                    user.DoClass_Factory();
                }
            }
        }

        sw.Stop();
        string scenario = useStruct ? "Struct" : "Class";
        Console.WriteLine($"Scenario: {scenario} NoFactory: {nofactory} Nop: {nop} Direct: {direct} Did execute {Runs:N0} Trace calls in {sw.Elapsed.TotalMilliseconds:F0} ms");
    }
}


class User
{
    [MethodImpl(MethodImplOptions.NoInlining)]
    public void DoClass_Factory()
    {
        using var tmp = CWrapper.Create(nameof(DoClass_Factory));
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    public void DoStruct_Factory()
    {
        using var tmp = Wrapper.Create(nameof(DoStruct_Factory));
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    public void DoStructTryFinally()
    {
        var tmp = Wrapper.Create(nameof(DoStruct_Factory));
        try
        {

        }
        finally
        {
            tmp.Dispose();
        }
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    public void DoStructNoFinally_Factory()
    {
        var tmp = Wrapper.Create(nameof(DoStruct_Factory));
        tmp.Dispose();
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    public void DoStructNoFinally_NoFactory()
    {
        var tmp = new Wrapper(nameof(DoStruct_Factory));
        tmp.Dispose();
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    internal void DoStruct_NoFactory()
    {
        using var tmp = new Wrapper(nameof(DoStruct_NoFactory));
    }
}
public struct Wrapper : IDisposable
{
    CustomData data_;

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Wrapper Create(string input)
    {
        return CustomData.IsEnabled ? new Wrapper(input) : default;
    }

    public Wrapper(string a)
    {
        data_ = CustomData.IsEnabled ? new CustomData(a) : null;
    }

    public void Dispose()
    {
        if (data_ != null)
        {
            data_.Dispose();
        }

    }
}

public class CWrapper : IDisposable
{
    CustomData data_;

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static CWrapper Create(string input)
    {
        return CustomData.IsEnabled ?  new CWrapper(input) : default;
    }

    public CWrapper(string a)
    {
        data_ = CustomData.IsEnabled ? new CustomData(a) : null;
    }

    public void Dispose()
    {
        if (data_ != null)
        {
            data_.Dispose();
        }

    }
}

class CustomData : IDisposable
{
    public static bool IsEnabled;

    string myData;

    public CustomData(string data)
    {
        myData = data;
        Console.WriteLine("Entered method {0}", data);
    }

    public void Dispose()
    {
        Console.WriteLine("Left method {0}", myData);
    }
}

}

Некоторые результаты теста:

JitStruct.exe -nop
Scenario: Struct NoFactory: False Nop: True Direct: False Did execute 1,500,000,000 Trace calls in 404 ms

JitStruct.exe -struct
Scenario: Struct NoFactory: False Nop: False Direct: False Did execute 1,500,000,000 Trace calls in 4837 ms

JitStruct.exe -struct
Scenario: Struct NoFactory: False Nop: False Direct: False Did execute 1,500,000,000 Trace calls in 4832 ms

JitStruct.exe -struct -direct
Scenario: Struct NoFactory: False Nop: False Direct: True Did execute 1,500,000,000 Trace calls in 4146 ms

JitStruct.exe -struct -direct
Scenario: Struct NoFactory: False Nop: False Direct: True Did execute 1,500,000,000 Trace calls in 4156 ms

JitStruct.exe -struct -direct -nofactory
Scenario: Struct NoFactory: True Nop: False Direct: True Did execute 1,500,000,000 Trace calls in 6424 ms

JitStruct.exe -struct -direct -nofactory
Scenario: Struct NoFactory: True Nop: False Direct: True Did execute 1,500,000,000 Trace calls in 6389 ms

JitStruct.exe -struct -direct -nofactory
Scenario: Struct NoFactory: True Nop: False Direct: True Did execute 1,500,000,000 Trace calls in 6417 ms

JitStruct.exe -struct  -nofactory
Scenario: Struct NoFactory: True Nop: False Direct: False Did execute 1,500,000,000 Trace calls in 6063 ms

1 Ответ

0 голосов
/ 11 февраля 2020

то, что вы видите, это стоимость ветвления, если вы удалите if и попробуете, struct затем измените код и попробуйте с классом, вы увидите огромную разницу во времени выполнения

вы можете также поменяйте местами if / else, и вы получите, в основном, обратное тому, что вы в настоящее время получаете

, дублируете for, один для структуры и один для класса, у вас будет другой вид результата


уточнение;

для l oop,

попробуйте, затем скомпилируйте, запустите

        for (int i = 0; i < Runs; i++)
        {
            user.DoStruct();
        }

затем попробуйте, скомпилируйте, запустите

        for (int i = 0; i < Runs; i++)
        {
            user.DoClass();
        }

вы получите другой результат.

Я знаю, что мой ответ не соответствует названию, но я отвечаю на первую строку поста.

...