Страницы

Поиск по вопросам

вторник, 28 января 2020 г.

Скорость выполнения при передаче ссылки на объект

#c_sharp


Добрый день!
Прошу Вас подсказать мне.
Вопрос: какой код будет выполняться быстрее и почему? 
Или разницы в скорости не будет, т.к передается ссылка на область в куче.

Допустим, есть некоторый тип:

class MyClass
{
    internal int a;
    public MyClass(int a)
    {
        this.a = a;
    }
}


Вариант 1

class Program
{
    static void Main(string[] args)
    {
        MyClass myClass = new MyClass(5);
        Work(myClass.a);            
    }

    static void Work(int a)
    {
        int b = a + 5;
        Console.WriteLine(b);
    }
}


Вариант 2

class Program
{
    static void Main(string[] args)
    {
        MyClass myClass = new MyClass(5);
        Work(myClass);            
    }

    static void Work(MyClass myClass)
    {
        int b = myClass.a + 5;
        Console.WriteLine(b);
    }
}

    


Ответы

Ответ 1



Все значения переменных передаются по значению:в ссылочных типах значение это ссылка, в значимых - значение. Конкретно в вашем случае: пример 1 Work(myClass.a); - Вы обращаетесь к куче и копируете значение из кучи, далее уже работаете с локальной переменной, которая будет храниться в стеке пример 2 Work(myClass); - Вы обращаетесь к переменной myClass и копируете ее значение, т.е. ссылку на область памяти, где хранится объект. В данном случае объектом является класс, поэтому и хранится он в куче. Далее в методе Вы обращаетесь уже к локальной переменной, хранящейся в куче, для извлечения адреса памяти и извлечения значения объекта. На первый взгляд разница очевидна: передавать по значению проще, но представьте ситуацию, когда вам надо передать миллион значений, а использовать только 1. Тогда быстрее будет передать массив ссылок, чем массив значений. Если посмотреть на Ваш случай, то разницы практически не видно, но первый метод будет работать быстрее. О том, где хранятся переменные, можно прочитать здесь

Ответ 2



Честнее всего будет ответить на ваш вопрос - "невозможно предсказать, возьмите и померяйте на реальном коде". И вот почему: Вот IL, в который компилируются ваши примеры: IL1: Main IL_0006: ldc.i4.5 IL_0007: newobj instance void ConsoleApplication15.MyClass::.ctor(int32) IL_000c: ldfld int32 ConsoleApplication15.MyClass::a IL_0011: call void ConsoleApplication15.Program::Work(int32) Work IL_0000: ldarg.0 IL_0001: ldc.i4.5 IL_0002: add IL_0003: call void [mscorlib]System.Console::WriteLine(int32) IL_0008: ret IL2: Main IL_0006: ldc.i4.5 IL_0007: newobj instance void ConsoleApplication15.MyClass::.ctor(int32) IL_000c: call void ConsoleApplication15.Program::Work(class ConsoleApplication15.MyClass) Work IL_0000: ldarg.0 IL_0001: ldfld int32 ConsoleApplication15.MyClass::a IL_0006: ldc.i4.5 IL_0007: add IL_0008: call void [mscorlib]System.Console::WriteLine(int32) IL_000d: ret Ок, что видно из этого кода? Компилятор уже немного оптимизировал выполнение. Например, в IL1.Main он пользуется удачным совпадением - параметры в функции передаются через стек, результат вызова конструктора также складывается в стек - а значит для передачи myClass в качестве параметра Work никаких телодвижений делать не нужно. Т.е. того, что видно в C# коде: Вы обращаетесь к переменной myClass и копируете ее значение, т.е. ссылку на область памяти, где хранится объект В IL не происходит. Но все равно - явно видно, что в первом примере кода меньше. А значит можно сделать вывод, что он работает быстрее. Но на самом деле из IL выводы делать нельзя. И вот почему: IL код не выполняется напрямую. Он компилируется в машинный код под конкретную платформу. А в конкретной платформе кроме стека могут использоваться и другие механизмы передачи параметров В реальности передача параметров может происходить не через железный стек. Более того, в CLR на x86/x64 первые два параметра передаются через регистры rcx/rdx (ecx/edx). А результат из метода возвращается через rax/eax Вот во что реально компилируется ваш код на x64 (на моей машине): ASM1: Main MyClass myClass = new MyClass(5); 00007FF8F6F20489 mov rcx,7FF8F6E15A88h 00007FF8F6F20493 call 00007FF956512510 00007FF8F6F20498 mov dword ptr [rax+8],5 Work(myClass.a); 00007FF8F6F2049F mov ecx,dword ptr [rax+8] 00007FF8F6F204A2 call 00007FF8F6F20080 Work: 00007FF8F6F204D4 add ecx,5 00007FF8F6F204D7 call 00007FF954497BE0 ASM2: Main: MyClass myClass = new MyClass(5); 00007FF8F6EF0489 mov rcx,7FF8F6DE5A98h 00007FF8F6EF0493 call 00007FF956512510 00007FF8F6EF0498 mov rcx,rax 00007FF8F6EF049B mov dword ptr [rcx+8],5 Work(myClass); 00007FF8F6EF04A2 call 00007FF8F6EF0088 Work 00007FF8F6F204D4 mov ecx,dword ptr [rcx+8] 00007FF8F6F204D7 add ecx,5 00007FF8F6F204DA call 00007FF954497BE0 Ок, чем это отличается от IL? Тем, что стек не используется. Совсем. Оптимизатор все запихнул в регистры. Чем реально отличаются варианты? Одной инструкцией: 00007FF8F6EF0498 mov rcx,rax mov из регистра в регистр занимает 1 такт процессора. Т.е. если какой-то из примеров и будет работать быстрее на x64 - вы этого скорее всего не заметите. Производительность зависит от слишком многих параметров - платформы, версии JIT, типа компиляции - и невозможно учесть их все при написании кода. Не заморачивайтесь. Решайте проблемы по мере их появления.

Комментариев нет:

Отправить комментарий