C ++ pimpl идиома тратит впустую инструкцию против стиля C? - PullRequest
6 голосов
/ 21 мая 2010

( Да, я знаю, что одна машинная инструкция обычно не имеет значения. Я задаю этот вопрос, потому что хочу понять идиому pimpl и использовать ее наилучшим образом; и потому что иногда я делаю это забота об одной машинной инструкции. )

В приведенном ниже примере кода есть два класса, Thing и OtherThing. Пользователи будут включать "thing.hh". Thing использует идиому pimpl, чтобы скрыть свою реализацию. OtherThing использует стиль C & ndash; функции, не являющиеся членами, которые возвращают и принимают указатели. Этот стиль производит немного лучший машинный код. я интересно: есть ли способ использовать стиль C ++ & ndash; т.е. сделать функции в функции-члены & ndash; и все же еще сохраните машинную инструкцию. Мне нравится этот стиль, потому что он не загрязняет пространство имен вне класса.

Примечание: я смотрю только на вызов функций-членов (в данном случае 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));
}

Makefile:

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. Вот соответствующий вывод:

Thing :: известково ():
0000000000000000 movq (% rdi),% rax
0000000000000003 movl (% rax),% eax
0000000000005000 вкл.% Eax
0000000000000007 рет
известково (OtherThing *):
0000000000000010 movl (% rdi),% eax
0000000000000012 вкл.% Eax
0000000000000014 рет

Обратите внимание на дополнительную инструкцию из-за косвенности указателя. Первая функция ищет два поля (impl, затем x), а вторая должна получить только x. Что можно сделать?

Ответы [ 5 ]

6 голосов
/ 21 мая 2010

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

5 голосов
/ 21 мая 2010

Не слишком сложно, просто используйте ту же технику в вашем классе. Любой на полпути приличный оптимизатор будет встроен тривиальная обертка.

class ThingImpl;
class Thing
{
    ThingImpl *impl;
    static int calc(ThingImpl*);
public:
    Thing();
    int calc() { calc(impl); }
};
2 голосов
/ 21 мая 2010

Я не согласен по поводу вашего использования: вы не сравниваете 2 одинаковые вещи.

#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));
}
  1. На самом деле у вас есть 2 вызова new, один явный, а другой неявный (выполняется конструктором Thing.
  2. У вас есть 1 новый здесь, неявный (внутри 2)

Вы должны разместить Thing в стеке, хотя это, вероятно, не изменит инструкцию двойной разыменования ..., но может изменить ее стоимость (убрать промах кэша).

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

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

Правильность важнее, чем производительность.

2 голосов
/ 21 мая 2010

Существует неприятный способ - заменить указатель на ThingImpl достаточно большим массивом беззнаковых символов, а затем поместить / переосмыслить новое / явно уничтожить объект ThingImpl.

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

0 голосов
/ 21 мая 2010

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

Наличие предметов в классах имеет гораздо больше преимуществ, чем просто инкапсуляция PIMPL - отличная идея, если вы забыли, как использовать ключевое слово private.

...