Подтвердить что ты не робот

Существуют ли методы для значительного улучшения времени построения С++ для 3D-приложений?

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

Я очень старомодный, так как мне нравится мой статически скомпилированный C или С++, и эти языки могут быть довольно длинными для компиляции на тех ноутбуках с ограниченными возможностями, особенно С++ 11 и С++ 14.

Мне нравится делать 3D-программирование, а библиотеки, которые я использую, могут быть большими и не будут прощать: пулевая физика, Ogre3D, SFML, не говоря уже о сильном голоде современных IDE.

Существует несколько решений, позволяющих сделать строительство быстрее:

  • Решение A: Не используйте эти большие библиотеки и придумывайте что-нибудь более легкое для облегчения компилятора. Напишите соответствующие make файлы, не используйте IDE.

  • Решение B: настройте сервер здания в другом месте, создайте файл makefile на мускулистой машине и автоматически загрузите полученный exe. Я не думаю, что это случайное решение, так как вам нужно настроить таргетинг на ваш ноутбук.

  • Решение C: используйте неофициальный модуль С++

  • ???

Любое другое предложение?

4b9b3361

Ответ 1

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

1. Предварительно скомпилированные заголовки.

Предварительно скомпилированный заголовок - это обычный заголовок (файл .h), который содержит наиболее распространенные объявления, typedefs и include. Во время компиляции он анализируется только один раз - перед компиляцией любого другого источника. Во время этого процесса компилятор генерирует данные некоторого внутреннего (наиболее вероятного, двоичного) формата. Затем он использует эти данные для ускорения генерации кода.

Это образец:

#pragma once

#ifndef __Asx_Core_Prerequisites_H__
#define __Asx_Core_Prerequisites_H__

//Include common headers
#include "BaseConfig.h"
#include "Atomic.h"
#include "Limits.h"
#include "DebugDefs.h"
#include "CommonApi.h"
#include "Algorithms.h"
#include "HashCode.h"
#include "MemoryOverride.h"
#include "Result.h"
#include "ThreadBase.h"
//Others...

namespace Asx
{

    //Forward declare common types
    class String;
    class UnicodeString;

    //Declare global constants
    enum : Enum
    {
        ID_Auto     = Limits<Enum>::Max_Value,
        ID_None     = 0
    };

    enum : Size_t
    {
        Max_Size            = Limits<Size_t>::Max_Value,
        Invalid_Position    = Limits<Size_t>::Max_Value
    };

    enum : Uint
    {
        Timeout_Infinite    = Limits<Uint>::Max_Value
    };

    //Other things...

}

#endif /* __Asx_Core_Prerequisites_H__ */

В проекте, когда используется PCH, каждый исходный файл обычно содержит #include к этому файлу (я не знаю о других, но в VC++ это фактически требование - каждый источник, подключенный к проекту, настроен для использования PCH, должен начинаться с: #include PrecompiledHedareName.h). Конфигурация предварительно скомпилированных заголовков очень зависит от платформы и выходит за рамки этого ответа.

Обратите внимание на один важный момент: вещи, которые определены/включены в PCH, следует менять только тогда, когда это абсолютно необходимо - каждый chnge может вызвать перекомпиляцию всего проекта (и других зависимых модулей)!

Подробнее о PCH:

Wiki
Док GCC
Документ Microsoft

2. Форвардные декларации.

Если вам не нужно полное определение класса, перешлите его, чтобы удалить ненужные зависимости в вашем коде. Это также подразумевает широкое использование указателей и ссылок, когда это возможно. Пример:

#include "BigDataType.h"

class Sample
{
protected:
    BigDataType _data;
};

Вам действительно нужно хранить _data как значение? Почему бы не так:

class BigDataType; //That enough, #include not required

class Sample
{
protected:
    BigDataType* _data; //So much better now
};

Это особенно выгодно для крупных типов.

3. Не злоупотребляйте шаблонами.

Мета-программирование - очень мощный инструмент в наборе инструментов для разработчиков. Но не пытайтесь использовать их, когда они не нужны.

Они отлично подходят для таких вещей, как черты характера, оценка во время компиляции, статическое отражение и так далее. Но они приносят много неприятностей:

  • Сообщения об ошибках - если вы когда-либо видели ошибки, вызванные неправильным использованием итераторов или контейнеров std:: (особенно сложных, таких как std::unordered_map), чем вы знаете, что это такое.
  • Удобочитаемость - сложные шаблоны могут быть очень сложными для чтения/изменения/обслуживания.
  • Причуды - многие методы, для которых используются шаблоны, не так хорошо известны, поэтому обслуживание такого кода может быть еще сложнее.
  • Время компиляции - самое важное для нас сейчас:

Помните, если вы определяете функцию как:

template <class Tx, class Ty>
void sample(const Tx& xv, const Ty& yv)
{
    //body
}

он будет скомпилирован для каждой исключительной комбинации Tx и Ty. Если такая функция используется часто (и для многих таких комбинаций), она действительно может замедлить процесс компиляции. Теперь представьте, что произойдет, если вы начнете чрезмерно использовать шаблоны для целых классов...

4. Использование идиомы PIMPL.

Это очень полезный метод, который позволяет нам:

  • скрыть детали реализации
  • ускорить генерацию кода
  • простые обновления, не нарушая код клиента

Как это работает? Рассмотрим класс, который содержит много данных (например, представляющих человека). Это может выглядеть так:

class Person
{
protected:
    string name;
    string surname;
    Date birth_date;
    Date registration_date;
    string email_address;
    //and so on...
};

Наше приложение развивается, и нам нужно расширить/изменить определение Person. Мы добавляем некоторые новые поля, удаляем другие... и все падает: размер Персоны меняется, имена полей меняются... Катаклизм. В частности, каждый клиентский код, который зависит от определения Person, должен быть изменен/обновлен/исправлен. Не хорошо.

Но мы можем сделать это умным способом - скрыть детали Person:

class Person
{
protected:
    class Details;
    Details* details;
};

Теперь мы делаем несколько приятных вещей:

  • клиент не может создать код, который зависит от того, как определен Person
  • перекомпиляция не требуется, если мы не изменяем открытый интерфейс, используемый клиентским кодом
  • мы сокращаем время компиляции, потому что определения string и Date больше не должны присутствовать (в предыдущей версии нам приходилось включать соответствующие заголовки для этих типов, что добавляет дополнительные зависимости).

5. Директива #pragma Once.

Несмотря на то, что это не может повысить скорость, оно более четкое и менее подвержено ошибкам. По сути, это то же самое, что использование include guard:

#ifndef __Asx_Core_Prerequisites_H__
#define __Asx_Core_Prerequisites_H__

//Content

#endif /* __Asx_Core_Prerequisites_H__ */

Предотвращает многократный анализ одного и того же файла. Хотя #pragma once не является стандартным (на самом деле, прагма отсутствует - прагмы зарезервированы для директив, специфичных для компилятора), он довольно широко поддерживается (примеры: VC++, GCC, CLang, ICC) и может использоваться, не беспокоясь - компиляторы должны игнорировать неизвестные прагмы (более или менее тихо).

6. Устранение ненужных зависимостей.

Очень важный момент! Когда код подвергается рефакторингу, зависимости часто меняются. Например, если вы решите провести некоторую оптимизацию и использовать указатели/ссылки вместо значений (см. пункт 2 и 4 этого ответа), некоторые включения могут стать ненужными. Рассмотрим:

#include "Time.h"
#include "Day.h"
#include "Month.h"
#include "Timezone.h"

class Date
{
protected:
    Time time;
    Day day;
    Month month;
    Uint16 year;
    Timezone tz;

    //...
};

Этот класс был изменен, чтобы скрыть детали реализации:

//These are no longer required!
//#include "Time.h"
//#include "Day.h"
//#include "Month.h"
//#include "Timezone.h"

class Date
{
protected:
    class Details;
    Details* details;

    //...
};

Хорошо бы отслеживать такие избыточные включения, используя либо мозг, встроенные инструменты (такие как VS Dependency Visualizer) или внешние утилиты (например, GraphViz).

Visual Studio также имеет очень хороший вариант - если вы щелкнете по RMB по любому файлу, вы увидите опцию "Создать график включаемых файлов" - он сгенерирует хороший, читаемый график, который можно легко проанализировать и использовать для отслеживания ненужных зависимостей.

Пример графика, созданного в моем файле String.h:

Sample Graph

Ответ 2

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

  • Используйте предварительно скомпилированные заголовки для любого заголовка, который вы не ожидаете изменить, включая заголовки операционной системы, заголовки сторонних библиотек и т.д.
  • Уменьшить количество заголовков, включенных в другие заголовки, до минимума.
    • Определите, требуется ли включение в заголовке или его можно переместить в файл cpp. Это иногда вызывает эффект пульсации, потому что кто-то другой зависел от вас, чтобы включить заголовок для него, но лучше в долгосрочной перспективе переместить включение туда, где оно действительно необходимо.
    • Использование передовых объявленных классов и т.д. часто может исключать необходимость включения заголовка, в котором объявлен этот класс. Конечно, вам все равно нужно включить заголовок в файл cpp, но это происходит только один раз, а не каждый раз, когда включен соответствующий заголовочный файл.
  • Использовать #pragma один раз (если он поддерживается вашим компилятором), а не включать символы защиты. Это означает, что компилятору даже не нужно открывать файл заголовка для обнаружения включенного защитника. (Разумеется, многие современные компиляторы все равно выходят за вас.)

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

Ответ 3

Если вы пробовали все вышеперечисленное, есть коммерческий продукт, который творит чудеса, предполагая, что у вас есть несколько доступных ПК в локальной сети. Раньше мы использовали его на предыдущей работе. Он назывался Incredibuild (www.incredibuild.com), и он сократил время сборки с более чем часа (С++) до 10 минут. На своем веб-сайте:

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

Ответ 4

Еще один момент, который не упоминается в других ответах: Шаблоны. Шаблоны могут быть хорошим инструментом, но у них есть фундаментальные недостатки:

  • Необходимо включить шаблон и все его шаблоны. Передовые декларации не работают.

  • Код шаблона часто компилируется несколько раз. Во сколько .cpp файлов вы используете std::vector<>? Это то, сколько раз ваш компилятор должен будет скомпилировать его!

    (Я не выступаю против использования std::vector<>, напротив, вы должны использовать его часто, это просто пример действительно часто используемого шаблона здесь.)

  • При изменении реализации шаблона вы должны перекомпилировать все, что использует этот шаблон.

С шаблоном тяжелого кода вы часто имеете относительно небольшое количество единиц компиляции, но каждый из них огромен. Конечно, вы можете пойти все-шаблон и иметь только один .cpp файл, который тянет во всем. Это позволило бы избежать многократной компиляции кода шаблона, однако он делает make бесполезным: любая компиляция займет до тех пор, пока компиляция после чистой.

Я бы порекомендовал идти в противоположном направлении: избегайте создания шаблонов или только шаблонов и избегайте создания сложных шаблонов. Чем более взаимозависимыми становятся ваши шаблоны, тем более повторная компиляция выполняется, и чем больше файлов .cpp нужно перестраивать при изменении шаблона. В идеале любой шаблон, который у вас есть, не должен использовать какой-либо другой шаблон (если только этот другой шаблон не является std::vector<>, конечно...).