БлогNot. Как применить настройки оптимизации GCC в QT?

Как применить настройки оптимизации GCC в QT?

GCC/CNU - классический образец оптимизирующего компилятора, но в норме он предназначен для Unix-систем.

Тем не менее, многое из того, что написано про GCC, можно выполнить и под Windows.

Самый очевидный путь - установить QT, бесплатный IDE для C++ с компилятором MinGW.

MinGW - это программный порт GCC для Windows.

Почти все хитрости из этой статьи для новых сборок QT уже не понадобятся, но убедитесь, что в пути к QT нет пробелов и символов кириллицы, а компилятором служит именно MinGW.

Создать проект можно как описано здесь (шаги 1-5), только на шаге 1 нужно выбрать "Консольное приложение QT" и тогда шага 4 не будет.

Окно из меню Инструменты - Параметры позволяет проверить, каким компилятором будет собираться активный проект:

выбор компилятора в QT Creator
выбор компилятора в QT Creator

Код приложения пишется в файле main.cpp, автоматически сгенерированный код можно удалить. В приложении QT Creator опции компиляции указываются в секции QMAKE_CXXFLAGS файла проекта .pro, а опции сборки - в секции QMAKE_LFLAGS, например:

QT       += core
QT       -= gui
TARGET = Timer
CONFIG   += console
CONFIG   -= app_bundle
TEMPLATE = app
SOURCES += main.cpp
QMAKE_CXXFLAGS += -Ofast

Обратите внимание на знак += и правильную запись опции после дефиса.

После применения новых опций к файлу проекта нужно выполнить в QT Creator команду меню Сборка - Очистить проект [ИМЯ_ПРОЕКТА]

В случае необходимости перед запуском проекта с изменёнными опциями также можно удалить папку Build-[ИМЯ_ПРОЕКТА] из папки проектов QT:

удаление папки Build проекта
удаление папки Build проекта

Обращайте внимание на путь к папке проекта - в нём не должно быть пробелов, кириллицы и каких-либо ещё посторонних символов. Лучше всего, если там только латинские буквы и/или цифры и подчёркивания.

Если получили сообщение

Процесс qmake.exe завершился с кодом 2. Ошибка при сборке/установке проекта *** (комплект: ***) Во время выполнения этапа "qmake"

скорее всего, причина в этом. Поменяйте путь на нормальный.

Во-вторых, попытайтесь удалить файл *.pro.user из папки проекта, папку build проекта, а затем вновь скомпилируйтесь.

Теперь о самой оптимизации.

Число оптимизирующих преобразований в современном компиляторе велико. Явное задание всех необходимых оптимизирующих преобразований было бы громоздким. Поэтому вводится понятие уровня оптимизации как множества используемых оптимизирующих преобразований. Как правило, компиляторы имеют несколько уровней оптимизации. Например, в GCC есть следующие уровни.

1. На уровне O0 почти все оптимизации отключены. Компиляция выполняется быстрее, чем на любом другом уровне оптимизации. При необходимости производить отладку программы или изучать ассемблерный листинг сгенерированного кода данный уровень оптимизации предпочтителен, так как получаемый листинг проще в понимании по сравнению с листингами для других уровней оптимизации.

2. На уровне O1 включены оптимизации для уменьшения размера бинарного исполняемого файла и такие оптимизации, уменьшающие время работы программы, которые не сильно замедляют работу компилятора.

3. На уровне O2 включены практически все доступные оптимизации, кроме тех, что ускоряют вычисления за счет увеличения размера кода.

4. В уровне O3 включены все оптимизации из уровня O2, к ним добавлены оптимизации времени работы программы, которые могут приводить к увеличению размера бинарного исполняемого файла.

5. Уровень Os служит для оптимизации размера программ, в него включено подмножество оптимизаций из уровня O2.

6. Уровень Ofast включает все оптимизации уровня O3, а также ряд других, таких как использование более быстрых и менее точных математических функций.

7. Уровень Og производит все оптимизации, которые сохраняют возможность просмотра стека вызовов, фрагментов исходного текста программы, относящихся к разным уровням этого стека, и возможность приостановки программы для каждой строки исходного текста, содержащей операторы. Для многих программ оптимизация следующего уровня не дает выигрыша по скорости в сравнении с предыдущим. В ряде случаев использование уровня оптимизации O3 приводит к генерации более медленной программы по сравнению с уровнем оптимизации O2. Общий подход к выбору уровня сводится к замерам времени исполнения программы, скомпилированной для каждого из этих уровней.

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

Список всех ключей оптимизации с аннотацией можно напечатать с помощью ключа -- help=optimizers. Рассмотрим примеры некоторых оптимизирующих преобразований, применяемых в GCC.

Удаление мертвого кода (dead code elimination) – преобразование, удаляющее фрагменты кода, которые не влияют на результат программы. К мертвому коду относят код, который не исполняется ни при каких условиях, и код, изменяющий значения переменных, которые никогда не используются. Это преобразование уменьшает размер исполняемого кода и иногда уменьшает время исполнения, так как исключает выполнение команд, не влияющих на результат. Преобразование включается ключами -fdce, -fdse, -ftree-dce, -ftree-builtin-call-dce, последнее из которых активно на уровнях оптимизации O2, O3, а остальные – на всех уровнях оптимизации кроме O0.

Отображение переменных на регистры процессора. При отсутствии оптимизаций компилятор отображает данные программы в оперативную память. В таком случае для каждого их чтения или записи происходит доступ к памяти. Если же данные имеют небольшой размер, то компилятор может отобразить их на регистры. Компилятор GCC отображает локальные переменные на регистры. Компилятор Compaq C Compiler для архитектуры Alpha может отображать на регистры небольшие массивы. Преобразование доступно на уровнях O1, O2, O3.

Раскрутка циклов включается ключами GCC -funroll-loops, -funroll-all-loops. Исходный цикл преобразуется в другой цикл, в котором одно тело цикла содержит несколько тел старого цикла. При этом счетчик цикла меняется соответственно. Эта оптимизация может уменьшать время исполнения за счет того, что уменьшается количество команд проверки условия выхода из цикла и команд условного перехода, которые могут приводить к приостановке конвейера команд. Однако, иногда раскрутка цикла приводит к увеличению времени исполнения программы. Другой недостаток преобразования – увеличение размера результирующего кода.

Встраивание функций. При использовании этого преобразования вместо вызова функции в код встраивается тело функции. При этом ценой разросшегося кода устраняются расходы на вызов функции и передачу аргументов. Встраивание функций, размер кода которых меньше или приблизительно равен размеру кода их вызова, включается ключом -finline-small-functions (включено по умолчанию на уровнях оптимизации O2, O3). Оптимизация по встраиванию более крупных функций включается с помощью ключей -finline-functions (включено на O3), -finline-functions-called-once (не включено только на O0), -findirect-inlining (включено на O2, O3). Кроме этих основных ключей есть дополнительные ключи для настройки параметров преобразования. Например, -finline-limit задает максимальный размер функций, которые следует встраивать.

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

Использование расширений процессора – группа специфичных для платформы преобразований. При генерации кода используются дополнительные команды, специфичные для данной архитектуры. В результате код может получиться более быстрым, особенно при векторизации вычислений, однако может потерять переносимость, т.е. не будет функционировать на процессорах других версиях архитектуры.

Вынос инвариантных вычислений за циклы. Если в цикле присутствуют вычисления, которые не зависят от итерации цикла, то они выносятся за цикл и тем самым многократно не повторяются.

Перепрыгивание переходов. Если в программе имеется цепочка последовательных переходов (условных или безусловных), она заменяется на единственный переход, который ведет сразу в окончательный пункт назначения, минуя промежуточные переходы. Преобразование включается ключом -fcrossjumping и активно на уровнях O2, O3.

Устранение несущественных проверок указателей на NULL. Считается, что обращение по нулевому указателю всегда приведёт к исключению (и аварийной остановке программы). Поэтому, если в коде встречается проверка указателя на ноль после обращения по этому адресу, то такая проверка из кода исключается, так как указатель заведомо не нулевой, если исполнение дойдёт до этой точки. Оптимизирующее преобразование включается ключом -fdelete-null-pointer-checks и выключается -fno-delete-null-pointer-checks. Для платформ x86, x86_64 преобразование активно на всех уровнях, включая O0.

Сработает ли указание опцией, я проверял на программе, содержащий несложный класс Timer для измерения промежутков времени и код для вычисления определённого интеграла методом средних прямоугольников (с малым шагом) в качестве "измеряемого" вычислительного процесса.

С опцией -O0 время выполнения оказывается почти вдвое большим, чем с опцией -Ofast

Вот текст файла main.cpp и архив .zip с проектом.

#include <iostream>
#include <ctime>
#include <cmath>
using namespace std;

class Timer {
protected:
 clock_t timeStart, timeEnd;
public:
 Timer(): timeStart(clock()) {};
 void start () { this->timeStart = clock(); };
 double end() {
  clock_t t = clock()-timeStart;
  return ((double)t)/CLOCKS_PER_SEC;
 };
};

double f(double x) { return sin(x); }

int main() {
 Timer t;
 t.start();

 //Реализация вычислительного процесса
 const double _PI = 3.14159265358979323846;
 double s = 0, dx = 0.000001;
 for (double x=0; x<=2*_PI; x+=dx) {
     s += dx*f(x+dx/2);
 }
 cout.precision(15);
 cout << s << endl;

 cout.precision(8);
 cout << t.end();

 cin.get();
 return 0;
}

 Скачать архив .zip с папкой проекта QT5 для этого примера (1 Кб)

26.05.2018, 15:33 [7855 просмотров]


теги: программирование windows c++ время qt unix

К этой статье пока нет комментариев, Ваш будет первым