C++ pimpl идиома тратит впустую инструкцию по сравнению со стилем C?

(Да, я знаю, что одна машинная команда обычно не имеет значения. Я задаю этот вопрос, потому что я хочу понять 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. Что может быть сделано?

6
задан Rob N 21 May 2010 в 08:19
поделиться

5 ответов

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

7
ответ дан 8 December 2019 в 14:41
поделиться

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

class ThingImpl;
class Thing
{
    ThingImpl *impl;
    static int calc(ThingImpl*);
public:
    Thing();
    int calc() { calc(impl); }
};
5
ответ дан 8 December 2019 в 14:41
поделиться

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

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

2
ответ дан 8 December 2019 в 14:41
поделиться

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

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

0
ответ дан 8 December 2019 в 14:41
поделиться

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

#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
ответ дан 8 December 2019 в 14:41
поделиться
Другие вопросы по тегам:

Похожие вопросы: