PDA

Просмотр полной версии : [Урок] Мифы о Pawn-скриптинге - #3



Daniel_Cortez
11.11.2015, 20:22
Внимание: данная тема закрыта для защиты от копирования.
Если есть какие-то вопросы, замечания или просто пожелания по поводу данного урока - оставляйте их здесь (http://pro-pawn.ru/showthread.php?12774-%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5).




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

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

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

Пример кода:

new Float:pos[3];
GetPlayerPos(playerid, pos[1], pos[2], pos[3]);


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

Образец 1:

#include <a_samp>
main()
{
new Float: pos[3];
GetPlayerPos(0, pos[0], pos[1], pos[2]);
}

Образец 2:

#include <a_samp>
main()
{
new Float:x, Float:y, Float:z;
GetPlayerPos(0, x, y, z);
}

Объявление массива в первом образце записывается короче, чем объявление 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 лишних инструкции при доступе к каждой ячейке массива, кроме нулевой.

Наконец, протестируем скорость доступа к обычным переменным и к ячейкам массива.
Для этой цели, как всегда, воспользуемся профайлером (http://pro-pawn.ru/showthread.php?12585):

/*Настройки.*/
const PROFILE_ITERATIONS_MAJOR = 1000_000;
const PROFILE_ITERATIONS_MINOR = 1_000;

new const code_snippets_names[2][] =
{
{"Ячейки массива"},
{"Одиночные переменные"}
};

// Функция для подавления варнинга 203 (переменные a, x, y, и z
// никак не используются, им только присваиваются значения).
stock DoNothing(Float:a[], Float:b, Float:c, Float:d)
{
#pragma unused a, b, c, d
}

#define Prerequisites();\
new Float:a[3];\
new Float:x, Float:y, Float:z;\
DoNothing(a, x, y, z); // Подавить варнинг 203.

#define CodeSnippet1();\
a[0] = 0.0, a[1] = 0.0, a[2] = 0.0;

#define CodeSnippet2();\
x = 0.0, y = 0.0, z = 0.0;
/*Конец настроек.*/

Результаты (без JIT и с JIT соответственно):


Тестирование: <Одиночные переменные> vs <Ячейки массива>
Режим: интерпретируемый, 1000000x1000 итераций.
Массив: 97382
Одиночные переменные: 31840



Тестирование: <Одиночные переменные> vs <Ячейки массива>
Режим: с JIT-компиляцией, 1000000x1000 итераций.
Массив: 4065
Одиночные переменные: 3385

Результат предсказуем: доступ к одиночным переменным происходит быстрее, чем к ячейкам массива.
Всё в точности, как и было сказано в теории (см. пункт "Описание").

В предыдущем сравнении было замечено, что компилятор оптимизирует код доступа к 0-й ячейке массива.
Проверим, насколько действенна эта оптимизация и может ли она сравниться с доступом к одиночной переменной.

/*Настройки.*/
const PROFILE_ITERATIONS_MAJOR = 1000_000;
const PROFILE_ITERATIONS_MINOR = 1_000;

new const code_snippets_names[2][] =
{
{"a[0] = 1"},
{"b = 1"}
};

// Функция для подавления варнинга 203 (переменные a, x, y, и z
// никак не используются, им только присваиваются значения).
stock DoNothing(a[], b)
{
#pragma unused a, b
}

#define Prerequisites();\
new a[1];\
new b;\
DoNothing(a, b);

#define CodeSnippet1();\
a[0] = 1;

#define CodeSnippet2();\
b = 1;
/*Конец настроек.*/

Результат (без 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 (http://www.pro-pawn.ru)
Не разрешается копирование данной статьи на других ресурсах без разрешения автора.