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)
Не разрешается копирование данной статьи на других ресурсах без разрешения автора.
Если есть какие-то вопросы, замечания или просто пожелания по поводу данного урока - оставляйте их здесь (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)
Не разрешается копирование данной статьи на других ресурсах без разрешения автора.