PDA

Просмотр полной версии : [Урок] Почему не следует использовать длинные строки в Pawn



Tracker1
03.08.2013, 22:19
Вступление:
У людей есть весьма необычные взгляды на строки в ПАВН, особенно на их длинну. Много людей думают что стринги не могут быть длинее 256*, почему, я незнаю, похоже на произвольно введенный лимит для них без нужды, когда на самом деле лимита нету. Другие люди думают что все стринги должны быть 256 без причины, неважно насколько длинно они создают стринг. Стринг в ПАВН тоже самое что и массив, у людей вроде нет больших проблем с использованием массивом, так почему же есть со стрингами?
*заметьте что много людей используют либо 255 либо 256, но это так же глупо.

Предпосылка
Стринг в павне это просто массив символов, без отличия от других массивов. Стринги заканчиваются NULL'ом, это занчит что они все имеют символ \0 (NULL) на конце (ASCII код 0, не путать с символом "0" у которого код 4). Если у вас такая вот строка


new str[3] = "hi";

То на самом деле она использует 3 ячейки. Одна для h, другая для i, и последня для символа NULL, что сигнализирует об окончании строки. Если копнуть глубже, то это выглядит вот так


new str[3] = {'h', 'i', '\0');


Или так


new str[3] = {104,105,0};


Где 104 и 105 это ASCII код для символов 'h' и 'i' соотвественно, и 0 - код для NULL символа.

В павн по дефолту все переменные равняются 0 когда вы их обьявляете, это значит что когда вы делаете.


new str[256];

На самом деле вы делаете


new str[256];
for(new i=0;i<256;i++)
{
str[i]=0;
}

Оптимизации больше, но это все равно требует времени

Так почему же вы не должны использовать 256?
Во-первых - медленно

Как я обьяснил в предпосылке все переменные ставятся на 0, когда мы их задаем, больше переменных - больше нагрузка на сервер чтобы он выставил их всех на 0. Этим занимается оптимизированная сборка, не интепретатор PAWN, спешу заметить что в AMXModX переменные не отчищаются при обьявлении, и они страдают проблемами из-за этого.*

*Заметка переводчика: текст йоба, могут быть проблемы с восприятием

Во-вторых - вам это не надо

Рассмотрим пост который я недавно видел
Quote: Originally Posted by X_Cutterz


stock ReturnModeratorCmd(playerid,reqlvl)
{
new rmcmd[256];
format(rmcmd,sizeof(rmcmd),"Only moderators level %d+ can use this command!",reqlvl);
return SendClientMessage(playerid,Green,rmcmd);
}


Предположим что максимум админ уровня это 10000, чтобы не запутаться (0 до 9999), таким образом самый длинный номер который будет вставлен в стринг состоит из 4 символов.

Quote: Only moderators level %d+ can use this command!

Но мои подсчеты подсказывают мне что их здесь всего 47, 2 из которых это %d которые не появятся в окончательном стринге, получается 45 уходят в окончательную строку. Мы уже убедились что самое длинное записываемое число будет длинной в 4(кстати, самое длинное число в павн может быть длинной в 11 символов (-2147483647)), и мы знаем что все строки нуждаются в NULL на конце, поэтому в этой строке максимальная длинна:

47 - 2 + 4 + 1 = 50

50 ячеек, и зачем вам 256? Это просто трата впустую 206 ячеек (824 байта - почти килобайт потерянной памяти)

Максимальное поле ввода - 128

В SA-MP чате максимальная длинна линий - 128, если кто-то что-то вводит, вы может быть уверены что это никогда не будет длинее 128. Это включает текст и комманды, так для чего же выделять массив который в 2 раза больше чем длинна вводимой строки?


Вам на самом деле это не надо



public OnPlayerCommandText(playerid, cmdtext[])
{
new
string[256],
cmd[256];
cmd = strtok(cmdtext, idx);
if (strcmp(cmd, "/num", true) == 0)
{
format(string, sizeof (string), "Random number: %d", random(27));
SendClientMessage(playerid, 0xFF0000AA, string);
}
}


Достаточно много смысловых ошибок в этом коде, но к сожалению это встречается ОЧЕНЬ часто. Игнорируем то что они используют strtok, сейчас речь не об этом. Код ОЧЕНЬ плохой:



new string[256],cmd[256];

Зачем два? Зачем вам нужен один огромный стринг для комманды и другой огромный стринг для данных которые вы будете обрабатывать после ввода команды, когда данные в переменной cmd уже ненужны? Просто используйте это еще раз, в ином случае вы создаете двойную нагрузку.


new string[256];

Почему 256? Как я уже обьяснил вводимая строка не может быть больше 128. А в коде выше можно сказать из-за заносимой строки в стринг нам не понадобится ячеек больше чем 18.

Более разумная версия всего происходящего будет выглядеть вот так:



public OnPlayerCommandText(playerid, cmdtext[])
{
new
string[128]; // cmdtext НИКОГДА НЕ БУДЕТ ДЛИНЕЕ 128!!!
string = strtok(cmdtext, idx);
if (strcmp(string, "/num", true) == 0)
{
format(string, sizeof (string), "Random number: %d", random(27)); // Нам надо 18 ячеек есть, однако так как вверху нам требовалось 128, все не так уж и плохо
SendClientMessage(playerid, 0xFF0000AA, string);
}
}



Использование Stack'а

Когда вы вызываете функцию, память используемая в ней хранится на штуке называемой stack(или Heap, но они используют одинаковый размер памяти, так что для упрощения мы называем stack), память выделяемая функции не может быть выделена статично, потому что функция может вызывать саму себя*, тем самым вынуждая выделенную память двоится. Та зона памяти где хранятся данные функция называется dynamic(динамичная) память. Например:



new
g_var = 2;
stock StackUse()
{
new
str[9];
format(str, sizeof (str), "g_var = %d", g_var);
SendClientMessageToAll(0xFF0000AA, str);
if (g_var)
{
g_var--;
StackUse();
}
}


Каждый вызов этой функции выделяет 9 ячеек в stack для использования, а затем она вызовет сама себя и выделит еще 9 ячеек, и еще раз она вызывет сама себя и выделит 9 ячеек. В сумме 3 выполнении, у нас выделено 27 ячеек в stack'е. На третье выполнение g_var становится равным 0, поэтому функция не вызывает себя и заканчивает свое действие, удаляя 9 ячеек из stack'а. Функция передает контроль прошлой инстанции этой функции, которая так же заканчивается и высвобождает 9 ячеек из stack'а, так же поступает и первая инстанция. В итоге теперь у нас занято 0 ячеек.

А теперь представьте этот код


new
g_var = 2;
stock StackUse()
{
new
str[256];
format(str, sizeof (str), "g_var = %d", g_var);
SendClientMessageToAll(0xFF0000AA, str);
if (g_var)
{
g_var--;
StackUse();
}
}


Тоже самое, но теперь она, при вызове ее 3 раза, выделит 768 ячеек (3 килобайта) в stack'е, сравнивая с оригинальными 27 ячейками (0,1 килобайт, кпд больше чем 2800%).

Некотоыре из вас могли замечать такие сообщения при компиляции


Header size: 216 bytes
Code size: 776 bytes
Data size: 528 bytes
Stack/heap size: 16384 bytes; estimated max. usage: unknown, due to recursion
Total requirements: 17904 bytes

или


Header size: 200 bytes
Code size: 588 bytes
Data size: 512 bytes
Stack/heap size: 16384 bytes; estimated max. usage=10250 cells (41000 bytes)
Total requirements: 17684 bytes


Это значит что компилятор обнаружил что вы используете больше места в stack'е чем доступно. Много важной информации хранится в stack'е, такой как техническая информация о текущей функции, из-за которой PAWN знает куда возвращаться. Используя слишком много памяти, вы можете переписать информацию в stack, возвращая(returning) в случайную точку в коде, что означает абсолютный крэш. В конце концов ваша информация может быть испорчена, когда ее перезапишет другая информация. Когда люди получают эти ошибки стандартный совет использовать #pragma dynamic, который служит обходом проблемы, не решением. Я уверен что застану вас в расплох, сказав то что YSI не вызывает этой ошибки, не смотря на его огромный размер, но маленькие скрипты нубоскриптеров вызывают. А ваш скрипт вызывает ли эту ошибку?

Прошу заметить что в 1 ошибке что я продемонстрировал, значится что вы используете больше stack'а чем положено, но так как функции вызывают сами себя(рекурсия епта), компилятор не может сказать точно сколько stack'а использует скрипт. Заметьте что компилятор не может самостоятельно рассчитать кол-во запусков функции. Во второй ошибке рекурсии нету.

Это относится ко всем массивам, но к стрингам в первую очередь.

Запакованные стринги *запакованные - сжатые.
Фишка PAWN которая очень редко используется в SA:MP это запакованные стринги. Обычные стринги содержат 1 символ в каждой ячейке, а ячейка это 4 байта. Запакованные стринги же хранят по 4 символа на каждую ячейку.

Не запакованные:

new string[12] = "Hello there"; // 12 cells, 48 bytes
Запакованные

new string[12 char] = !"Hello there"; // 3 cells, 12 bytes

Такие стринги хорошо описаны в pawn-lang.pdf, поэтому я здесь долго не задерживаюсь, но если у вас большие массивы стрингов, то это было бы очень хорошо, если вы будете использовать сжатые стринги для хранения, не для манипуляции. Если бы этот метод использовался в ReturnModeratorCmd выше, то кпд было бы больше 2000%(1 килобайт к 50 байтам)

Когда я должен использовать 256?

Говоря все это, хочу заметить что есть такие случаи, когда большие массивы необходимы.


SQL
Запросы в SQL могут быть очень длинными. Мне когда-то потребовался стринг размером в 1024 ячейки, однако это просто исключение из правила "Вам это не нужно", в этом случае вам это НУЖНО.
File Reading
Когда вы читаете файл, нет уверенности в длинне строки. Будет неплохо использовать стринг размером в 256.

Заключение

Если сомневаешься - пропусти это*. Бывают такие случаи, когда огромные массивы удобны для использования, думайте каждый раз когда вы объявляете массив. Думайте о том что вы собираетесь с ним делать

Osetin
03.08.2013, 22:24
Неплохо