Форум Pawn-Wiki.Ru - Воплоти мечту в реальность!: Стэк, Сегмент Данный, Куча - Форум Pawn-Wiki.Ru - Воплоти мечту в реальность!

Перейти к содержимому

Страница 1 из 1
  • Вы не можете создать новую тему
  • Вы не можете ответить в тему

[ Урок ]
Стэк, Сегмент Данный, Куча Для всех юзеров портала
Оценка: -----

#1
Пользователь офлайн   Long- 

  • Assassin Of Scripters
  • Вставить ник
  • Раскрыть информацию
Всем привет, с наступающим праздником.
Эта тема для всех пользователей портала, для более опытных пользователей, не на одном портале, вообще в интернете нет объяснениям этим "непонятным" словам.
Будет очень много букаф, лучше не читайте.

И так начнем:

В Pawn есть стэк, куча и сегмент данных.
Стэк и куча один и тот же участок памяти, который заполняется с разных концов , стэк с начала в конец, а куча с конца в начало.
Сегмент данных - вся остальная память.

Стэк

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

Поясню примером:

Скрытый текст [Показать]


Более сложный пример:

Скрытый текст [Показать]


Теперь о том, как рассчитывается размер занятого стэка.

Как уже заметили выше, для каждой функции рассчитывается свой стэк, ибо когда начинается обработка новой функции, обработка предыдущей функции уже 100% завершена, а значит стэк отчищен. Так что, суммировать размер для всех функции бессмысленно.

Как же определяется число, которое выводится при компиляции в информационное окно? А всё просто: ищется функция, в которой больше всего расходуется стэка. Это и будет то самое число. Логично, что если найден самый большой объём данных, значит меньшие объёмы данных точно поместятся в этот большой (по такому же принципу массивы строк работают).

И самое интересное: как происходит подсчёт размера для каждой функции в отдельности:

Для начала стоит отметить, что в стэке хранятся не только переменные, но и информация обо всех функциях, которые вызываются в функции. А именно:

  • 4 байта (1 ячейка) выделяется под адрес функции, который извлекается через funcidx
  • 4 байта выделяется под хранение значения из numargs (эта функция возвращает количество аргументов)
  • 4 байта выделяется под адрес кода, откуда была вызвана функция (адрес коллбэка и т.п.)
  • 0..128 байт выделяется под аргументы (максимально - 32 аргумента в одной функции)


Эти данные хранятся в стэке ровно до того момента, пока функция не вернёт какого-либо значения. Только вернула - данные удаляются из стэка.
Но это не значит, что данные выделяются под каждую функцию. Совсем нет. Тут действует тот же принцип стопки тарелок, по которому компилятор находит максимально прожорливую функцию (функцию с наибольшим числом аргументом) и уже "игнорирует" все остальные.

Теперь переменные. Как я уже показал в одном из примеров выше, переменные (массив - тоже переменная. Точнее, группа переменных) существуют только в том блоке кода, в котором они объявлены. А значит, что для таких переменных действует то же правило, по которому рассчитывается максимальный размер стэка в функциях.

Объясню на примере:

Скрытый текст [Показать]


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

И еще одна байка "Ааа, возвращать строки нельзя", рассмотрим.

Возвращать строки однозначно можно. Просто стоит учитывать особенность обработки таких функций. Возьмём в пример тот же пустой main и создадим функцию, которая будет возвращать массив

Скрытый текст [Показать]


И в завершение о стэке я бы хотел рассказать о переполнении. Многие говорят, что ничего страшного от переполнения не случится. Мол в RLS было переполнение и ничего, всё прекрасно работало.
Давайте составим простой пример, по которому вам сразу станет ясно как всё работает:

native print(const string[]);
main()
{
    new toggle = 1;
    if(toggle == 1)
    {
    
    }
    else
    {
        new string[10000];
        if(string[0] == 0) string[0] = 0;
    }
    print("\n\nКод успешно сработал!\n\n");
}


Видим следующее:

Stack/heap size:      16384 bytes; estimated max. usage=10004 cells (40016 bytes)


"16384 bytes" означает то, сколько байт доступно для стэка на данный момент, а "10004 cells (40016 bytes)" - то, сколько занято.
16384 явно меньше, нежели 40016, а значит поприветствуем переполнение. Но давайте попробуем запустить такой скрипт.

Если запустить данный скрипт, то проблем никаких не будет.
"Но ведь есть переполнение!" - скажите вы - "Почему тогда всё работает!?".
А всё просто. Так как локальные переменные существуют только тогда, когда обрабатывается "их" блок кода, наш массив, вызвавший переполнение, просто не инициализируется, так как наше условие сработает и обработается пустой блок.
Теперь давайте сделаем так, чтоб сработал блок с переполнением. Для этого "toggle" приравняем к нулю.

native print(const string[]);
main()
{
    new toggle = 0;
    if(toggle == 1)
    {
    
    }
    else
    {
        new string[10000];
        if(string[0] == 0) string[0] = 0;
    }
    print("\n\nКод успешно сработал!\n\n");
}


И посмотрите что получится.
Получим в консоль сообщение: "Stack/heap collision (insufficient stack size)".
Получилось переполнение стэка, из-за которого выполнение функции "main" прекратилось ровно на той строке, где случилось переполнение, и, соответственно, весь остальной код (в нашем случае это сообщение в консоль) так же не был обработан.

Именно поэтому в том же самом RLS всё более-менее работало и с переполнением. Но и именно поэтому в RLS были баги: код, в котором было переполнение, срабатывал далеко не всегда, но когда срабатывал, он обрывал выполнение обрабатываемой функции => рушил логику скрипта => образовывал баг.

Так что не стоит пренебрегать переполнением. Расходуйте память с умом.


И да, ещё хотелось бы сказать про "#pragma dynamic".
Данная директива используется для увеличения блока, в котором содержатся стек и куча.
Использование этой директивы существенно никак не повлияет на работу вашего сервера (ну разве что вы не выделите больше памяти для стэка, чем вообще доступно на хостинге для сервера). Использование данной директивы осуждается сообществом лишь потому, что она подразумевает, что скриптер совершенно халатно относился к выделению памяти в локальных переменных (выделяя, например, 1000 ячеек для строки в 300 ячеек), а подобный подход - не есть хорошо. SA-MP довольно примитивен по предоставленным возможностям и в нём довольно легко уложиться в стандартный размер стэка - 16384 байта (4096 ячеек). Поэтому эта директива и не в почёте.

Куча

На куче я особо останавливаться не буду.
Главное, что вы должны о ней знать, так это то, что она является одним и тем же сегментом данных, что и стэк. Просто они заполняются с разных концов (это особенность Pawn).
А так же вы должны понимать, что все глобальные переменные помещаются не в кучу, а в сегмент данных.
Кучей можно оперировать lctrl/sctrl инструкциями. Данные в кучу помещаются относительно нечасто и в довольно специфических случаях: хранение промежуточных данных при операциях (сложение/умножение и пр.) и тому подобное.

Сегмент данных

Если не вдаваться в подробности и описывать всё максимально просто, то сегментом данных можно считать всю остальную часть памяти, что не выделена под стэк и кучу.
В сегмент данных помещаются все глобальные переменные/константы и прочие данные, которые известны на этапе компиляции.
Так же в сегмент данных помещаются все строки, что используются в функциях, присваиваются локальным массивам при инициализации и т.п.
То бишь, когда вы пишите так

new string[100] = "Привет";
SendClientMessage(playerid, -1, "Hello");
format(string, sizeof(string), "%s мир!", string);


вы прописываете 3 неявных объявления глобальных массивов: "Привет", "Hello" и "%s мир!".

То бишь, такие строки были бы идентичны такой записи

static     message_1[] = "Привет",
        message_2[] = "Hello",
        message_3[] = "%s мир!";
new string[100] = message_1;
SendClientMessage(playerid, -1, message_2);
format(string, sizeof(string), message_3, string);


Оба случая выделят одинаковое количество памяти как в стэке, так и в сегменте данных.

Именно по этому тексту я учился, и познавал что-то новое.
Хочу поделиться и с вами.
Вроде все, спасибо за внимания!

Сообщение отредактировал Long-: 13 Декабрь 2017 - 10:03

6

#2
Пользователь офлайн   SooBad 

  • SB org
  • Вставить ник
  • Раскрыть информацию
Статья сама по себе понравилась, ибо всё написано общедоступно и понятно. Молодец.
Есть пара нюансов, исправлять или нет - выбор за тобой :)
0..256 байт

128 же)
Кучей можно оперировать lctrl/sctrl инструкциями.

Индекс кучи - это всего лишь один из операндов данных инструкций. Думаю, что новички, только осваивающие принцип работы памяти, вряд ли знакомы с ассемблером, либо с его альтернативой в pawn, а вот те, у кого данная статья не вызовет удивления, скорее всего, знают про операторы, предназначенные для ручных манипуляций с компилятором. Я бы сделал отсылки к другим статьям (например, kyousaur'a, на офф.портале), в которых подробно рассказывается о данных инструкциях. (Свою тему не пропиарил, тип)) ).
На протяжение всей темы рассматриваются примеры с участием блоков. По сути, блоки - это лишь структурная часть (почти)любого яп, и на принцип работы куча/стека и т.п. они влияют косвенно, ибо сначала обрабатываются внешние задействованные секреции памяти, и затем внутренние, а неиспользуемые и непроинициализированные массивы/переменные не занимают места в памяти.

Сообщение отредактировал SooBad: 12 Декабрь 2017 - 16:20

1

#3
Пользователь офлайн   Long- 

  • Assassin Of Scripters
  • Вставить ник
  • Раскрыть информацию

Просмотр сообщенияSooBad (12 Декабрь 2017 - 16:15) писал:

Нажмите сюда, чтобы прочитать это сообщение. [Показать]


Просмотр сообщенияSooBad (12 Декабрь 2017 - 16:15) писал:

0..256 байт
128 же)


Ой, верно, спасибо
Про эти инструкции я не знаю вообще, и как ими можно оперировать, просто знаю что ими можно как то оперировать, решил написать, может кто захочет изучит эти инструкции досконально.
Их я помню что VWWV статью писал или что-то подобное,там я это и услышал про кучу.

Просмотр сообщенияSooBad (12 Декабрь 2017 - 16:15) писал:

На протяжение всей темы рассматриваются примеры с участием блоков. По сути, блоки - это лишь структурная часть (почти)любого яп, и на принцип работы куча/стека и т.п. они влияют косвенно, ибо сначала обрабатываются внешние задействованные секреции памяти, и затем внутренние, а неиспользуемые и непроинициализированные массивы не занимают места в памяти.


На примерах показано как все работает это, по словам сложно передать ту суть которую хочешь, на примерах как раз таки мне кажется тут все и раскрыто и более понятно.
В статье все изложено, я не хочу это писать тебе еще раз, дабы что-то доказывать, про инициализацию и не, тут описано про метки которые занимает компилятор.

Сообщение отредактировал Long-: 12 Декабрь 2017 - 16:35

1

#4
Пользователь онлайн   VVWVV 

  • Эксперт
  • Вставить ник
  • Раскрыть информацию

Цитата

В стэк записываются данные о обработке текущей функции.

Чего?

Цитата

Именно из-за этого принципа локальные переменные Pawn работают именно как локальные переменные Pawn

Масло масляное.

Цитата

Тут variable_1 существует для этого куска кода

Это блок кода, а не кусок.

Цитата

4 байта выделяется под адрес кода, откуда была вызвана функция (адрес коллбэка и т.п.)

Адрес возврата. Следует отметить, что это всего лишь метка на инструкцию, следующую после вызова функции.

Цитата

И да, ещё хотелось бы сказать про "#pragma dynamic"

Следует дополнить: данная директива используется для увеличения блока, в котором содержатся стек и куча.

В целом статья написана нормально.
1

#5
Пользователь офлайн   Long- 

  • Assassin Of Scripters
  • Вставить ник
  • Раскрыть информацию

Просмотр сообщенияVVWVV (12 Декабрь 2017 - 16:49) писал:

Нажмите сюда, чтобы прочитать это сообщение. [Показать]


К словам можно было и не придираться, на счет Адреса возврата, отмечу, спасибо
Если еще что упустил, пишите.
0

#6
Пользователь офлайн   Nestyreff 

  • Stone tm
  • Вставить ник
  • Раскрыть информацию
Узнал много нового)
0

Поделиться темой:


Страница 1 из 1
  • Вы не можете создать новую тему
  • Вы не можете ответить в тему

1 человек читают эту тему
0 пользователей, 1 гостей, 0 скрытых пользователей