(Да, я знаю, что одна машинная команда обычно не имеет значения. Я задаю этот вопрос, потому что я хочу понять pimpl идиому и использовать ее самым лучшим способом; и потому что иногда я действительно забочусь об одной машинной команде.)
В примере кода ниже, существует два класса, Thing
и OtherThing
. Пользователи включали бы "thing.hh". Thing
использует pimpl идиому для сокрытия, это - реализация. OtherThing
использует стиль C – не являющиеся членом функции, которые возвращают и берут указатели. Этот стиль производит немного лучший машинный код. Я задаюсь вопросом: есть ли способ использовать стиль C++ – т.е., превратить функции в функции членства – и все же все еще сохранить машинную команду. Мне нравится этот стиль, потому что он не загрязняет пространство имен вне класса.
Примечание: Я только смотрю на вызов функций членства (в этом случае, calc
). Я не смотрю на объектное выделение.
Ниже файлы, команды и машинный код, на моем Mac.
thing.hh:
class ThingImpl;
class Thing
{
ThingImpl *impl;
public:
Thing();
int calc();
};
class OtherThing;
OtherThing *make_other();
int calc(OtherThing *);
thing.cc:
#include "thing.hh"
struct ThingImpl
{
int x;
};
Thing::Thing()
{
impl = new ThingImpl;
impl->x = 5;
}
int Thing::calc()
{
return impl->x + 1;
}
struct OtherThing
{
int x;
};
OtherThing *make_other()
{
OtherThing *t = new OtherThing;
t->x = 5;
}
int calc(OtherThing *t)
{
return t->x + 1;
}
main.cc (только для тестирования кода на самом деле работает...),
#include "thing.hh"
#include <cstdio>
int main()
{
Thing *t = new Thing;
printf("calc: %d\n", t->calc());
OtherThing *t2 = make_other();
printf("calc: %d\n", calc(t2));
}
Make-файл:
all: main
thing.o : thing.cc thing.hh
g++ -fomit-frame-pointer -O2 -c thing.cc
main.o : main.cc thing.hh
g++ -fomit-frame-pointer -O2 -c main.cc
main: main.o thing.o
g++ -O2 -o $@ $^
clean:
rm *.o
rm main
Выполненный make
и затем посмотрите на машинный код. На Mac я использую otool -tv thing.o | c++filt
. На Linux я думаю, что это objdump -d thing.o
. Вот соответствующий вывод:
Вещь:: calc ():
0000000000000000 movq (%rdi), %rax
0000000000000003 movl (%rax), %eax
0000000000000005 incl %eax
0000000000000007 мочат
calc (OtherThing*):
0000000000000010 movl (%rdi), %eax
0000000000000012 incl %eax
0000000000000014 мочат
Заметьте дополнительную инструкцию из-за косвенности указателя. Первая функция ищет два поля (impl, затем x), в то время как второе только должно получить x. Что может быть сделано?
Одна инструкция редко вызывает беспокойство. Во-первых, компилятор может кэшировать pImpl в более сложном сценарии использования, таким образом амортизируя стоимость в реальном сценарии. Во-вторых, конвейерные архитектуры делают практически невозможным прогнозирование реальной стоимости в тактовых циклах. Вы получите гораздо более реалистичное представление о стоимости, если запустите эти операции в цикле и рассчитаете разницу во времени.
Не слишком сложно, просто используйте ту же технику внутри своего класса. Любой наполовину достойный оптимизатор будет встроен банальная обертка.
class ThingImpl;
class Thing
{
ThingImpl *impl;
static int calc(ThingImpl*);
public:
Thing();
int calc() { calc(impl); }
};
Есть неприятный способ, который заключается в замените указатель на ThingImpl
достаточно большим массивом беззнаковых символов, а затем разместите / переинтерпретируйте приведение / явно уничтожьте объект ThingImpl
.
Или вы можете просто передать Thing
по значению, так как оно не должно быть больше, чем указатель на ThingImpl
, хотя может потребоваться немного больше (подсчет ссылок из ThingImpl
приведет к невозможности оптимизации, поэтому вам нужен способ пометить «владеющую» Thing
, что может потребовать дополнительного места на некоторых архитектурах).
Пусть об этом позаботится компилятор. Он знает гораздо больше о том, что на самом деле быстрее или медленнее, чем мы. Особенно в таком минутном масштабе.
Наличие элементов в классах дает гораздо больше преимуществ, чем просто инкапсуляция. PIMPL - отличная идея, если вы забыли, как использовать ключевое слово private.
Я не согласен с вашим использованием: вы не сравниваете две одинаковые вещи.
#include "thing.hh"
#include <cstdio>
int main()
{
Thing *t = new Thing; // 1
printf("calc: %d\n", t->calc());
OtherThing *t2 = make_other(); // 2
printf("calc: %d\n", calc(t2));
}
Thing
. Вы должны разместить Thing
в стеке, хотя это, вероятно, не изменит инструкцию двойного разыменования ... но может изменить ее стоимость (удалить промах в кэше).
Однако главное в том, что Thing
управляет своей памятью самостоятельно, поэтому вы не можете забыть удалить фактическую память, в то время как вы определенно можете это сделать с помощью метода в стиле C.
Я бы сказал, что автоматическая обработка памяти стоит потраченных усилий. дополнительная инструкция памяти, в частности потому, что, как было сказано, разыменованное значение, вероятно, будет кэшировано, если вы обратитесь к нему более одного раза, что практически ничего не даст.
Корректность важнее производительности.