C#, как происходит возвращение значимого типа из метода на уровне памяти(stack)
В книге Рихтера наткнулся на следующее строки: "Повторюсь: для метода, возвращающего значимый тип, поля экземпляра копируются в память,выделенную вызывающим кодом в месте возврата из метода, что снижает эффективность работы программы."
public static void Main()
{
MyStruct myStruct = new MyStruct();
double result = myStruct.RadiusVector(3);
}
struct MyStruct
{
double x = 9;
public double RadiusVector(double z)
{
double y = z * 2;
return Math.Sqrt(x * x + y * y + z * z);
}
}
На примере этого кода хотелось бы узнать: что именно скопируется в память, выделенную вызывающим кодом в месте возврата из метода и как весь этот код будет лежать в стеке? (если я правильно понял, то после отработки метода RadiusVector в стеке, во фрейме метода Main будут лежать три переменные: x, y и result каждая по 8 байт? поправьте меня)
Ответы (1 шт):
После возврата из метода в стеке будет лежать две переменные myStruct 8 байт и result 8 байт, но это не точно. Код не лежит в стеке никогда, код лежит в кеше процессора и выполняется.
К примеру, у вас 64-битное приложение, тогда первые 3 аргумента не кладутся в стек вообще, а передаются через регистры процессора. Логически весь код будет работать со стеком так:
0 пустой стек
8 push myStruct
16 call RadiusVector (push текущий адрес возврата)
24 push y
здесь математика
16 rsp-8 (очистка стека RadiusVector)
8 ret (pop текущий адрес возврата)
16 push result
Но это если код не оптимизирован.
Давайте посмотрим на оптимизированный код, я только ваш код переписал так, чтобы он комиилировался.
public class Program
{
public void Main()
{
MyStruct myStruct = new MyStruct(9);
double result = myStruct.RadiusVector(3);
Console.WriteLine(result);
}
}
struct MyStruct
{
double x;
public MyStruct(double x)
{
this.x = x;
}
public double RadiusVector(double z)
{
double y = z * 2;
return Math.Sqrt(x * x + y * y + z * z);
}
}
А вот во что он компилируется
Program.Main()
L0000: sub rsp, 0x28 ; выделить 28 байт в стеке
L0004: vzeroupper
L0007: vmovsd xmm0, [0x7ff9052d0488]
L000f: vmovsd [rsp+0x20], xmm0
L0015: vmovsd xmm1, [rsp+0x20]
L001b: vmulsd xmm1, xmm1, xmm1
L001f: vaddsd xmm1, xmm1, [0x7ff9052d0490]
L0027: vaddsd xmm0, xmm1, xmm0
L002b: vsqrtsd xmm0, xmm0, xmm0
L002f: call 0x00007ff8fc64f840 ; Console.WriteLine
L0034: nop
L0035: add rsp, 0x28 ; очистить 28 байт в стеке
L0039: ret
MyStruct..ctor(Double)
L0000: vzeroupper
L0003: vmovsd [rcx], xmm1 ; this.x = x;
L0007: ret
MyStruct.RadiusVector(Double)
L0000: vzeroupper
L0003: vaddsd xmm0, xmm1, xmm1 ; z + z
L0007: vmovsd xmm2, [rcx] ; xmm2 = this.x;
L000b: vmulsd xmm2, xmm2, xmm2 ; x * x
L000f: vmulsd xmm0, xmm0, xmm0 ; y * y
L0013: vaddsd xmm0, xmm2, xmm0 ; +
L0017: vmulsd xmm1, xmm1, xmm1 ; z * z
L001b: vaddsd xmm0, xmm0, xmm1 ; +
L001f: vsqrtsd xmm0, xmm0, xmm0 ; Math.Sqrt
L0023: ret
В ассемблере ; - это коммертарий, если что. xmm* регистры процессора типа double. rsp - адрес стека. rcx - регистр общего назначения. Еще по конвенции fastcall для x64 приложений, первый аргумент метода с плавающей точкой передается через xmm1, а возвращаемое значение - через xmm0.
Обратите внимание, JIT компилятор вообще заинлайнил код метода RadiusVector внутрь Main. Стек вообще не используется.
Кстати вот это использование стека
L000f: vmovsd [rsp+0x20], xmm0
L0015: vmovsd xmm1, [rsp+0x20]
Но выглядит как баг компилятора, точнее не баг, а недостаточная оптимизация, так как эти 2 инструкции легко заменить на одну
L000f: vmovsd xmm1, xmm0
Если без этой оптимизации, то в стеке всего 8 байт данных, если с оптимизацией, то 0.
Отсюда вывод. Ничего общего на настоящимой момент между содержимым книжки и реальности я не вижу. Не верьте всему, что пишут, везде есть нюансы.