Как стать автором
Обновить

Правила по оптимизация кода (для начинающих)

Долго думая над темой для первой статьи, пришел к решению описать некоторые полезные правила по оптимизации C++ кода. Статья рассчитана для начинающих, но, возможно, освежит память засидевшимся программистам выше уровня Junior.

В современном мире начинающие программисты почти не уделяют времени на изучение каких-либо правил по оптимизации и времени выполнения программного кода. Ведь производительность компьютеров достигла высокого уровня и даже не оптимизированный код выполняется довольно быстро. Но квалификация программиста зависит не только от умения писать работающий код. С точки зрения железа, компьютер — это устройство, а у каждого устройства есть срок его работы. Если измерять время работы компьютера в количестве выполненных операций, то не оптимизированный код уменьшает жизнь устройству. Мы, как программисты, должны ориентироваться на конечного пользователя — значит должны думать не только о своем продукте, а и об устройстве нашего клиента. Так давайте не будем заставлять компьютеры перебирать бесконечное множество не нужных байт информации.

Правило 1: Всегда заменяйте операции умножения/деления на степень 2-ки, побитовым сдвигом влево/вправо.

Как известно операции умножения/деления одни из самых дорогостоящих операций, которые требуют большое количество тиков процессора. Если эти операции работают с числами степени 2, их необходимо заменить на операции сдвига. Это уменьшит время работы алгоритма, не заставляя процессор перебирать не нужные биты.

Пример:
int a = 16;
int b = 8; // 2 ^ 3 = 8
// int c = a * b;
int c = a << 3;
// int c = a / b;
int c = a >> 3;

Правило 2: Предпочитайте префиксный инкремент/декремент постфиксному.

Префиксный инкремент/декремент по эффективности работают быстрее, т. к. эти операторы возвращают ссылку, а не временный объект (как постфиксные). Если не нужно сохранять старое значение переменной, то лучше использовать префиксные операторы.

Пример:
int b = 5;
int a = b++; // переменная a будет хранить старое значение b = 5
int a = ++b; // а = новое значение b = 6

Правило 3: Предпочитайте операции с совмещенным присваиванием унарным.

Операции с совмещенным присваиванием: +=, -=, *=, /= ..., унарные операции: +, -, *, /…
Как и в правиле 2, эти операторы возвращают ссылку, а не временный объект.

Пример:
int a += b; // эффективнее и выглядит красивее, чем
int a = a + b;

Правило 4: Если возможно, заменяйте условия if else на if.

Условия — трудоемкая операция, которая разрывает конвейер команд. Если использовать if else, то переход выполнится в любом случаи. Использую простой if, переход будет только если выполнится условие.

Пример:
bool a = false;
/*if (a)
return false;
else
return true;*/
if (a)
return false;
return true;

Правило 5: Составные условия

a) с оператором &&, первым должно стоять то, которое будет выполняется реже.


Так как первым проверится первое условие и если оно ложно, то второе проверятся не будет.

Пример:
bool one, two;
if (one && two) {} // one = false, следовательно two не проверяем

b) с оператором ||, первым должно стоять то, которое будет выполняется чаще.

Так как первым проверится первое условие и если оно истинно, то второе проверятся не будет.

Пример:
bool one, two;
if (one || two) {} // one = true, следовательно two не проверяем

c) с && или || в составном условии параметры которого функции, то первым должна быть функция с меньшим временем выполнения.

Нужно учесть предыдущие правила и к ним добавить, что первым параметром должна быть функция с меньшим временем выполнения.

Пример:
if (func1() && func2()) // func1() — меньшая по времени функция и если вернет false, то func2() не нужно вызывать
if (func1() || func2()) // func1() — меньшая по времени функция и если вернет true, то func2() не нужно вызывать

Правило 6: switch, всегда можно заменить на константный массив.

Под последовательными значениями представляется:
switch (a) {
case 0:
case 1:
case 2:

}

Безусловно switch работает быстрее многоуровневых if else, но все-равно это условие. Для повышения производительности всегда можно воспользоваться константным массивом.

Пример:
int a, b = 0;
/*switch (a) {
case 0: b *= 10;
case 2: b *= 20;
case 3: b *= 30;

}*/
const int array[3] = {10, 0, 20, 30};
b *= array[a];

Правило 7: Не передавайте в функции большое количество параметров.

В идеале функция должна быть без параметров или с 1-м параметром. Большое количество параметров будет хранится в стеке, а не в регистрах процессора. Достичь этого можно используя структуру с параметрами и указатель или ссылку на неё.

Пример:
//int a, b, c, d;
struct data {
int a, b, c d
};

//func(a, b, c, d);
func(data* data);

Правило 8: Не используйте inline функции.

inline функции — это наследие языка C. Если функция объявлена как inline, то её тело должно быть подставлено в место её вызова, НО современные компиляторы вправе отказать функции в подстановке. Более того, теперь компиляторы сами могут сделать функцию как inline, без вашего ведома.

Правило 9: Если параметр функции не будет изменяться, стоит определить его как const.

Передавая параметр как const, компилятор может оптимизировать его, определив в памяти как только для чтения. Если в качестве параметра передается структура или класс, стоит передавать её по константной ссылке.

Пример:
class A;
void func(const A& a);

Правило 10: В структурах или классах определяя данные, стоит располагать их в порядке убывания.

Это правило поможет с экономить память за счет выравнивания типов в памяти. Данные-члены выравниваются в памяти по первому типу.

Пример:
struct A {
bool b;
int i;
double d;
}
// менее эффективно, чем
struct B {
double d;
int i;
bool b;
};

// sizeof(A) = 24
// sizeof(B) = 16

Правило 11: В конструкторе объекта всегда предпочитайте инициализацию, а не присваивание.

При выделении памяти по объект, конструктор всегда инициализирует её. Выполнять присваивание после инициализации неэффективно. Также к конструкторам относится Правило 7.

Пример:
class A {
public:
/*A() {
data = 0; // присваивание
}*/
A(): data(0) {} // инициализация
private:
int data;
};

Правило 12: Не объявляйте функцию виртуальной, если нет необходимости в её переопределении.

Виртуальные функции — это мало эффективный механизм, для получения полиморфизма. Все классы содержащие хотя бы одну виртуальную функцию неявно добавляют к своим данным указатель (vptr) на таблицу виртуальных функций (vtable). Определяя виртуальную функцию не переопределяя её в потомках не эффективно.

Правило 13: Функцию член не изменяющую данные класса, нужно объявить как const.

Функции члены, не изменяющие состояние объекта, должны быть объявлены как const, т. к. компилятор может оптимизировать такие функции.

Пример:
class A {
public:
int getData() const {
return data; // данные не изменяются
}
private:
int data;
};

Правило 14: Если функция член не использует данные класса, стоит определить её как статическая.

Функция член, не использующая данные класса, должна быть определена как статическая (static). Для вызова такой функции не нужно создавать объект, а так как данные класса этой функции не нужны, то и в объекте нет смысла.

Пример:
class A {
public:
A() {
++number;
}
~A() {
--number;
}
int getData() const {
return data;
}
static int getNumber() {
return number;
}
private:
int data;
static int number;
};

Большое спасибо, за внимание! До следующих статей по оптимизации кода.
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.
Изменить настройки темы