Я нашел в OpenSource-коде Tizen Project шаблон, который может сократить время компиляции проекта. Он используется во многих местах проекта.
В качестве примера я выбрал одно название класса ClientSubmoduleSupport
, Это короткий. Вот их источники: client_submode_support.h
, client_submode_support.cpp
.
Как вы можете видеть на client_submode_support.h
это определено ClientSubmoduleSupport
а также client_submode_support.cpp
там определено ClientSubmoduleSupportImplementation
класс, который делает работу для ClientSubmoduleSupport
,
Вы знаете эту модель? Мне любопытны плюсы и минусы этого подхода.
Эта картина называетсяМост«, также известный как»Идиома».
Намерение:
«отделить абстракцию от ее реализации, так что оба могут варьироваться независимо»Souce: книга «Банды четырех».
Как sergej
уже упоминалось, это Pimpl
идиома, которая также является подмножеством Bridge
шаблон дизайна.
Но я хотел дать перспективу 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) — это когда вы выставляете API для разработчиков плагинов (в том числе сторонних разработчиков, пишущих исходный код вне вашего контроля), например, В таком случае, если вы выставите внутреннее состояние class
или же struct
таким образом, что ручки, к которым получают доступ плагины, напрямую включают эти внутренние компоненты, мы получаем очень хрупкий ABI. Двоичные зависимости могут начать напоминать эту природу:
[Plugin Developer]----------------->[Internal Data Fields]
Одна из самых больших проблем здесь заключается в том, что если вы вносите какие-либо изменения в эти внутренние состояния, ABI для внутреннего разрыва, от которого плагины напрямую зависят от работы. Практический результат: теперь у нас есть куча плагинов, написанных, возможно, разными людьми для нашего продукта, которые больше не работают, пока не будут опубликованы новые версии для нового ABI.
Здесь непрозрачный указатель (Pimpl
в том числе) вводит посредника, который защищает нас от таких поломок ABI.
[Plugin Developer]----->[Opaque Pointer]----->[Internal Data Fields]
… и это может очень сильно повлиять на обратную совместимость плагинов, когда вы теперь можете свободно менять частные внутренние компоненты, не рискуя такими поломками плагинов.
Вот краткое изложение плюсов и минусов, а также несколько дополнительных минусов:
Плюсы:
Минусы:
Так или иначе, выше приведено краткое введение в эту идиому вместе с некоторой историей и параллелями с практиками, предшествовавшими ей в C.
Вы будете использовать этот шаблон в основном при написании кода для библиотеки, которая используется сторонними разработчиками, и вы никогда не сможете изменить API. Это дает вам свободу изменять базовую реализацию функции, не требуя, чтобы ваши клиенты перекомпилировали свой код, когда они используют новую версию вашей библиотеки.
(Я видел, как требования стабильности API записывались в юридические контракты)
Использование этого шаблона для сокращения времени компиляции широко обсуждается в
Дж. Лакос. «Крупномасштабный дизайн программного обеспечения C ++» (Addison-Wesley, 1996).
Херб Саттер также обсудил достоинства этого подхода.
Вот.