Общие оптимизации
Вот некоторые из моих любимых оптимизаций. Я фактически увеличил время выполнения и уменьшил размеры программы, используя их.
Объявление небольших функций как inline
или макросов
Каждый вызов функции (или метода) связан с накладными расходами, такими как помещение переменных в стек. Некоторые функции также могут повлечь за собой дополнительные расходы при возврате. Неэффективная функция или метод имеют меньше операторов в своем содержании, чем комбинированные издержки. Это хорошие кандидаты для встраивания, будь то макросы #define
или функции inline
. (Да, я знаю, что inline
- только предложение, но в этом случае я рассматриваю его как напоминание компилятору.)
Удалить мертвый и избыточный код
Если код не используется или не влияет на результат программы, избавьтесь от него.
Упрощение разработки алгоритмов
Однажды я удалил много кода сборки и времени выполнения из программы, записав алгебраическое уравнение, которое она вычисляла, и затем упростил алгебраическое выражение. Реализация упрощенного алгебраического выражения заняла меньше места и времени, чем исходная функция.
Развертывание петли
У каждого цикла есть издержки инкремента и проверки завершения. Чтобы получить оценку фактора производительности, подсчитайте количество инструкций в служебной информации (минимум 3: приращение, проверка, начало цикла) и разделите на количество операторов внутри цикла. Чем меньше число, тем лучше.
Редактировать: привести пример развертывания цикла
До:
unsigned int sum = 0;
for (size_t i; i < BYTES_TO_CHECKSUM; ++i)
{
sum += *buffer++;
}
После развертывания:
unsigned int sum = 0;
size_t i = 0;
**const size_t STATEMENTS_PER_LOOP = 8;**
for (i = 0; i < BYTES_TO_CHECKSUM; **i = i / STATEMENTS_PER_LOOP**)
{
sum += *buffer++; // 1
sum += *buffer++; // 2
sum += *buffer++; // 3
sum += *buffer++; // 4
sum += *buffer++; // 5
sum += *buffer++; // 6
sum += *buffer++; // 7
sum += *buffer++; // 8
}
// Handle the remainder:
for (; i < BYTES_TO_CHECKSUM; ++i)
{
sum += *buffer++;
}
В этом преимуществе получено вторичное преимущество: выполняется больше операторов, прежде чем процессор должен перезагрузить кэш команд.
У меня были потрясающие результаты, когда я развернул цикл до 32 операторов. Это было одним из узких мест, так как программе приходилось вычислять контрольную сумму для файла объемом 2 ГБ. Эта оптимизация в сочетании с чтением блоков улучшила производительность с 1 часа до 5 минут. Развертывание цикла обеспечивало отличную производительность и на ассемблере, мой memcpy
был намного быстрее, чем memcpy
компилятора. - Т.М.
Сокращение if
заявлений
Процессоры ненавидят переходы или скачки, так как это заставляет процессор перезагружать свою очередь инструкций.
Булева арифметика ( Отредактировано: применен формат кода к фрагменту кода, добавлен пример)
Преобразование if
операторов в логические назначения. Некоторые процессоры могут условно выполнять инструкции без ветвления:
bool status = true;
status = status && /* first test */;
status = status && /* second test */;
Короткое замыкание оператора Logical AND (&&
) предотвращает выполнение тестов, если status
равно false
.
Пример: * * одна тысяча шестьдесят пять
struct Reader_Interface
{
virtual bool write(unsigned int value) = 0;
};
struct Rectangle
{
unsigned int origin_x;
unsigned int origin_y;
unsigned int height;
unsigned int width;
bool write(Reader_Interface * p_reader)
{
bool status = false;
if (p_reader)
{
status = p_reader->write(origin_x);
status = status && p_reader->write(origin_y);
status = status && p_reader->write(height);
status = status && p_reader->write(width);
}
return status;
};
Выделение переменной фактора вне циклов
Если переменная создается на лету внутри цикла, переместите создание / выделение до цикла. В большинстве случаев переменную не нужно выделять во время каждой итерации.
Коэффициент константного выражения вне циклов
Если вычисление или значение переменной не зависит от индекса цикла, переместите его за пределы (до) цикла.
Ввод / вывод в блоках
Чтение и запись данных большими кусками (блоками). Больше лучше. Например, чтение одного октета за раз менее эффективно, чем чтение 1024 октетов за одно чтение.
Пример: * 1 081 *
static const char Menu_Text[] = "\n"
"1) Print\n"
"2) Insert new customer\n"
"3) Destroy\n"
"4) Launch Nasal Demons\n"
"Enter selection: ";
static const size_t Menu_Text_Length = sizeof(Menu_Text) - sizeof('\0');
//...
std::cout.write(Menu_Text, Menu_Text_Length);
Эффективность этой техники может быть продемонстрирована визуально. : -)
Не использовать printf
семейство для постоянных данных
Постоянные данные могут быть выведены с использованием блочной записи. Форматированная запись будет тратить время на сканирование текста для форматирования символов или обработки команд форматирования. См. Пример кода выше.
Форматирование в память, затем запись
Отформатируйте в массив char
, используя несколько sprintf
, затем используйте fwrite
. Это также позволяет разбивать макет данных на «постоянные секции» и переменные секции. Подумайте о mail-merge .
Объявить константный текст (строковые литералы) как static const
Когда переменные объявляются без static
, некоторые компиляторы могут выделять пространство в стеке и копировать данные из ПЗУ. Это две ненужные операции. Это можно исправить с помощью префикса static
.
Наконец, код, подобный компилятору
Иногда компилятор может оптимизировать несколько небольших операторов лучше, чем одна сложная версия. Также помогает написание кода для оптимизации компилятора. Если я хочу, чтобы компилятор использовал специальные инструкции по передаче блоков, я напишу код, который выглядит так, как будто он должен использовать специальные инструкции.