Добро пожаловать на Pro Pawn - Портал о PAWN-скриптинге.
Показано с 1 по 1 из 1
  1. #1
    Аватар для Daniel_Cortez
    "Это не хак, это фича"

    Статус
    Оффлайн
    Регистрация
    06.04.2013
    Адрес
    Novokuznetsk, Russia
    Сообщений
    2,192
    Репутация:
    2589 ±

    Мифы о Pawn-скриптинге - #6

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


    Миф 6: "Не стоит измерять длину строк по формуле с sizeof: это неудобно, замедляет компиляцию, увеличивает расход памяти, нагружает сервер лишними расчётами."

    Статус: Опровергнут.

    Описание:
    Данный миф чем-то похож на предыдущий миф о вреде использования sizeof.
    Интересен он тем, что его распространяют не какие-то нубы, а те, кто уже хорошо разбирается в скриптинге, но доказать в своих утверждениях ничего не могут. Впрочем, им это и не нужно, чтобы продолжать вешать всем лапшу на уши.

    Доказательство:
      Открыть/закрыть

    Скажу сразу, я не буду сравнивать расчёт по формуле с быдлокодерским "new shit[144];". В обоих случаях будет именно подсчёт размера строки, а не впихивание везде одного и того же магического числа, т.е. сравнение будет производиться в равных условиях и при одинаковой постановке задачи.

    Итак, допустим, у нас есть 2 образца кода:
    1. CMD:myinfo(playerid, params[])
    2. {
    3. new string[57];
    4. GetPlayerName(playerid, string, sizeof(string));
    5. format(string, sizeof(string), "Ваш ID: %d, никнейм: %s", playerid, string);
    6. return SendClientMessage(playerid, -1, string);
    7. }

    1. CMD:myinfo(playerid, params[])
    2. {
    3. static const fmt_str[] = "Ваш ID: %d, никнейм: %s";
    4. new string[sizeof(fmt_str) + (-2 + 5) + (-2 + MAX_PLAYER_NAME)];
    5. GetPlayerName(playerid, string, sizeof(string));
    6. format(string, sizeof(string), fmt_str, playerid, string);
    7. return SendClientMessage(playerid, -1, string);
    8. }



    Удобство.
    Сценарий первый: нужно сделать расчёт размера массива под форматирование строки.

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

    Но если считать по формуле с sizeof (отрывок #2), то компилятор сам подсчитает размер форматной строки (не длину, а именно размер, т.к. в него уже включена +1 ячейка под конец строки).
    Затем каждое форматируемое значение следует добавить в формулу по уже проработанному шаблону "+ (-<размер спецификатора> + <макс. размер значения>)".
    Получившаяся формула скармливается компилятору и он сам всё считает, тем самым исключая арифметические ошибки (от них никто не застрахован).
    Кроме того, формула подсчёта расписана в коде - можно посмотреть на неё ещё раз и перепроверить её правильность, что исключает оставшуюся вероятность возникновения ошибок из-за неправильного составлении формулы.
    Придётся написать одну лишнюю строку кода, чтобы объявить строковую константу для её использования в sizeof, но зато вам не придётся напрягаться с расчётами, при этом обеспечив минимальное использование стека и надёжность расчётов.


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

    При подсчёте по формуле, если в форматной строке не были изменены спецификаторы (а именно так чаще всего и бывает), то и пересчитывать ничего не нужно - вы уже объяснили компилятору, по какой формуле нужно считать, и он сам всё сделает.

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


    Скорость компиляции.
    Как и в предыдущем мифе, попробуем сгенерировать два скрипта с большим количеством функций, в одном скрипте размером форматного буфера будет обычное число, во втором - формула.
    В качестве повторяемой функции можно использовать оба варианта команды /myinfo, приведённые в самом начале доказательства. Количество повторов: 5000.
    Я добавил в код теста несколько незначительных изменений, но думаю нет смысла описывать их все.
    1. #include <a_samp>
    2. #include "../include/pawnCompiler.inc"
    3.  
    4. #define TEST_SCRIPT_1 "test1"
    5. #define TEST_SCRIPT_2 "test2"
    6. #define TEST_SCRIPT_PWN_1 TEST_SCRIPT_1 ".pwn"
    7. #define TEST_SCRIPT_PWN_2 TEST_SCRIPT_2 ".pwn"
    8. #define TEST_SCRIPT_ASM_1 TEST_SCRIPT_1 ".asm"
    9. #define TEST_SCRIPT_ASM_2 TEST_SCRIPT_2 ".asm"
    10. #define TEST_SCRIPT_AMX_1 TEST_SCRIPT_1 ".amx"
    11. #define TEST_SCRIPT_AMX_2 TEST_SCRIPT_2 ".amx"
    12. #define TEST_FUNCTIONS_COUNT 5_000
    13.  
    14.  
    15. GenerateScript(name[], bool:use_formula)
    16. {
    17. static const header[] =
    18. "/* !! This file has been generated automatically. !! */" "\n"\
    19. "\n"\
    20. "#include <a_samp>" "\n"\
    21. "\n"\
    22. "\n";
    23. static const function_str0[] = "public Function";
    24. static const function_str1[] = "(playerid, params[]);\npublic Function";
    25. static const function_str2[] = "(playerid, params[])\n{\n";
    26. static const function_str3_0[] =
    27. "\t" "new string[47];" "\n"\
    28. "\t" "GetPlayerName(playerid, string, sizeof(string));" "\n"\
    29. "\t" "format(string, sizeof(string), \"ID: %d, nickname: %s.\", playerid, string);" "\n"\
    30. "\t" "return SendClientMessage(playerid, -1, string);";
    31. static const function_str3_1[] =
    32. "\t" "static const fmt_str[] = \"ID: %d, nickname: %s.\";" "\n"\
    33. "\t" "new string[sizeof(fmt_str) + (-2 + 5) + (-2 + MAX_PLAYER_NAME)];" "\n"\
    34. "\t" "GetPlayerName(playerid, string, sizeof(string));" "\n"\
    35. "\t" "format(string, sizeof(string), fmt_str, playerid, string);" "\n"\
    36. "\t" "return SendClientMessage(playerid, -1, string);";
    37. static const function_str4[] =
    38. "\n"\
    39. "}" "\n";
    40. new File:handle = fopen(name, io_write);
    41. handle,
    42. header
    43. );
    44. new num_buffer[11];
    45. for (new i = 0; i < TEST_FUNCTIONS_COUNT; ++i)
    46. {
    47. fwrite(handle, function_str0);
    48. valstr(num_buffer, i);
    49. fwrite(handle, num_buffer);
    50. fwrite(handle, function_str1);
    51. fwrite(handle, num_buffer);
    52. fwrite(handle, function_str2);
    53. fwrite(handle, use_formula ? function_str3_1 : function_str3_0);
    54. fwrite(handle, function_str4);
    55. }
    56. fclose(handle);
    57. }
    58.  
    59. dc_SleepFix(time) // by Daniel_Cortez \\ pro-pawn.ru
    60. { // WARNING: This function is still only usable in main().
    61. // It just fixes the stack/heap collision error in default sleep().
    62. static heap_ptr, stack_ptr;
    63. #emit lctrl 2
    64. #emit stor.pri heap_ptr
    65. #emit lctrl 4
    66. #emit stor.pri stack_ptr
    67. sleep(time);
    68. #emit load.pri stack_ptr
    69. #emit sctrl 4
    70. #emit load.pri heap_ptr
    71. #emit sctrl 2
    72. }
    73. #if defined _ALS_sleep
    74. #undef sleep
    75. #else
    76. #define _ALS_sleep
    77. #endif
    78. #define sleep(%0) dc_SleepFix(%0)
    79.  
    80. main()
    81. {
    82. GenerateScript(!TEST_SCRIPT_PWN_1, false);
    83. GenerateScript(!TEST_SCRIPT_PWN_2, true);
    84. fremove(TEST_SCRIPT_ASM_1);
    85. fremove(TEST_SCRIPT_ASM_2);
    86. fremove(TEST_SCRIPT_AMX_1);
    87. fremove(TEST_SCRIPT_AMX_2);
    88. new t1, t2, t;
    89. t = GetTickCount();
    90. WinExec("pawno/pawncc.exe -(+ -;+ -v0 -O1 -d0 -a scriptfiles/" TEST_SCRIPT_PWN_1);
    91. do { sleep(0); } while (0 == (fexist(TEST_SCRIPT_ASM_1) | fexist(TEST_SCRIPT_AMX_1)));
    92. t1 = GetTickCount() - t;
    93. t = GetTickCount();
    94. WinExec("pawno/pawncc.exe -(+ -;+ -v0 -O1 -d0 -a scriptfiles/" TEST_SCRIPT_PWN_2);
    95. do { sleep(0); } while (0 == (fexist(TEST_SCRIPT_ASM_2) | fexist(TEST_SCRIPT_AMX_2)));
    96. t2 = GetTickCount() - t;
    97. new Float:dt = float(t2 - t1) / float(TEST_FUNCTIONS_COUNT);
    98. printf("Компиляция без расчёта по формуле: %d", t1);
    99. printf("Компиляция с расчётом по формуле: %d", t2);
    100. "dt = %d / %d = %.4f тиков = %.8f мс",
    101. t2 - t1, TEST_FUNCTIONS_COUNT, dt, dt / 1000
    102. );
    103. }

    На моём ПК тест выдал следующий результат:
    Код:
    Компиляция без расчёта по формуле: 16577
    Компиляция с расчётом по формуле: 18119
    dt = 1542 / 5000 = 0.3084 тиков = 0.00030839 мс
    Итак, разница от расчётов размера буфера по формуле составила ~300 микросекунд.


    Расход памяти и нагрузка на сервер.

    Модифицируем приведённый выше тест, изменив число повторов функций с 2000 до 1.
    1. #define TEST_FUNCTIONS_COUNT 1


    После этого снова запустим тестовый скрипт. В результате в папке scriptfiles будут сгенерированы файлы test1.pwn и test2.pwn.
      Открыть/закрыть

    test1.pwn:
    1. /* !! This file has been generated automatically. !! */
    2.  
    3. #include <a_samp>
    4.  
    5. public Function0(playerid, params[]);
    6. public Function0(playerid, params[])
    7. {
    8. new string[47];
    9. GetPlayerName(playerid, string, sizeof(string));
    10. format(string, sizeof(string), "ID: %d, nickname: %s.", playerid, string);
    11. return SendClientMessage(playerid, -1, string);
    12. }


    test2.pwn:
    1. /* !! This file has been generated automatically. !! */
    2.  
    3. #include <a_samp>
    4.  
    5. public Function0(playerid, params[]);
    6. public Function0(playerid, params[])
    7. {
    8. static const fmt_str[] = "ID: %d, nickname: %s.";
    9. new string[sizeof(fmt_str) + (-2 + 5) + (-2 + MAX_PLAYER_NAME)];
    10. GetPlayerName(playerid, string, sizeof(string));
    11. format(string, sizeof(string), fmt_str, playerid, string);
    12. return SendClientMessage(playerid, -1, string);
    13. }


    Кроме них сгенерируются файлы 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.
    Результаты абсолютно одинаковые, что было вполне ожидаемо, т.к. ассемблерные листинги тоже совпадали.


    Выводы:
      Открыть/закрыть
    Первый способ подсчёта (без sizeof и формул) подойдёт для тех, кто не любит/не умеет много печатать.
    Второй же способ с расчётом по формуле подойдёт тем, кто не любит напрягать мозг из-за арифметики и пересчитывать всё из-за каждого изменения в тексте, а также кому важно минимальное использование ресурсов вкупе с максимальной защищённостью от ошибок.

    Кому-то удобен первый способ, кому-то второй, поэтому чьи-либо суждения об удобстве/неудобстве одного из этих них сугубо субъективны (чего, кстати, нельзя сказать о надёжности расчётов) и не могут быть восприняты всеми одинаково.
    К тому же, как уже было сказано ранее, недостатки есть у обоих способов и доказывать однозначное превосходство одного из них нет смысла. У каждого из них есть своя аудитория, которой удобно использовать данный конкретный способ расчёта.
    Поэтому утверждение о неудобстве расчётов по формуле в корне неправильно: что неудобно для одного, может оказаться очень даже удобно для другого.

    При расчёте размера форматного буфера по формуле, разумеется, тратится больше времени, чем при объявлении массива с размером в виде одиночной числовой константы, т.к. компилятору нужно разобрать формулу и вычислить её результат.
    Выявленная разница во времени при компиляции с расчётом размеров буфера по формуле - ~300 микросекунд или 0.0003 секунды - и это для целых 5000 расчётов, когда вряд ли на практике существует хотя бы один проект, в котором расчёт по формуле можно применить более 1000 раз.
    Ничтожно малое время, особенно в сравнении с общим временем компиляции.
    Итак, время, затрачиваемое на компиляцию с расчётом по формуле, с трудом поддаётся измерению и ни на миллисекунду не влияет на результат.

    По поводу якобы увеличения нагрузки на сервер и повышения расхода памяти - полная чушь.
    Изучение ассемблерных листингов и отчётов о компиляции скриптов (*.amx) только доказывают факт абсурдности подобных высказываний.
    Но как же так!? Ведь во втором примере были лишние расчёты! Это должно создать лишнюю нагрузку!
    Формула рассчитывается ещё во время компиляции и превращается в числовую константу.
    Т.е. если скормить компилятору "new string[sizeof(fmt_str) + (-2 + 5) + (-2 + MAX_PLAYER_NAME);", он превратит это в "new string[57];" - в точности, как в первом примере.
    Таким образом, на сервере от этого никакой нагрузки нет. Лишняя нагрузка есть только на компилятор, но как было показано выше, она не увеличивает время компиляции ни на одну миллисекунду.
    Но во втором примере ещё был лишний массив! От этого должно использоваться больше памяти!
    В первом примере этот массив тоже был. Любая строка - это тоже массив и для её хранения тоже требуется место в секции данных (а вы думали, что эти строки должны браться из ниоткуда?)
    В примере с расчётом размера по формуле этот массив всего лишь был объявлен в явном виде, на результат компиляции это не повлияло - в обоих примерах строки оказались в секции данных.

    Общий вердикт: миф о вреде расчёта размера строк по формуле опровергнут.



    Специально для Pro-Pawn.ru
    Не разрешается копирование данной статьи на других ресурсах без разрешения автора.
    Индивидуально в ЛС по скриптингу не помогаю. Задавайте все свои вопросы здесь (click).

  2. 9 пользователя(ей) сказали cпасибо:
    $continue$ (15.01.2016) Alanchick (13.07.2016) Desulaid (15.01.2016) enj0y (03.10.2016) gangzone.ini (15.01.2016) LLIapuk (15.01.2016) MaKcuM (12.01.2017) Profyan (16.01.2016) [ForD] (18.01.2016)
 

 

Информация о теме

Пользователи, просматривающие эту тему

Эту тему просматривают: 1 (пользователей: 0 , гостей: 1)

Ваши права

  • Вы не можете создавать новые темы
  • Вы не можете отвечать в темах
  • Вы не можете прикреплять вложения
  • Вы не можете редактировать свои сообщения
  •