Почему не следует использовать строки длиной в 256 ячеек

Описание: Уроки по скриптингу
Модератор: SJplayer

Дим M
Автор темы, Администратор
Администратор
Аватара
Дим M
Автор темы, Администратор
Администратор
Сообщения: 1607
Зарегистрирован: 5 апреля 2013
С нами: 10 лет 11 месяцев

#1 Дим » 9 апреля 2013, 17:57

Почему не следует использовать строки длиной в 256 ячеек
Автор: Tracker1

Вступление:
У людей есть весьма необычные взгляды на строки в ПАВН, особенно на их длинну. Много людей думают что стринги не могут быть длинее 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 
    gVar 
= 2; 
stock StackUse
() 
{ 
    new 
        str
[9]; 
    format
(str, sizeof (str), "gVar = %d", gVar); 
    SendClientMessageToAll
(0xFF0000AA, str); 
    if 
(gVar) 
    
{ 
        gVar
--; 
        StackUse
(); 
    
} 


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

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

Код: Выделить всё

new 
    gVar 
= 2; 
stock StackUse
() 
{ 
    new 
        str
[256]; 
    format
(str, sizeof (str), "gVar = %d", gVar); 
    SendClientMessageToAll
(0xFF0000AA, str); 
    if 
(gVar) 
    
{ 
        gVar
--; 
        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?

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

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

2. File Reading
Когда вы читаете файл, нет уверенности в длинне строки. Будет неплохо использовать стринг размером в 256.

Заключение

Если сомневаешься - пропусти это*. Бывают такие случаи, когда огромные массивы удобны для использования, думайте каждый раз когда вы объявляете массив. Думайте о том что вы собираетесь с ним делать
[center]i love you [s]mxIni[/s] Mysql[/center]


  • Похожие темы
    Ответы
    Просмотры
    Последнее сообщение

Вернуться в «Уроки»

Кто сейчас на форуме (по активности за 5 минут)

Сейчас этот раздел просматривают: 1 гость