Странный паттерн C ++ для сокращения времени компиляции

Я нашел в OpenSource-коде Tizen Project шаблон, который может сократить время компиляции проекта. Он используется во многих местах проекта.

В качестве примера я выбрал одно название класса ClientSubmoduleSupport, Это короткий. Вот их источники: client_submode_support.h, client_submode_support.cpp.

Как вы можете видеть на client_submode_support.h это определено ClientSubmoduleSupport а также client_submode_support.cpp там определено ClientSubmoduleSupportImplementation класс, который делает работу для ClientSubmoduleSupport,

Вы знаете эту модель? Мне любопытны плюсы и минусы этого подхода.

4

Решение

Эта картина называетсяМост«, также известный как»Идиома».

Намерение:
«отделить абстракцию от ее реализации, так что оба могут варьироваться независимо»

Souce: книга «Банды четырех».

6

Другие решения

Как sergej уже упоминалось, это Pimpl идиома, которая также является подмножеством Bridge шаблон дизайна.

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

C Перспектива

В C довольно распространенная практика иметь непрозрачный указатель на объявленный вперед struct, вот так:

// Foo.h:
#ifndef FOO_H
#define FOO_H

struct Foo* foo_create(void);
void foo_destroy(struct Foo* foo);
void foo_do_something(struct Foo* foo);

#endif

// Foo.c:
#include "Foo.h"
struct Foo
{
// ...
};

struct Foo* foo_create(void)
{
return malloc(sizeof(struct Foo));
}

void foo_destroy(struct Foo* foo)
{
free(foo);
}

void foo_do_something(struct Foo* foo)
{
// Do something with foo's state.
}

Это несет в себе аналогичные плюсы / минусы Pimpl но с одним дополнительным профи для C. В C нет private спецификатор для structsделая это единственным способом скрыть информацию и предотвратить доступ к struct внутренности из внешнего мира. Таким образом, он стал средством скрытия и предотвращения доступа к внутренним частям.

В C ++ есть то, что приятно private спецификатор, позволяющий нам предотвратить доступ к внутренним объектам, но мы не можем полностью скрыть их видимость от внешнего мира, если не будем использовать что-то вроде Pimpl который в основном оборачивает такую ​​идею C непрозрачных указателей в заранее объявленный UDT в форме class с одним или несколькими конструкторами и деструктором.

КПД

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

[Opaque Pointer]-------------------------->[Internal Data Fields]

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

С этим представлением мы также больше не можем просто выделить
все, что нам нужно в стеке. Только указатель может быть назначен
стек, в то время как внутренние компоненты должны быть размещены в куче.

Затраты на производительность, связанные с этим, имеют тенденцию быть наиболее заметными, если мы храним массив из набора этих дескрипторов (в C сам непрозрачный указатель, в C ++ объект, содержащий один). В таком случае мы получаем массив, скажем, миллион указателей, которые потенциально могут указывать повсеместно, и в итоге мы платим за это в виде увеличения количества сбоев страниц, кеша и кучи (бесплатный магазин) распределение / перераспределение накладных расходов.

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

Эффективность: фиксированный распределитель

Один из способов значительно снизить (но не устранить) эти затраты — использовать, скажем, фиксированный распределитель O (1), который обеспечивает более непрерывную структуру памяти для внутренних устройств. Это может существенно помочь в тех случаях, когда мы работаем с массивом Foosнапример, с помощью распределителя, который позволяет Foo внутреннее устройство должно храниться с (более) непрерывным расположением памяти (улучшая локальность ссылок).

Эффективность: массовый интерфейс

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

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

«Если вы хотите пиковой производительности, вы должны быть накачаны! Наберите эти интерфейсы! Доберитесь до чоппы!» — Мнимый совет Арни после того, как вставил отвертку в чей-то яремный аппарат.

Легкие заголовки

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

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

Так что пока Pimpls есть некоторые затраты времени выполнения, они снижают стоимость времени сборки. Даже в самых критичных для производительности областях большая часть сложной кодовой базы будет способствовать производительности больше, чем максимальная эффективность времени выполнения. С точки зрения производительности, длительное время сборки может быть убийственным, поэтому обмен небольшим снижением производительности во время выполнения для производительности сборки может быть хорошим компромиссом.

Каскадные изменения

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

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

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

API и ABI

Один менее очевидный аргумент (но весьма существенный в контексте API) — это когда вы выставляете API для разработчиков плагинов (в том числе сторонних разработчиков, пишущих исходный код вне вашего контроля), например, В таком случае, если вы выставите внутреннее состояние class или же struct таким образом, что ручки, к которым получают доступ плагины, напрямую включают эти внутренние компоненты, мы получаем очень хрупкий ABI. Двоичные зависимости могут начать напоминать эту природу:

[Plugin Developer]----------------->[Internal Data Fields]

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

Здесь непрозрачный указатель (Pimpl в том числе) вводит посредника, который защищает нас от таких поломок ABI.

[Plugin Developer]----->[Opaque Pointer]----->[Internal Data Fields]

… и это может очень сильно повлиять на обратную совместимость плагинов, когда вы теперь можете свободно менять частные внутренние компоненты, не рискуя такими поломками плагинов.

Плюсы и минусы

Вот краткое изложение плюсов и минусов, а также несколько дополнительных минусов:

Плюсы:

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

Минусы:

  • Эффективность во время выполнения (снижается за счет более объемных интерфейсов или эффективных фиксированных распределителей).
  • Незначительный: немного больше стандартного кода для чтения / записи для разработчиков (хотя нет дублирования какой-либо нетривиальной логики).
  • Не может применяться к шаблонам классов, которые требуют, чтобы их полное определение было видно на сайте, на котором генерируется код.

TL; DR

Так или иначе, выше приведено краткое введение в эту идиому вместе с некоторой историей и параллелями с практиками, предшествовавшими ей в C.

3

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

(Я видел, как требования стабильности API записывались в юридические контракты)

0

Использование этого шаблона для сокращения времени компиляции широко обсуждается в
Дж. Лакос. «Крупномасштабный дизайн программного обеспечения C ++» (Addison-Wesley, 1996).

Херб Саттер также обсудил достоинства этого подхода.
Вот.

0
По вопросам рекламы [email protected]