PDA

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



Daniel_Cortez
19.10.2015, 22:31
Внимание: данная тема закрыта для защиты от копирования.
Если есть какие-то вопросы, замечания или просто пожелания по поводу данного урока - оставляйте их здесь (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).




Миф 2: "Если кода меньше, значит он лучше."

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

Описание:

Чем-то похоже на предыдущий миф о якобы влиянии объёма исходного кода на оптимизацию, однако здесь имеется в виду сравнение не двух одинаковых кусков кода, один из которых "утрамбован" в одну строку, а просто разного кода.
Например, некоторые "профессионалы" с govno-info твердят, что цикл for быстрее, чем while, или что "соединение" строк с помощью format происходит быстрее, чем с помощью strcat.


Доказательство:
Проверим этот миф на примере "format vs strcat", сравним скрепление 2 и более строк.
И, в отличие от предыдущего мифа, код будет проверяться на скорость с помощью профайлера (http://pro-pawn.ru/showthread.php?12585).
Метод с использованием функции format:

new string[128];
format(string, sizeof(string), "%s%s", s1, s2);

Метод с использованием strcat:

new string[128];
string = s1;
strcat(string, s2);

Код с использованием strcat несколько объёмнее, чем с format, и будет ещё объёмнее, если нужно будет соединить не 2, а 3 и более строк, т.к. для каждой новой строки нужен будет отдельный вызов strcat.
Также обратите внимание: константы s1 и s2 не объявлены.
С ними мы разберёмся позже, пока что нужно ещё разобраться с измерением скорости выполнения кода из обоих методов.
Для такого измерения прекрасно подойдёт профайлер:

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

new const code_snippets_names[2][] =
{
{"format (2)"},
{"strcat (2)"}
};

#define Prerequisites();\
static const s1[] = "1234567890123456789012345678901234567890";\
static const s2[] = "1234567890123456789012345678901234567890";\
new str[256];

#define CodeSnippet1();\
format(str, sizeof(str), "%s%s", s1, s2);

#define CodeSnippet2();\
str = s1, strcat(str, s2);
/*Конец настроек.*/

Результаты профилирования:


Тестирование: <format (2)> vs <strcat (2)>
Режим: интерпретируемый, 10000x1000 итераций.
format (2): 8406
strcat (2): 2811

Как видно из результатов, метод с использованием strcat работает более 2 раз быстрее, чем format.
И это несмотря на то, что метод с format более лаконичен в плане объёма исходного кода.
Справедливости ради, следует отметить, что чем больше строк нужно соединить, тем меньше выигрыш при использовании strcat.
Поэтому при сцеплении 8 и более строк format вырывается вперёд:

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

new const code_snippets_names[2][] =
{
{"format (8)"},
{"strcat (8)"}
};

#define Prerequisites();\
static const s1[] = "1234567890123456789012345678901234567890";\
static const s2[] = "1234567890123456789012345678901234567890";\
static const s3[] = "1234567890123456789012345678901234567890";\
static const s4[] = "1234567890123456789012345678901234567890";\
static const s5[] = "1234567890123456789012345678901234567890";\
static const s6[] = "1234567890123456789012345678901234567890";\
static const s7[] = "1234567890123456789012345678901234567890";\
static const s8[] = "1234567890123456789012345678901234567890";\
new str[sizeof(s1) * 8];

#define CodeSnippet1();\
format(str, sizeof(str), "%s%s%s%s%s%s%s%s", s1, s2, s3, s4, s5, s6, s7, s8);

#define CodeSnippet2();\
str = s1, strcat(str, s2), strcat(str, s3), strcat(str, s4);\
strcat(str, s5), strcat(str, s6), strcat(str, s7), strcat(str, s8);
/*Конец настроек.*/

Результат:


Тестирование: <format (8)> vs <strcat (8)>
Режим: интерпретируемый, 10000x1000 итераций.
format (8): 20433
strcat (8): 21238

Но даже если strcat выигрывает не во всех случаях, то хотя бы в нескольких, причём самых распространённых - соединять 8 и более строк приходится сравнительно редко.

Рассмотрим ещё один пример: сравнение двух вещественных чисел.
Образец 1:

new Float: x = Float: random(0);
new bool:b;
b = (x == 1.0);

Образец 2:

new Float: x = Float: random(0);
new bool:b;
b = (_:x == _:1.0);

Следует заметить, что в Pawn не предусмотрены инструкции для работы с вещественными числами, для таких операций используются нативные функции (floatcmp для сравнения, floatadd для сложения, floatsub для вычитания, floatmul для умножения и т.д.)
Поэтому в первом примере сравнение вещественных чисел преобразуется в вызов нативной функции floatcmp, которая их и сравнивает.
Иными словами, код
b = (x == 1.0);
превратится в
b = (floatcmp(x, 1.0) == 0);
Во втором же примере сравниваются не сами вещественные числа, а их внутренние представления, которые, благодаря замене тегов, воспринимаются, как целые числа.

Для сравнения производительности снова воспользуемся профайлером:

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

new const code_snippets_names[2][] =
{
{"Образец 1"},
{"Образец 2"}
};

DoNothing(arg)
{
#pragma unused arg
}

#define Prerequisites();\
new bool:b, Float:x = Float: random(0);\
DoNothing(_:b);// Подавить варнинг 204 (неиспользуемая переменная "b").

#define CodeSnippet1();\
b = (x == 1.0);

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


Результаты тестирования:


Тестирование: <Образец 1> vs <Образец 2>
Режим: интерпретируемый, 100000x1000 итераций.
Образец 1: 8404
Образец 2: 3025


Результаты того же тестирования, только с JIT-компиляцией:


Тестирование: <Образец 1> vs <Образец 2>
Режим: с JIT-компиляцией, 100000x1000 итераций.
Образец 1: 1270
Образец 2: 384


Итак, в обоих рассмотренных примерах быстрее работают именно те образцы, в которых больше кода.

На практике же встречается ещё больше техник.
Пример: кэширование никнеймов игроков при подключении к серверу.
На само запоминание никнейма может уйти какое-то время. Да и кода может оказаться предостаточно (перехваты OnPlayerConnect, OnPlayerDisconnect, GetPlayerName, SetPlayerName).
С другой стороны, этим можно ускорить получение никнеймов игроков, заменив вызовы GetPlayerName на обращения к массиву с кэшированными никами.
Похожую картину можно наблюдать в инклуде foreach.inc от Y_Less: много кода, затраты на добавление и удаление игроков из итератора, но в противовес получаем крайне эффективный метод перебора подключенных игроков.

Как результат, утверждение о том, что чем меньше кода, тем больше производительность - неверно. Что и требовалось доказать.

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



Специально для Pro-Pawn.ru (http://www.pro-pawn.ru)
Не разрешается копирование данной статьи на других ресурсах без разрешения автора.