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

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

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

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


    Миф 3: "Лучше использовать массивы вместо обычных переменных."

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

    Описание:
      Открыть/закрыть
    Бывают такие уникумы, которые любят впихивать массивы куда ни попадя, даже если они занимают всего 1 ячейку. Либо вместо нескольких переменных используют массив (например, используют массив из 3 ячеек для хранения координат игрока, когда можно обойтись 3 одиночными переменными).
    На деле же доступ к ячейкам массива медленнее, чем к одиночным переменным. Для сравнения: доступ к обычным переменным происходит по их физическим адресам (взять значение из памяти по адресу - 1 операция), а к ячейкам массива по физическому адресу массива и смещению (взять адрес массива, прибавить смещение элемента, по полученному адресу взять значение из памяти - 3 операции).
    Обычно быдлокодеры аргументируют использование массивов тем, что "так записывать проще!" или в особо запущенных случаях "так оптимизированнее!"

    Пример кода:
    1. new Float:pos[3];
    2. GetPlayerPos(playerid, pos[1], pos[2], pos[3]);


    Доказательство:
      Открыть/закрыть
    Как обычно, сравним два образца кода, использовав приведённый выше пример с получением игрока.
    В одном образце координаты игрока будут записываться в массив, а в другом в отдельные переменные.

    Образец 1:
    1. #include <a_samp>
    2. main()
    3. {
    4. new Float: pos[3];
    5. GetPlayerPos(0, pos[0], pos[1], pos[2]);
    6. }

    Образец 2:
    1. #include <a_samp>
    2. main()
    3. {
    4. new Float:x, Float:y, Float:z;
    5. GetPlayerPos(0, x, y, z);
    6. }

    Объявление массива в первом образце записывается короче, чем объявление 3 отдельных переменных во втором.
    Но в то же время приходится при каждом обращении к массиву записывать индексы.
    Длина 1-го отрывка - 97 символов, второго - 94. Получается, что код без массива записывается даже короче, чем с массивом.
    Можно переименовать массив "pos" в что-нибудь из одной буквы (например, "p"), тогда получится выигрыш в 3 символа у отрывка с массивом, но назначение массива будет не так просто определить по названию. К тому же, вы всё равно потеряете больше времени на то, чтобы каждый раз дотянуться до фигурных скобок при каждом обращении к массиву.

    Итак, запись кода с массивом нисколько не проще. Но что же с оптимизацией?

    Скомпилируем оба образца с параметром "-v2", чтобы получить подробные результаты компиляции.
    Образец 1:
    Код:
    Pawn compiler 3.2.3664	 	 	Copyright (c) 1997-2006, ITB CompuPhase
    
    Header size:            124 bytes
    Code size:              136 bytes
    Data size:                0 bytes
    Stack/heap size:      16384 bytes; estimated max. usage=14 cells (56 bytes)
    Total requirements:   16644 bytes
    
    Done.
    Образец 2:
    Код:
    Pawn compiler 3.2.3664	 	 	Copyright (c) 1997-2006, ITB CompuPhase
    
    Header size:            124 bytes
    Code size:              108 bytes
    Data size:                0 bytes
    Stack/heap size:      16384 bytes; estimated max. usage=14 cells (56 bytes)
    Total requirements:   16616 bytes
    
    Done.
    Различаются только размеры секции кода (Code size), а вместе с ними и общие требования к памяти (Total requirements).
    Меньше размер секции кода во втором образце.

    Теперь скомпилируем образцы с параметром "-a" и сравним ассемблерные листинги.
    Образец 1:
    Код:
    CODE 0	; 0
    ;program exit point
    	halt 0
    
    	proc	; main
    	; line 3
    	; line 4
    	;$lcl pos fffffff4
    	stack fffffff4
    	zero.pri
    	addr.alt fffffff4
    	fill c
    	; line 5
    	addr.pri fffffff4
    	add.c 8
    	push.pri
    	;$par
    	addr.pri fffffff4
    	add.c 4
    	push.pri
    	;$par
    	push.adr fffffff4
    	;$par
    	push.c 0
    	;$par
    	push.c 10
    	sysreq.c 0	; GetPlayerPos
    	stack 14
    	;$exp
    	stack c
    	zero.pri
    	retn
    
    
    STKSIZE 1000
    Образец 2:
    Код:
    CODE 0	; 0
    ;program exit point
    	halt 0
    
    	proc	; main
    	; line 3
    	; line 4
    	;$lcl x fffffffc
    	push.c 0
    	;$exp
    	;$lcl y fffffff8
    	push.c 0
    	;$exp
    	;$lcl z fffffff4
    	push.c 0
    	;$exp
    	; line 5
    	push.adr fffffff4
    	;$par
    	push.adr fffffff8
    	;$par
    	push.adr fffffffc
    	;$par
    	push.c 0
    	;$par
    	push.c 10
    	sysreq.c 0	; GetPlayerPos
    	stack 14
    	;$exp
    	stack c
    	zero.pri
    	retn
    
    
    STKSIZE 1000
    Во втором образце всё, как и должно быть: в стеке создаются 3 переменные и в GetPlayerPos передаются их адреса (инструкции push.adr).
    А вот в первом образце для передачи pos[1] и pos[2] берётся адрес массива (addr.pri), прибавляется смещение (add.c) и уже полученный результат помещается в стек (push.pri).
    Как исключение, для передачи pos[0] в стек помещается только базовый адрес массива (push.adr): у нулевой ячейки смещение равно нулю, поэтому компилятор отбрасывает добавление смещения.
    В итоге имеем по 2 лишних инструкции при доступе к каждой ячейке массива, кроме нулевой.

    Наконец, протестируем скорость доступа к обычным переменным и к ячейкам массива.
    Для этой цели, как всегда, воспользуемся профайлером:
    1. /*Настройки.*/
    2. const PROFILE_ITERATIONS_MAJOR = 1000_000;
    3. const PROFILE_ITERATIONS_MINOR = 1_000;
    4.  
    5. new const code_snippets_names[2][] =
    6. {
    7. {"Ячейки массива"},
    8. {"Одиночные переменные"}
    9. };
    10.  
    11. // Функция для подавления варнинга 203 (переменные a, x, y, и z
    12. // никак не используются, им только присваиваются значения).
    13. stock DoNothing(Float:a[], Float:b, Float:c, Float:d)
    14. {
    15. #pragma unused a, b, c, d
    16. }
    17.  
    18. #define Prerequisites();\
    19.   new Float:a[3];\
    20.   new Float:x, Float:y, Float:z;\
    21.   DoNothing(a, x, y, z); // Подавить варнинг 203.
    22.  
    23. #define CodeSnippet1();\
    24.   a[0] = 0.0, a[1] = 0.0, a[2] = 0.0;
    25.  
    26. #define CodeSnippet2();\
    27.   x = 0.0, y = 0.0, z = 0.0;
    28. /*Конец настроек.*/

    Результаты (без JIT и с JIT соответственно):
    Код:
    Тестирование: <Одиночные переменные> vs <Ячейки массива>
    Режим: интерпретируемый, 1000000x1000 итераций.
    Массив: 97382
    Одиночные переменные: 31840
    Код:
    Тестирование: <Одиночные переменные> vs <Ячейки массива>
    Режим: с JIT-компиляцией, 1000000x1000 итераций.
    Массив: 4065
    Одиночные переменные: 3385
    Результат предсказуем: доступ к одиночным переменным происходит быстрее, чем к ячейкам массива.
    Всё в точности, как и было сказано в теории (см. пункт "Описание").

    В предыдущем сравнении было замечено, что компилятор оптимизирует код доступа к 0-й ячейке массива.
    Проверим, насколько действенна эта оптимизация и может ли она сравниться с доступом к одиночной переменной.
    1. /*Настройки.*/
    2. const PROFILE_ITERATIONS_MAJOR = 1000_000;
    3. const PROFILE_ITERATIONS_MINOR = 1_000;
    4.  
    5. new const code_snippets_names[2][] =
    6. {
    7. {"a[0] = 1"},
    8. {"b = 1"}
    9. };
    10.  
    11. // Функция для подавления варнинга 203 (переменные a, x, y, и z
    12. // никак не используются, им только присваиваются значения).
    13. stock DoNothing(a[], b)
    14. {
    15. #pragma unused a, b
    16. }
    17.  
    18. #define Prerequisites();\
    19.   new a[1];\
    20.   new b;\
    21.   DoNothing(a, b);
    22.  
    23. #define CodeSnippet1();\
    24.   a[0] = 1;
    25.  
    26. #define CodeSnippet2();\
    27.   b = 1;
    28. /*Конец настроек.*/

    Результат (без JIT):
    Код:
    Тестирование: <a[0] = 1> vs <b = 1>
    Режим: интерпретируемый, 1000000x1000 итераций.
    a[0] = 1: 28093
    b = 1: 32096
    Довольно интересный результат: доступ к 0-й ячейке даже быстрее, чем к обычной переменной.
    Сначала я подумал, что во всём виновата неэффективная реализация интерпретатора под Windows (в исходном коде интерпретатора под Linux вместо конструкции switch используются расширения GNU C, благодаря которым интерпретатор работает быстрее).
    Поэтому я провёл тот же самый тест на сервере под Linux:
    Код:
    [18:29:11] Тестирование: <a[0] = 1> vs <b = 1>
    [18:29:11] Режим: интерпретируемый, 1000000x1000 итераций.
    [18:30:02] a[0] = 1: 22747
    [18:30:02] b = 1: 25903
    Под Linux код действительно выполнялся быстрее, но преимущество так и осталось за доступом к 0-й ячейке массива.
    С другой стороны, чтобы измерить эту разницу в скорости (3000 тиков ~ 3 секунды), понадобился один миллиард итераций (1000000 x 1000, см. результаты).
    А теперь разделите 3000 на 1 миллиард. Будет ли такое преимущество заметно на практике? Вряд ли. Зато можно сделать весь свой код неразборчивым, заменив все переменные на массивы из 1 ячейки.

    Кроме того, мы ведь так и не провели тесты с использованием JIT.
    Windows:
    Код:
    Тестирование: <a[0] = 1> vs <b = 1>
    Режим: c JIT-компиляцией, 1000000x1000 итераций.
    a[0] = 1: 3909
    b = 1: 3984
    Linux:
    Код:
    [19:12:55] Тестирование: <a[0] = 1> vs <b = 1>
    [19:12:55] Режим: c JIT-компиляцией, 10000000x1000 итераций.
    [19:13:07] a[0] = 1: 5171
    [19:13:07] b = 1: 5065
    Как видно, JIT ставит всё на свои места: оба образца выполняются примерно одинаково быстро.

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


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

  2. 17 пользователя(ей) сказали cпасибо:
    #Vito (17.11.2015) $continue$ (11.11.2015) Avertus (12.11.2015) Danny_Marcelo (16.11.2015) DeimoS (23.01.2016) Desulaid (09.12.2015) Jonick (19.05.2016) Maranzalla (22.02.2016) Nurick (12.11.2015) Osetin (14.11.2015) Seviel (31.12.2016) Skyline (14.07.2018) Unreal (09.12.2015) vovandolg (07.11.2016) wAx (11.11.2015) [ForD] (12.11.2015) ^_^ (13.12.2015)
 

 

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

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

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

Ваши права

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