Научно-образовательный IT-форум

Объявление

Консультации по C# --> ссылка | Новостная лента форума --> ссылка | Пожертвования (донаты) --> ссылка

Информация о пользователе

Привет, Гость! Войдите или зарегистрируйтесь.


Вы здесь » Научно-образовательный IT-форум » Задачи и вопросы » [-] Глубина рекурсивного вызова для геттера и сеттера в C#


[-] Глубина рекурсивного вызова для геттера и сеттера в C#

Сообщений 1 страница 8 из 8

1

Почему глубина рекурсивного вызова может различаться для геттера и сеттера в C#?

Код:
static int counter = 0;
static void Main(string[] args)
{
	//int x = TestVar;
	TestVar = 1;
}

static int TestVar
{
	//get { Console.WriteLine(++counter); return TestVar; } //12649
	set { Console.WriteLine(++counter); TestVar = value; } //15823
}


В комментарии указал, на какой глубине возникло StackOverflowException для геттера и сеттера, соответственно. Как видим, почти на 3 тысячи значение отличается, с чего бы это? :)

2

Заглянем в IL-код:

Код:
IL_0000:  nop         
IL_0001:  call        UserQuery.get_TestVar
IL_0006:  stloc.0     // x
IL_0007:  ret         

get_TestVar:
IL_0000:  nop         
IL_0001:  ldsfld      UserQuery.counter
IL_0006:  ldc.i4.1    
IL_0007:  add         
IL_0008:  dup         
IL_0009:  stsfld      UserQuery.counter
IL_000E:  call        System.Console.WriteLine
IL_0013:  nop         
IL_0014:  call        UserQuery.get_TestVar
IL_0019:  stloc.0     
IL_001A:  br.s        IL_001C
IL_001C:  ldloc.0     
IL_001D:  ret  


для

Код:
static int counter = 0;
static void Main(string[] args)
{
	int x = TestVar;
	//TestVar = 1;
}

static int TestVar
{
	get { Console.WriteLine(++counter); return TestVar; } //12649
	//set { Console.WriteLine(++counter); TestVar = value; } //15823
}


И заглянем в IL-код:

Код:
IL_0000:  nop         
IL_0001:  ldc.i4.1    
IL_0002:  call        UserQuery.set_TestVar
IL_0007:  nop         
IL_0008:  ret         

set_TestVar:
IL_0000:  nop         
IL_0001:  ldsfld      UserQuery.counter
IL_0006:  ldc.i4.1    
IL_0007:  add         
IL_0008:  dup         
IL_0009:  stsfld      UserQuery.counter
IL_000E:  call        System.Console.WriteLine
IL_0013:  nop         
IL_0014:  ldarg.0     
IL_0015:  call        UserQuery.set_TestVar
IL_001A:  nop         
IL_001B:  ret  


для

Код:
static int counter = 0;
static void Main(string[] args)
{
	//int x = TestVar;
	TestVar = 1;
}

static int TestVar
{
	//get { Console.WriteLine(++counter); return TestVar; } //12649
	set { Console.WriteLine(++counter); TestVar = value; } //15823
}

3

Потому что по своей сути грубо говоря get является "методом" который возвращает значение определенного типа (в данном случае int), а set является "методом" типа void который просто присваивает значение.
Если мы создадим две рекурсивные функции, где одна возвращает себя же но со значением, а вторая просто вызывает себя, то получим аналогичный результат.

Код:
    static int foo(int c)
    {
    	c++;
    	Console.WriteLine(c);
    	return foo(c);
    }

    static int counter = 0;
    static void foo()
    {
    	counter++;
    	Console.WriteLine(counter);
    	foo();
    }


Так откуда же разница в целых три тысячи? Все просто. Вызывая методы рекурсивно, мы заносим данные в стек, а размер его ограничен. Если допустим тип данных будет не int, а BigInteger или даже string, то стек переполнится быстрее из-за более "жирного" типа данных

Код:
    static BigInteger foo(BigInteger c)
    {
    	c++;
    	Console.WriteLine(c);
    	return foo(c);
    }


Код:
    static string foo(string c)
    {
    	c+="0";
    	Console.WriteLine(c.Length);
    	return foo(c);
    }


Код:
    	foo(); //19000
    	foo((int)1); //15000
    	foo((BigInteger)1); //5000
    	foo(""); //11000


Башев Даниил

Отредактировано MegaLoogin (2020-02-25 02:05:28)

4

Инкапсуляция приветик🙂. Сама инкапсуляция нужна для такого рода фильтра между значением переменной и выводом этого значения. Что бы пользователь не мог своими ручками поменять значение, там где ему это не нужно. То есть мы значение переменной задаём в сет, и делает гет с активацией функции сет, и когда нужно у нас значение функции проходит через всю эту карусель и выдает результат. Почему же разные варианты, аж на 3к. Мое первое мышление знает что геттеры и сеттеры нужны прямо внутри функции, а не после нее. И то есть первый поток значений мог пройти уже не дойдя до геттеров. Второе мышление подсказывает что мы вообще закомметировали геттеры , а они без друг друга, как я без зачета, живётся им плохо. И пользователь перехватил данные.

5

Иван Квашнин написал(а):

То есть мы значение переменной задаём в сет, и делает гет с активацией функции сет, и когда нужно у нас значение функции проходит через всю эту карусель и выдает результат. Почему же разные варианты, аж на 3к. Мое первое мышление знает что геттеры и сеттеры нужны прямо внутри функции, а не после нее. И то есть первый поток значений мог пройти уже не дойдя до геттеров.

get и set это совершенно разные функции. Т.к. get в конечном итоге возвращает значение из оперативной памяти, а set заносит значение.
И на заполненность стека насколько я знаю влияет только две вещи: вызывание функцией саму себя и очень большие размеры переменных.
Если мы допустим сделаем бесконечный цикл, то такой ошибки (переполнения стека) вроде не будет

Иван Квашнин написал(а):

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

Методы доступа (get и set) независимы от друг друга.

По своей сути это:

Код:
private int Num;
public int num{
   get{return Num;};
   set{Num = value};
}


можно заменить этим:

Код:
private int Num;
public void SetNum(int value){
   Num = value;
}

public int GetNum(){
   return Num;
}


И если мы один из методов опустим то получим read/write-only доступ к переменной.

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

6

Проанализировав некоторые источники, попробую ответить на вопрос.

Стек - это область оперативной памяти, которая создаётся для каждого потока. Он работает в порядке LIFO (Last In, First Out),  то есть последний добавленный в стек кусок памяти будет первым в очереди на вывод из стека. Каждый раз, когда функция объявляет новую переменную, она добавляется в стек, а когда эта переменная пропадает из области видимости (например, когда функция заканчивается), она автоматически удаляется из стека. Когда стековая переменная освобождается, эта область памяти становится доступной для других стековых переменных. Из-за такой природы стека управление памятью оказывается весьма логичным и простым для выполнения на ЦП; это приводит к высокой скорости, в особенности потому, что время цикла обновления байта стека очень мало, т.е. этот байт скорее всего привязан к кэшу процессора. Тем не менее, у такой строгой формы управления есть и недостатки. Размер стека - это фиксированная величина, и превышение лимита выделенной на стеке памяти приведёт к переполнению стека. Размер задаётся при создании потока, и у каждой переменной есть максимальный размер, зависящий от типа данных. Это позволяет ограничивать размер некоторых переменных (например, целочисленных), и вынуждает заранее объявлять размер более сложных типов данных (например, массивов), поскольку стек не позволит им изменить его. Кроме того, переменные, расположенные на стеке, всегда являются локальными. [1]

Большая часть кода, который мы пишем, инкапсулирован в классы и методы, которые вызывают другие методы, и так далее. .NET Framework обязан всегда "помнить" порядок вызовов участков кода. Более того, так же нужно хранить данные о состоянии переменных и значениях параметров, передаваемых при вызове методов (дабы суметь восстановить состояние вызывающего метода после завершения работы вызываемого). [2]

При каждом вызове метода .NET инициализирует стек-фрейм (что-то вроде контейнера), где и хранится вся необходимая информация для выполнения методов: параметры, локальные переменные, адреса вызываемых строчек кода. Стек-фреймы создаются в стеке друг на друге. Все это прекрасно проиллюстрировано ниже:

https://bitbucket.org/landwatersun/forum/downloads/202002280000.jpg

Стек используется для хранения порядка выполнения кода и часто называется стеком вызова, стеком выполнения или программным стеком.

Давайте взглянем на следующий участок кода:

https://bitbucket.org/landwatersun/forum/downloads/202002280001.jpg

Дабы вызвать Method2, фреймворк должен сохранить адрес конца выполнения метода, которым будет следующая строчка кода (строчка 4 в примере ниже). Этот адрес вместе с параметрами и локальными переменными вызываемого и вызывающего метода хранятся в стеке вызова, как показано на схеме ниже.

https://bitbucket.org/landwatersun/forum/downloads/202002280002.jpg

Также можно увидеть, что происходит, когда Method3 завершает свое выполнение (стек-фрейм покидает стек вызова).

Если мы заглянем краем глаза на еще более низкий уровень, то узнаем или же вспомним что память на самом деле является виртуальной и что она поделена на страницы объемом 4 КБ. Каждая такая страница может физически существовать или же нет. А если она существует, то может быть отображена на файл или же реальную оперативную память. Именно этот механизм виртуализации позволяет приложениям иметь раздельную друг от друга память и обеспечивает уровни безопасности между приложением и операционной системой. При чем же здесь стек потока? Как и любая другая оперативная память приложения стек потока является ее частью и также состоит из страниц объемом 4 КБ. По краям от выделенного для стека пространства находятся страницы, доступ к которым приводит к системному исключению, сообщая операционной системе о том что приложение пытается обратиться к не выделенному участку памяти. Внутри этого региона реально выделенными участками являются только те страницы, к которым обратилось приложение: т.е. если приложение резервирует под поток 2МБ памяти это не значит что они будут выделены сразу же. Отнюдь, они будут выделены по требованию: если стек потока вырастет до 1 МБ, это будет означать что приложение получило именно 1 МБ оперативной памяти под стек. [3]

Поэтому, когда вы напишете рекурсивный метод, который уходит в бесконечную рекурсию, вы получите StackOverflowException: заняв всю выделенную под стек память (весь доступный регион), вы напоритесь на тип страницы - Guard Page, доступ к которой вызовет нотификацию операционной системы, которая инициирует StackOverflow уровня ОС, которое уйдет в .NET, будет перехвачено и выбросится исключение StackOverflowException для .NET приложения.

Анализируя код с сеттером

Код:
static void Main(string[] args)
{
	TestVar = 1;
}

static int TestVar
{
	set { TestVar = value; }
}

, который можно рассматривать как метод принимающий параметр value, стековый указатель смещался на 0x7 при каждом рекурсивном вызове.

В случае кода геттера

Код:
static void Main(string[] args)
{
	int x = TestVar;	
}

static int TestVar
{
	get { return TestVar; }
}

, который можно рассматривать как метод без параметров, стековый указатель смещался на 0x1 при каждом рекурсивном вызове (т.е. сохранения данных в стек-фрейме не требовалось).

Таким образом, стековая память в случае сеттера должна расти быстрее, снижая допустимое число рекурсивных вызовов. Однако при запусках сеттера и геттера я всякий раз получал различные значения глубины рекурсивного спуска. Причем, чем меньше интервал времени между запусками указанных программ, тем меньше - число допустимых рекурсивных вызовов. Объяснить данное поведение программ затруднительно, пока мне не удалось найти источников с детальным описанием организации управления платформой .NET областью памяти, выделяемой под хранение стек-фреймов. Кое-что написано здесь https://habr.com/ru/post/221861/ , но сомневаюсь в авторитетности источника.

7

У меня есть предположение, что операционная система, поверх которой реализована общеязыковая инфраструктура платформы .NET, предоставляет потокам под хранение стек-фреймов различные размеры памяти. В результате имеем разные глубины  рекурсивного спуска. Отчего зависит этот выделяемый размер памяти, требует отдельного изучения.  Сам я рекурсией никогда не пользуюсь, но для тех, кто это делает, рекомендую контролировать глубину рекурсии – если глубина рекурсии превышает некий порог, то выполнять другую ветку кода.

8

Код с сеттером, который можно рассматривать как метод принимающий параметр value, стековый указатель смещался на 0x7 при каждом рекурсивном вызове.
В случае кода геттера, , который можно рассматривать как метод без параметров, стековый указатель смещался на 0x1 при каждом рекурсивном вызове (т.е. сохранения данных в стек-фрейме не требовалось).


Неделю пытался выяснить: почему в вашем случае глубина рекурсии для get и set разная, так как в моем случае разница была всего лишь в несколько единиц?
https://i.ibb.co/b5pJFbt/asd.png

Затем я попытался задать вручную размер стека для потока:

Код:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Threading;
namespace Task_About_Memory
{
    class RecursionLimit
    {
        static int counter = 0;
        static void Main(string[] args)
        {
            var stackSize = 10000000;
            Thread thread = new Thread(new ThreadStart(Count), stackSize);
            thread.Start();

        }
        public static int TestVar
        {
            // get { Console.WriteLine(++counter); return TestVar; } //156350
            set { Console.WriteLine(++counter); TestVar = value; } //156350
        }
        public static void Count()
        {
           // int x = TestVar; 
            TestVar = 1; //
        }
    }
}

https://i.ibb.co/wLH46DG/full-thread.png

И в этом случае у меня глубина рекурсии одинакова и для геттера и для сеттера :sceptic:  :suspicious:
Я предполагаю, что геттер и сеттер занимают одинаковое количество памяти, т.к. при фиксированном размере стэка глубина рекурсии одинкова. А то что под стэк по-умолчанию выделяется 1 МБ, у меня возникли сомнения. Видимо под поток все-таки выделяется всегда разное количество памяти.

После всего этого самому стало интересно, буду следить за развитием событий)

Отредактировано Emil F (2020-03-28 22:02:09)

Быстрый ответ

Напишите ваше сообщение и нажмите «Отправить»



Вы здесь » Научно-образовательный IT-форум » Задачи и вопросы » [-] Глубина рекурсивного вызова для геттера и сеттера в C#