PDA

Просмотр полной версии : [Урок] Оптимизация. Что выбрать: switch или if?



VVWVV
05.04.2018, 17:34
Введение

Множество разработчиков желают оптимизировать свой код, используя различные техники оптимизации. Довольно интересным методом оптимизации является использование оператора множественного выбора (или switch) и обычного условного оператора (или if).

Обычный условный оператор

Обычный условный оператор предполагает выражение, которые будет проверено на этапе исполнения, то есть в процессе работы программы. Таким образом, сначала оператору необходимо вычислить все выражения, и только потом сравнить их. На данные вычисления уходит достаточного много времени. Тем более, если вам необходимо несколько сравнений для одного и того же значения (к примеру, значения из массива).
new baz = 4;
if ((baz == 1) || (baz == 2) || (baz == 4)) {
/* какой-то код.. */
}В таком случае компилятору придётся создать несколько инструкций, которые будут каждый раз загружать в регистр значение из памяти; если говорить проще, то они будут каждый раз копироваться в регистр, и только потом сравниваться. Поэтому в этом случае лучше воспользоваться оператором множественного выбора (или switch).


load.s.pri fffffffc ; Загрузка значения из переменной (1)
const.alt 1 ; Загрузка значения в регистр alt.
eq ; Сравнение.
jnz 1 ; Переход к телу цикла.
load.s.pri fffffffc ; Загрузка значения из переменной (2)
const.alt 2 ; Загрузка значения в регистр alt.
eq ; Сравнение.
jnz 1 ; Переход к телу цикла.
load.s.pri fffffffc ; Загрузка значения из переменной (3)
const.alt 4 ; Загрузка значения в регистр alt.
eq ; Сравнение.
jnz 1 ; Переход к телу цикла.

Случаи использования: Тем не менее следует использовать данный оператор, если вместо операторов равенство (==) или неравенство (!=) используются операторы больше (>), меньше (<), больше или равно (>=), меньше или равно (<=). Тем более, когда они используются при обозначении диапазона значений (например, диапазон значений для n от нуля до трёх включительно – 0 < n <= 3).

Оператор множественного выбора

Оператор множественного выбора предполагает значение, которое будет сравниваться с числами. Эти числа необходимо занести в тело оператора.
new baz = 4;
switch (baz) {
case 1,2,4: { /* какой-то код.. */ }
}Таким образом, компилятор создаст таблицу значений, где будут наши предполагаемые числа и переходы к их инструкциям, а также всего лишь одна инструкция копирования.


load.s.pri fffffffc ; Только одно копирование из памяти.
switch 0 ; Начало оператора.
l.2 ; Переход к телу
jump 1
l.0 ; Таблица
casetbl
case 3 1 ; Обычная ветка, если ничего не нашлось.
case 1 2 ; Сравнение.
case 2 2 ; Сравнение.
case 4 2 ; Сравнение.

Случаи использования: Не следует использовать данный оператор при обозначении диапазона значений (например, case 0…3), поскольку компилятор создаст слишком большую таблицу, которая займёт больше времени, чем от использования простого условного оператора.

Тестирование

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

Тестирование: <if> vs <switch>
Режим: интерпретируемый, 10000x10000 итераций.
if: 18286
switch: 10852

// Profiler v1.3 (copyright (c) 2014-2017 Daniel_Cortez) \\ Pro-Pawn.ru
// Условия использования данного кода: http://pro-pawn.ru/showthread.php?12585


/*======== Настройки =========================================================*/
// Кол-во итераций в циклах.
const PROFILER_ITERATIONS_MAJOR = 10_000;
const PROFILER_ITERATIONS_MINOR = 10_000;

// Названия отрывков кода.
new const code_snippets_names[2][] =
{
{"if"},
{"switch"}
};

// Здесь вы можете объявить переменные, используемые в профилируемых отрывках кода
// и выполнить некоторые действия непосредственно перед профилированием.
#define Prerequisites(); \
new baz;

#define CodeSnippet0(); \
if ((baz==1) || (baz==2) || (baz==4)) {}

#define CodeSnippet1(); \
switch (baz) { \
case 1,2,4: {} \
}
/*======== Конец настроек ===================================================*/



// Не рекомендую изменять следующий код, если вы в нём не разбираетесь.
#tryinclude <a_samp>
#if defined _samp_included
#define LINE_BREAK ""
#else
#define LINE_BREAK "\n"
#include <core>
#include <time>
#define GetTickCount() tickcount()
#endif

#define _PROFILE_START(%0) \
__t[%0] = GetTickCount(); \
for (__j = 0; __j < PROFILER_ITERATIONS_MINOR; ++__j) \
{
#define _PROFILE_END(%0) __t[%0] = GetTickCount()-__t[%0]

#if defined _samp_included
#define PROFILE_START(%0); _PROFILE_START(%0)
#define PROFILE_END(%0); \
} \
_PROFILE_END(%0);
#else
#define PROFILE_START(%0); _PROFILE_START(%0)
#define PROFILE_END(%0);\
} \
if ((_PROFILE_END(%0)) < 0) {--__i; continue;}
#endif

#define CODE_SNIPPET_EXISTS(%0) \
(sizeof(code_snippets_names) >= ((%0) + 1)) && (defined CodeSnippet%0)


new code_snippets_time[sizeof(code_snippets_names)] = {0, ...};

main()
{
new __i, __j, __t[sizeof(code_snippets_names)];
#emit zero.pri
#emit lctrl 7
#emit stor.s.pri __i
#if CODE_SNIPPET_EXISTS(2)
static const ending[] =
#if (sizeof(code_snippets_names) <= 4)
"ка";
#else
"ков";
#endif
printf(
"Тестирование: %d отрыв%s кода." LINE_BREAK,
sizeof(code_snippets_names), ending
);
#else
printf(
"Тестирование: <%s> vs <%s>" LINE_BREAK,
code_snippets_names[0], code_snippets_names[1]
);
#endif
static const JIT_status_strings[2][] =
{"интерпретируемый", "с JIT-компиляцией"};
printf(
"Режим: %s, %dx%d итераций.\a" LINE_BREAK,
JIT_status_strings[__i],
PROFILER_ITERATIONS_MAJOR, PROFILER_ITERATIONS_MINOR
);
Prerequisites();
for (__i = 0; __i < PROFILER_ITERATIONS_MAJOR; ++__i)
{
PROFILE_START(0);
CodeSnippet0();
PROFILE_END(0);
PROFILE_START(1);
CodeSnippet1();
PROFILE_END(1);
#if CODE_SNIPPET_EXISTS(2)
PROFILE_START(2);
CodeSnippet2();
PROFILE_END(2);
#endif
#if CODE_SNIPPET_EXISTS(3)
PROFILE_START(3);
CodeSnippet3();
PROFILE_END(3);
#endif
#if CODE_SNIPPET_EXISTS(4)
PROFILE_START(4);
CodeSnippet4();
PROFILE_END(4);
#endif
#if CODE_SNIPPET_EXISTS(5)
PROFILE_START(5);
CodeSnippet5();
PROFILE_END(5);
#endif
#if CODE_SNIPPET_EXISTS(6)
PROFILE_START(6);
CodeSnippet6();
PROFILE_END(6);
#endif
#if CODE_SNIPPET_EXISTS(7)
PROFILE_START(7);
CodeSnippet7();
PROFILE_END(7);
#endif
#if CODE_SNIPPET_EXISTS(8)
PROFILE_START(8);
CodeSnippet8();
PROFILE_END(8);
#endif
#if CODE_SNIPPET_EXISTS(9)
PROFILE_START(9);
CodeSnippet9();
PROFILE_END(9);
#endif
#if CODE_SNIPPET_EXISTS(10)
PROFILE_START(10);
CodeSnippet10();
PROFILE_END(10);
#endif
#if CODE_SNIPPET_EXISTS(11)
PROFILE_START(11);
CodeSnippet11();
PROFILE_END(11);
#endif
#if CODE_SNIPPET_EXISTS(12)
PROFILE_START(12);
CodeSnippet12();
PROFILE_END(12);
#endif
#if CODE_SNIPPET_EXISTS(13)
PROFILE_START(13);
CodeSnippet13();
PROFILE_END(13);
#endif
#if CODE_SNIPPET_EXISTS(14)
PROFILE_START(14);
CodeSnippet14();
PROFILE_END(14);
#endif
#if CODE_SNIPPET_EXISTS(15)
PROFILE_START(15);
CodeSnippet15();
PROFILE_END(15);
#endif
for (__j = 0; __j < sizeof(code_snippets_names); ++__j)
code_snippets_time[__j] += __t[__j];
}
for (__i = 0; __i < sizeof(code_snippets_names); ++__i)
printf(
"%s: %d" LINE_BREAK,
code_snippets_names[__i], code_snippets_time[__i]
);
printf("\a\a" LINE_BREAK);
}


Статью подготовил: VVWVV (http://pro-pawn.ru/member.php?4348)


Если у вас все ещё остались вопросы по оптимизации кода при использовании данных операторов, то пишите их.

Исключительно для pro-pawn.ru
Копирование данной статьи на других ресурсах без разрешения автора запрещено!

Nexius_Tailer
05.04.2018, 19:17
Некоторые тесты скорости между конкретными случаями использования обоих операторов были бы очень уместны (особенно для примера, где быстрее тот или иной оператор, и уже как вывод на основе тестов описывалось бы когда что лучше использовать).

VVWVV
05.04.2018, 19:21
Некоторые тесты скорости между конкретными случаями использования обоих операторов были бы очень уместны (особенно для примера, где быстрее тот или иной оператор, и уже как вывод на основе тестов описывалось бы когда что лучше использовать).

Я ж их... сейчас..

Daniel_Cortez
07.04.2018, 01:03
Оператор множественного выбора
(...)
Не следует использовать данный оператор при обозначении диапазона значений (например, case 0…3)
Диапазон из 4 значений как раз достаточно мал, чтобы оператор switch работал с ним быстрее, чем if. Нужно как минимум 9-10 значений в диапазоне, чтобы switch начал проигрывать - по крайней мере, это справедливо в случае со специфичным для GCC ядром интерпретатора.

Касаемо теста производительности, необязательно было выкладывать весь код - достаточно было просто указать ссылку на тему с тестом, а в своём посте указать только настройки (я не просто так отделил их комментариями в виде горизонтальных линий).

Над формулировками в теме тоже стоит поработать (то и дело просматривается тавтология) - если не против, могу с этим помочь.

Elrmrnt-Kritik
07.04.2018, 01:07
Нужно как минимум 9-10 значений в диапазоне, чтобы switch начал проигрывать
Не оговорка? Может выигрывать? Либо if, а не switch :)

DeimoS
07.04.2018, 10:16
Не оговорка? Может выигрывать? Либо if, а не switch :)

Не оговорка. Чем больший диапазон значений перебирается в switch, тем дольше он будет это делать, так как, например, в случае "case 0..10", switch не просто сравнит число с 0 и с 10, после выдав ответ, а будет сравнивать значение, указанное в switch 11 раз подряд (с 0, с 1, с 2, с 3, с 4 и т.п.).
Если же указать "if(0 <= var <= 10)", то это будет работать так же, как и "if(var >= 0 && var <= 10)", то бишь, будет только 2 сравнения, а не 11.

А в маленьких диапазонах switch выигрывает за счёт того, что значение переменной, указанной в switch, записывается в память один раз и потом сравнивается со всеми указанными в case значениями, когда, в случае с if, обращение к переменной происходит каждое сравнение (даже вот этот код: "if(0 <= var <= 10)" - на деле будет генерировать 2 отдельных обращения к переменной var, дабы сначала сравнить её с 0, а потом сравнить с 10). Иными словами, switch будет просто меньше инструкций генерировать и всё такое.

VVWVV
07.04.2018, 11:10
Диапазон из 4 значений как раз достаточно мал, чтобы оператор switch работал с ним быстрее, чем if. Нужно как минимум 9-10 значений в диапазоне, чтобы switch начал проигрывать - по крайней мере, это справедливо в случае со специфичным для GCC ядром интерпретатора.

Касаемо теста производительности, необязательно было выкладывать весь код - достаточно было просто указать ссылку на тему с тестом, а в своём посте указать только настройки (я не просто так отделил их комментариями в виде горизонтальных линий).

Над формулировками в теме тоже стоит поработать (то и дело просматривается тавтология) - если не против, могу с этим помочь.
Я ее написал за 10-20 минут, поскольку больше времени у меня не было. Конечно, я буду только рад исправлениям. Кстати, ты можешь переписать(удалить) ее в тему мифов.