Скажу сразу, я не буду сравнивать расчёт по формуле с быдлокодерским "new shit[144];". В обоих случаях будет именно
подсчёт размера строки, а не впихивание везде одного и того же магического числа, т.е. сравнение будет производиться в равных условиях и при одинаковой постановке задачи.
Итак, допустим, у нас есть 2 образца кода:
CMD:myinfo(playerid, params[])
{
new string[57];
format(string
, sizeof(string
), "Ваш ID: %d, никнейм: %s", playerid
, string
); }
CMD:myinfo(playerid, params[])
{
static const fmt_str[] = "Ваш ID: %d, никнейм: %s";
new string[sizeof(fmt_str) + (-2 + 5) + (-2 + MAX_PLAYER_NAME)];
format(string
, sizeof(string
), fmt_str
, playerid
, string
); }
Удобство.
Сценарий первый: нужно сделать расчёт размера массива под форматирование строки.
При подсчёте размера массива под форматирование вручную (1-й отрывок) нужно самому сначала считать длину форматной строки, затем в уме вычесть длину каждого спецификатора и прибавить максимальную длину подставляемых значений - и не забудьте прибавить ещё 1 ячейку под символ конца строки.
При таком подходе очень легко что-то упустить и ошибиться в расчётах, т.к. нельзя перепроверить на промежуточные результаты.
Даже если использовать какие-то программы для подсчёта длины строк, размеры спецификаторов и значений вам придётся прибавлять и вычитать самостоятельно.
Но если считать по формуле с sizeof (отрывок #2), то компилятор сам подсчитает размер форматной строки (не длину, а именно размер, т.к. в него уже включена +1 ячейка под конец строки).
Затем каждое форматируемое значение следует добавить в формулу по уже проработанному шаблону "+ (-<размер спецификатора> + <макс. размер значения>)".
Получившаяся формула скармливается компилятору и он сам всё считает, тем самым исключая арифметические ошибки (от них никто не застрахован).
Кроме того, формула подсчёта расписана в коде - можно посмотреть на неё ещё раз и перепроверить её правильность, что исключает оставшуюся вероятность возникновения ошибок из-за неправильного составлении формулы.
Придётся написать одну лишнюю строку кода, чтобы объявить строковую константу для её использования в sizeof, но зато вам не придётся напрягаться с расчётами, при этом обеспечив минимальное использование стека и надёжность расчётов.
Сценарий второй: требуется изменить выводимый текст, а вместе с ним и пересчитать размер массива.
При подсчёте по формуле, если в форматной строке не были изменены спецификаторы (а именно так чаще всего и бывает), то и пересчитывать ничего не нужно - вы уже объяснили компилятору, по какой формуле нужно считать, и он сам всё сделает.
Если же считать вручную, размер массива придётся пересчитывать, если изменились спецификаторы или длина форматной строки. Одно из них меняется почти всегда, поэтому пересчёт размера массива потребуется практически в любом случае.
Скорость компиляции.
Как и в предыдущем мифе, попробуем сгенерировать два скрипта с большим количеством функций, в одном скрипте размером форматного буфера будет обычное число, во втором - формула.
В качестве повторяемой функции можно использовать оба варианта команды /myinfo, приведённые в самом начале доказательства. Количество повторов: 5000.
Я добавил в код теста несколько незначительных изменений, но думаю нет смысла описывать их все.
#include <a_samp>
#include "../include/pawnCompiler.inc"
#define TEST_SCRIPT_1 "test1"
#define TEST_SCRIPT_2 "test2"
#define TEST_SCRIPT_PWN_1 TEST_SCRIPT_1 ".pwn"
#define TEST_SCRIPT_PWN_2 TEST_SCRIPT_2 ".pwn"
#define TEST_SCRIPT_ASM_1 TEST_SCRIPT_1 ".asm"
#define TEST_SCRIPT_ASM_2 TEST_SCRIPT_2 ".asm"
#define TEST_SCRIPT_AMX_1 TEST_SCRIPT_1 ".amx"
#define TEST_SCRIPT_AMX_2 TEST_SCRIPT_2 ".amx"
#define TEST_FUNCTIONS_COUNT 5_000
GenerateScript(name[], bool:use_formula)
{
static const header[] =
"/* !! This file has been generated automatically. !! */" "\n"\
"\n"\
"#include <a_samp>" "\n"\
"\n"\
"\n";
static const function_str0[] = "public Function";
static const function_str1[] = "(playerid, params[]);\npublic Function";
static const function_str2[] = "(playerid, params[])\n{\n";
static const function_str3_0[] =
"\t" "new string[47];" "\n"\
"\t" "GetPlayerName(playerid, string, sizeof(string));" "\n"\
"\t" "format(string, sizeof(string), \"ID: %d, nickname: %s.\", playerid, string);" "\n"\
"\t" "return SendClientMessage(playerid, -1, string);";
static const function_str3_1[] =
"\t" "static const fmt_str[] = \"ID: %d, nickname: %s.\";" "\n"\
"\t" "new string[sizeof(fmt_str) + (-2 + 5) + (-2 + MAX_PLAYER_NAME)];" "\n"\
"\t" "GetPlayerName(playerid, string, sizeof(string));" "\n"\
"\t" "format(string, sizeof(string), fmt_str, playerid, string);" "\n"\
"\t" "return SendClientMessage(playerid, -1, string);";
static const function_str4[] =
"\n"\
"}" "\n";
new File:handle
= fopen(name
, io_write); handle,
header
);
new num_buffer[11];
for (new i = 0; i < TEST_FUNCTIONS_COUNT; ++i)
{
fwrite(handle
, function_str0
); fwrite(handle
, function_str1
); fwrite(handle
, function_str2
); fwrite(handle
, use_formula
? function_str3_1
: function_str3_0
); fwrite(handle
, function_str4
); }
}
dc_SleepFix(time) // by Daniel_Cortez \\ pro-pawn.ru
{ // WARNING: This function is still only usable in main().
// It just fixes the stack/heap collision error in default sleep().
static heap_ptr, stack_ptr;
#emit lctrl 2
#emit stor.pri heap_ptr
#emit lctrl 4
#emit stor.pri stack_ptr
sleep(time);
#emit load.pri stack_ptr
#emit sctrl 4
#emit load.pri heap_ptr
#emit sctrl 2
}
#if defined _ALS_sleep
#undef sleep
#else
#define _ALS_sleep
#endif
#define sleep(%0) dc_SleepFix(%0)
main()
{
GenerateScript(!TEST_SCRIPT_PWN_1, false);
GenerateScript(!TEST_SCRIPT_PWN_2, true);
new t1, t2, t;
WinExec("pawno/pawncc.exe -(+ -;+ -v0 -O1 -d0 -a scriptfiles/" TEST_SCRIPT_PWN_1);
do { sleep(0); } while (0 == (fexist(TEST_SCRIPT_ASM_1
) | fexist(TEST_SCRIPT_AMX_1
))); WinExec("pawno/pawncc.exe -(+ -;+ -v0 -O1 -d0 -a scriptfiles/" TEST_SCRIPT_PWN_2);
do { sleep(0); } while (0 == (fexist(TEST_SCRIPT_ASM_2
) | fexist(TEST_SCRIPT_AMX_2
))); new Float:dt
= float(t2
- t1
) / float(TEST_FUNCTIONS_COUNT
); printf("Компиляция без расчёта по формуле: %d", t1
); printf("Компиляция с расчётом по формуле: %d", t2
); "dt = %d / %d = %.4f тиков = %.8f мс",
t2 - t1, TEST_FUNCTIONS_COUNT, dt, dt / 1000
);
}
На моём ПК тест выдал следующий результат:
Код:
Компиляция без расчёта по формуле: 16577
Компиляция с расчётом по формуле: 18119
dt = 1542 / 5000 = 0.3084 тиков = 0.00030839 мс
Итак, разница от расчётов размера буфера по формуле составила ~300 микросекунд.
Расход памяти и нагрузка на сервер.
Модифицируем приведённый выше тест, изменив число повторов функций с 2000 до 1.
#define TEST_FUNCTIONS_COUNT 1
После этого снова запустим тестовый скрипт. В результате в папке scriptfiles будут сгенерированы файлы test1.pwn и test2.pwn.
Открыть/закрыть
test1.pwn:
/* !! This file has been generated automatically. !! */
#include <a_samp>
public Function0(playerid, params[]);
public Function0(playerid, params[])
{
new string[47];
format(string
, sizeof(string
), "ID: %d, nickname: %s.", playerid
, string
); }
test2.pwn:
/* !! This file has been generated automatically. !! */
#include <a_samp>
public Function0(playerid, params[]);
public Function0(playerid, params[])
{
static const fmt_str[] = "ID: %d, nickname: %s.";
new string[sizeof(fmt_str) + (-2 + 5) + (-2 + MAX_PLAYER_NAME)];
format(string
, sizeof(string
), fmt_str
, playerid
, string
); }
Кроме них сгенерируются файлы test1.asm и test2.asm.
Открыть/закрыть
test1.asm:
Код:
CODE 0 ; 0
;program exit point
halt 0
proc ; Function0
; line 9
;$lcl params 10
;$lcl playerid c
; line a
;$lcl string ffffff44
stack ffffff44
zero.pri
addr.alt ffffff44
fill bc
; line b
push.c 2f
;$par
push.adr ffffff44
;$par
push.s c
;$par
push.c c
sysreq.c 0 ; GetPlayerName
stack 10
;$exp
; line c
push.adr ffffff44
;$par
push.adr c
;$par
push.c 0
;$par
push.c 2f
;$par
push.adr ffffff44
;$par
push.c 14
sysreq.c 1 ; format
stack 18
;$exp
; line d
push.adr ffffff44
;$par
push.c ffffffff
;$par
push.s c
;$par
push.c c
sysreq.c 2 ; SendClientMessage
stack 10
stack bc
retn
DATA 0 ; 0
dump 49 44 3a 20 25 64 2c 20 6e 69 63 6b 6e 61 6d 65
dump 3a 20 25 73 2e 0
STKSIZE 1000
test2.asm:
Код:
CODE 0 ; 0
;program exit point
halt 0
proc ; Function0
; line 9
;$lcl params 10
;$lcl playerid c
; line a
; line b
;$lcl string ffffff44
stack ffffff44
zero.pri
addr.alt ffffff44
fill bc
; line c
push.c 2f
;$par
push.adr ffffff44
;$par
push.s c
;$par
push.c c
sysreq.c 0 ; GetPlayerName
stack 10
;$exp
; line d
push.adr ffffff44
;$par
push.adr c
;$par
push.c 0
;$par
push.c 2f
;$par
push.adr ffffff44
;$par
push.c 14
sysreq.c 1 ; format
stack 18
;$exp
; line e
push.adr ffffff44
;$par
push.c ffffffff
;$par
push.s c
;$par
push.c c
sysreq.c 2 ; SendClientMessage
stack 10
stack bc
retn
DATA 0 ; 0
dump 49 44 3a 20 25 64 2c 20 6e 69 63 6b 6e 61 6d 65
dump 3a 20 25 73 2e 0
STKSIZE 1000
Итак, что же мы видим? В обоих скриптах опкоды совершенно одинаковые, за исключением комментариев о номерах строк в исходном файле.
Содержимое секции данных (после слова "DATA") тоже идентично.
Теперь немного изменим код теста, убрав параметр компилятора "-a", чтобы компилятор генерировал файл *.amx вместо *.asm.
Также заменим "-v0" на "-v2", чтобы он выдавал отчёт о результатах компиляции.
До:
Код:
WinExec("pawno/pawncc.exe -(+ -;+ -v0 -O1 -d0 -a scriptfiles/" TEST_SCRIPT_PWN_1);
После:
Код:
WinExec("pawno/pawncc.exe -(+ -;+ -v2 -O1 -d0 scriptfiles/" TEST_SCRIPT_PWN_1);
После этого снова скомпилируем и запустим скрипт. В консоли сервера последовательно появятся отчёты о компиляции первого и второго скриптов.
Код:
Header size: 196 bytes
Code size: 212 bytes
Data size: 88 bytes
Stack/heap size: 16384 bytes; estimated max. usage=59 cells (236 bytes)
Total requirements: 16880 bytes
Done.
Header size: 196 bytes
Code size: 212 bytes
Data size: 88 bytes
Stack/heap size: 16384 bytes; estimated max. usage=59 cells (236 bytes)
Total requirements: 16880 bytes
Done.
Результаты абсолютно одинаковые, что было вполне ожидаемо, т.к. ассемблерные листинги тоже совпадали.