как сократить количество vtbl
От: qaz77  
Дата: 21.04.17 19:30
Оценка:
ДВС, коллеги.

Пользователи моей софтины умудрились так размножить мелкие объекты, что довели бедняжку до bad_alloc (32 битная арх.).
Соответственно встала задача оптимизации потребления памяти.

Вскрытие показало, что размер объектика (честный sizeof, не глубокий) 140 байт.
Из которых 2 х 28 байт занимают 2 std::basic_string — от них я легко избавлюсь.
Но есть еще 9 x 4 = 36 байт виртуальных таблиц, и вот здесь вопрошаю совета, что с ними делать.
Желательно уменьшить размер объекта, т.к. сейчас sizeof(Bar)*instance_count > 500 Mb.

Класс выглядит следующим образом:
class Bar: public IBar1,
           public IBar2,
           ...
           public IBar9
{
  int ref_count;
  unsigned flags;
  ...    
  // реализация IBar1
  virtual void foo1() override;
  ...
};

Есть ли какой-нибудь паттерн/хак, чтобы "упаковать" несколько виртуальных таблиц в одну.
Или использовать NVI + что-то?

Избавиться от IBar1 ... IBar9 тяжело, т.к. есть много зависимого кода, который их активно использует.
Re: как сократить количество vtbl
От: Erop Россия  
Дата: 21.04.17 19:51
Оценка: 1 (1)
Здравствуйте, qaz77, Вы писали:

Q>Избавиться от IBar1 ... IBar9 тяжело, т.к. есть много зависимого кода, который их активно использует.


Я верно понимаю, что методы IBar1, ... IBar9 не пересекаются и ты можешь их менять?..

А ещё такой вопрос: класс Bar -- он твой?

То есть у тебя всё выглядит примерно так:
struct IBar1 { virtual void Foo1() {} };
struct IBar2 { virtual void Foo2() {} };
struct IBar3 { virtual void Foo3() {} };

struct CBar : IBar1, IBar2, IBar3 {
    void Foo1() { /*реализация IBar1::Foo1*/ }
    void Foo2() { /*реализация IBar2::Foo2*/ }
    void Foo3() { /*реализация IBar3::Foo3*/ }
};


Если таки да, то можно пойтить на такой трюк:

struct ISuperBar {
    virtual void Foo1() {}
    virtual void Foo2() {}
    virtual void Foo3() {}
};

class IBar1 : ISuperBar {
public:
    static IBar1* To(ISuperBar* p) { return static_cast<IBar1*>(p); }
    using ISuperBar::Foo1;
};

class IBar2 : ISuperBar {
public:
    static IBar2* To(ISuperBar* p) { return static_cast<IBar2*>(p); }
    using ISuperBar::Foo2;
};

class CBar : ISuperBar {
public:
    operator IBar1*() { return IBar1::To(this); }
    operator IBar2*() { return IBar2::To(this); }

    void Foo1() { /*реализация IBar1::Foo1*/ }
    void Foo2() { /*реализация IBar2::Foo2*/ }
    void Foo3() { /*реализация IBar3::Foo3*/ }

};

CBar bar;
IBar2* pBar2 = bar;
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Отредактировано 21.04.2017 19:53 Erop . Предыдущая версия .
Re[2]: как сократить количество vtbl
От: qaz77  
Дата: 21.04.17 20:15
Оценка:
Здравствуйте, Erop, Вы писали:

E>Я верно понимаю, что методы IBar1, ... IBar9 не пересекаются и ты можешь их менять?..

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

E>А ещё такой вопрос: класс Bar -- он твой?

Мой, но это класс реализации, типа Bar_impl, который не виден клиентам.
Объявлен в приватном хидере, а IBarN — публичные интерфейсы.

E>То есть у тебя всё выглядит примерно так:

Все так.

Но вот это клиенту не будет видно:
E>    operator IBar1*() { return IBar1::To(this); }
E>    operator IBar2*() { return IBar2::To(this); }



Если только выставить в паблик ISuperBar с
virtual operator IBar1*() = 0;
virtual operator IBar2*() = 0;

так можно вообще?
Re[3]: как сократить количество vtbl
От: Erop Россия  
Дата: 21.04.17 20:39
Оценка: 1 (1)
Здравствуйте, qaz77, Вы писали:

Q>Если только выставить в паблик ISuperBar с

Q>
Q>virtual operator IBar1*() = 0;
Q>virtual operator IBar2*() = 0;
Q>

Q>так можно вообще?

Лучше так:
virtual operator IBar1*() { return 0; }
virtual operator IBar2*() { return 0; }


QueryInterface знаешь?...

Можно не химичить с приватным наследованием, и сделать FooN protected в ISuperBar, тогда NVI-обёртки IBarN можно выводить из ISuperBar публично, а нужные FooN поднимать в public через using.

Соответственно, вместо того, что бы выводить MDT из IBar1, ... IBar9 можно будет вывести из ISuperBar, а то, что IBarN поддержан выражать, переопределяя соответствующий operator IBarN*

Да, это всё, конечно UB или где-то рядом, так как мы приводим ISuperBar* к IBarN*, хотя MDT-объект наследником IBarN не является.
Работать будет если в IBarN нет ничего, кроме using и статических методов.
При этом всё равно нельзя будет между IBarN ходить по dynamic_cast, зато можно будет прямо operator IBarN*() позвать
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re[4]: как сократить количество vtbl
От: qaz77  
Дата: 21.04.17 21:28
Оценка:
Здравствуйте, Erop, Вы писали:

E>QueryInterface знаешь?...

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

E>При этом всё равно нельзя будет между IBarN ходить по dynamic_cast, зато можно будет прямо operator IBarN*() позвать

Идея понятна, но в свете избавления от хранения указателей на кучу vtbl возникает вопрос, кто же все таки реализует интерфейс IBarN и как индекс функции
в vtbl ISuperBar преобразуется в индекс функции в vtbl IBarN?
Re[3]: как сократить количество vtbl
От: Zhendos  
Дата: 22.04.17 00:59
Оценка: +1
Здравствуйте, qaz77, Вы писали:

Q>Здравствуйте, Erop, Вы писали:


E>>Я верно понимаю, что методы IBar1, ... IBar9 не пересекаются и ты можешь их менять?..

Q>Не пересекаются. Менять могу не особо, т.к. клиентский код на них завязан.

E>>А ещё такой вопрос: класс Bar -- он твой?

Q>Мой, но это класс реализации, типа Bar_impl, который не виден клиентам.

А как клиенты получают доступ к нужному им интерфейсу?
Есть какие-то функции получающие на вход "opaque" тип и выдающие IBarX?

Можно ведь сделать так:
#include <iostream>

using std::cout;

struct IFoo1 {
    virtual void foo1() = 0;
};

struct IFoo2 {
    virtual void foo2() = 0;
};

struct Foo {
    void foo1() {}
    void foo2() {}
};

struct ImplementInterfaces : public IFoo1, IFoo2 {
    ImplementInterfaces(Foo &foo): foo_(foo) {}
    void foo1() override { foo_.foo1(); }
    void foo2() override { foo_.foo2(); }
private:
    Foo &foo_;
};


int main() {
    cout << sizeof(Foo) << "\n";
}


И в нужных местах создавать `ImplementInterfaces`, при этом ваш класс `Foo` будет минимального размера.
Отредактировано 22.04.2017 8:22 Zhendos . Предыдущая версия .
Re[4]: как сократить количество vtbl
От: Erop Россия  
Дата: 22.04.17 19:31
Оценка:
Здравствуйте, Zhendos, Вы писали:

Z>И в нужных местах создавать `ImplementInterfaces`, при этом ваш класс `Foo` будет минимального размера.


Это если объектами Foo владеют не через интерфейсы, как, например, в COM'е, а как-то ещё.
Иначе только хуже будет.
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re[5]: как сократить количество vtbl
От: Erop Россия  
Дата: 22.04.17 19:37
Оценка:
Здравствуйте, qaz77, Вы писали:

Q>Но QueryInterface решает другую задачу, когда есть разные реализации и мы спрашиваем, какие интерфейсы поддерживает данная реализация.

Q>У меня же реализация одна и только одна, просто спрятана от клиентов за абстрактный интерфейс, а точнее за пачку интерфейсов.

Ну тем лучше.
Делаешь универсальный интерфейс, а из твоих интерфейсов делаешь пустые NVI адаптеры.
И привет.

Q>Идея понятна, но в свете избавления от хранения указателей на кучу vtbl возникает вопрос, кто же все таки реализует интерфейс IBarN и как индекс функции

Интерфейс IBarN -- это просто подмножество (часть методов) единственного интерфейса ISuperBar.
А IBarN -- это просто дурилка для статических проверок компилятора, которая позволяет вызывать один методы ISuperBar и не позволяет вызывать другие

Q>в vtbl ISuperBar преобразуется в индекс функции в vtbl IBarN?

vtb есть только у ISuperBar.

Объекты типа IBarN можно вообще запретить создавать.

Кстати, если интерфейсы все твои и реализация только одна, то можно ещё и по другому пути пойти.

Можно просто вывести их все друг из друга защищённо, и получить тот же эффект без хаккерства...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re: как сократить количество vtbl
От: andrey.desman  
Дата: 22.04.17 22:25
Оценка: 1 (1)
Здравствуйте, qaz77, Вы писали:

Q>Класс выглядит следующим образом:

Q>
Q>class Bar: public IBar1,
Q>           public IBar2,
Q>           ...
Q>           public IBar9
Q>{
Q>  int ref_count;
Q>  unsigned flags;
Q>  ...    
Q>  // реализация IBar1
Q>  virtual void foo1() override;
Q>  ...
Q>};
Q>

Q>Есть ли какой-нибудь паттерн/хак, чтобы "упаковать" несколько виртуальных таблиц в одну.

Есть такой хак, но насколько он применим в твоем случае тебе лучше знать. А хак прост как топор:

class IBar1 {};
class IBar2: public IBar1 {}
class IBar3: public IBar2 {}
...
class Bar: public IBarN
{
Impl;
}
...
PROFIT


Нормальный компилятор в этом случае должен сделать только лишь один указатель на ТВМ класса.
Хотя тут private/protected наследование может быть лучше, но это от ситуации зависит.
Отредактировано 22.04.2017 22:35 andrey.desman . Предыдущая версия .
Re: как сократить количество vtbl
От: Videoman Россия https://hts.tv/
Дата: 22.04.17 23:03
Оценка: -1
Здравствуйте, qaz77, Вы писали:

Q>Но есть еще 9 x 4 = 36 байт виртуальных таблиц, и вот здесь вопрошаю совета, что с ними делать.


Виртуальные таблиц являются статическими данными. Т.е. у вас 36 байт на весь класс Bar и не важно сколько вы создадите экземпляров. Тут вы ничего не выиграете.
Re[2]: как сократить количество vtbl
От: andrey.desman  
Дата: 22.04.17 23:35
Оценка:
Здравствуйте, Videoman, Вы писали:

V>Виртуальные таблиц являются статическими данными. Т.е. у вас 36 байт на весь класс Bar и не важно сколько вы создадите экземпляров. Тут вы ничего не выиграете.


Таблицы да, а вот указатели на них совсем даже нет.
Re[3]: как сократить количество vtbl
От: Videoman Россия https://hts.tv/
Дата: 23.04.17 09:56
Оценка: +1
Здравствуйте, andrey.desman, Вы писали:

V>>Виртуальные таблиц являются статическими данными. Т.е. у вас 36 байт на весь класс Bar и не важно сколько вы создадите экземпляров. Тут вы ничего не выиграете.


AD>Таблицы да, а вот указатели на них совсем даже нет.


Да, да неправ. Не о том подумал. Сморозил спросонья
Re[6]: как сократить количество vtbl
От: qaz77  
Дата: 24.04.17 06:59
Оценка:
Здравствуйте, Erop, Вы писали:

E>Делаешь универсальный интерфейс, а из твоих интерфейсов делаешь пустые NVI адаптеры.


Там есть довольно общие интерфейсы, типа ISerializable, который реализует еще 100+ класов.
Если все перевести на NVI, то нельзя будет Bar'ы скармливать функциям, ожидающим полиморфное поведение.
Либо все такие функции переделывать в шаблоны.


E>Кстати, если интерфейсы все твои и реализация только одна, то можно ещё и по другому пути пойти.

E>Можно просто вывести их все друг из друга защищённо, и получить тот же эффект без хаккерства...

Я об этом тоже подумывал.
Теряется принцип "is a ...", но здесь этим можно пожертвовать.
Re[2]: как сократить количество vtbl
От: qaz77  
Дата: 24.04.17 07:17
Оценка:
Здравствуйте, andrey.desman, Вы писали:

AD>Нормальный компилятор в этом случае должен сделать только лишь один указатель на ТВМ класса.


Спасибо. Я рассматривал такой вариант.

Проблема есть с наиболее общими интерфейсами, типа ISerializable.
Которые еще куча классов реализует.

Если такой общий интерфейс только один, а другие более специфичны для Bar,
то общий помещаем на вершину иерархии и все ок.

А если общих два-три?
Как их "склеить" только для Bar, а для массы других классов оставить как есть?
Re[2]: как сократить количество vtbl
От: qaz77  
Дата: 24.04.17 08:33
Оценка:
Здравствуйте, andrey.desman, Вы писали:

AD>Есть такой хак, но насколько он применим в твоем случае тебе лучше знать. А хак прост как топор:


AD>
AD>class IBar1 {};
AD>class IBar2: public IBar1 {}
AD>class IBar3: public IBar2 {}
AD>...
AD>class Bar: public IBarN
AD>{
AD>Impl;
AD>}
AD>...
AD>PROFIT
AD>


В итоге так и сделал.
Спасибо за обсуждение.
Re[7]: как сократить количество vtbl
От: qaz77  
Дата: 24.04.17 08:43
Оценка:
E>>Кстати, если интерфейсы все твои и реализация только одна, то можно ещё и по другому пути пойти.
E>>Можно просто вывести их все друг из друга защищённо, и получить тот же эффект без хаккерства...

Q>Я об этом тоже подумывал.

Q>Теряется принцип "is a ...", но здесь этим можно пожертвовать.

В результате так и сделал.
Интерфейсы наследовал через public, чтобы было совместимо с существующим клиентским кодом.
Так появились артефакты в виде возможности неявного приведения IBar3 к IBar2, но я готов с эти мириться.
Спасибо за обсуждение.
Re[7]: как сократить количество vtbl
От: Erop Россия  
Дата: 24.04.17 19:59
Оценка:
Здравствуйте, qaz77, Вы писали:

Q>Там есть довольно общие интерфейсы, типа ISerializable, который реализует еще 100+ класов.

Q>Если все перевести на NVI, то нельзя будет Bar'ы скармливать функциям, ожидающим полиморфное поведение.
Q>Либо все такие функции переделывать в шаблоны.

Почему?
С точки зрения клиентского кода вообще ничего, кроме того, что кастить надо иначе, не поменяется же?

Q>Я об этом тоже подумывал.

Q>Теряется принцип "is a ...", но здесь этим можно пожертвовать.

is a к интерфейсам вообще не применим. Там другое отношение "поддерживает интерфейс"...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re[8]: как сократить количество vtbl
От: qaz77  
Дата: 25.04.17 07:47
Оценка:
Здравствуйте, Erop, Вы писали:

Q>>Если все перевести на NVI, то нельзя будет Bar'ы скармливать функциям, ожидающим полиморфное поведение.

Q>>Либо все такие функции переделывать в шаблоны.

E>Почему?

E>С точки зрения клиентского кода вообще ничего, кроме того, что кастить надо иначе, не поменяется же?

Функции ожидают параметр ISerializable& (к примеру) и зовут его виртуальные функции,
т.е. полиморфизм времени выполнения в чистом виде.

Если я для какого-то IBar: public ISerializable уберу наследование ISerializable
и сделаю не виртуальные функции load, save и пр. одноименные членам ISerializable,
то IBar нельзя будет скормить функциям, ожидающим ISerializable.

Сделать полиморфное поведение (времени компиляции) по одноименным функциям-членам можно
в шаблонной функции, когда не важно виртуальные функции-члены или нет.

Вот я и имел в виду, что если один из многих классов переделать на NVI,
то тогда все функции, куда его передавали как абстрактный интерфейс, нужно из обычных функций переделывать в шаблонные:
void foo(ISerializable& arg);

переделываем в:
template<typename Serializable>
void foo(Serializable& arg) { ... }


Даже если все классы, реализующие ISerializable, переделать на NVI, то все равно
полиморфизм времени выполнения использовать не получится, т.к. общей базы не будет.


E>is a к интерфейсам вообще не применим. Там другое отношение "поддерживает интерфейс"...


Я обычно думаю об интерфейсе как о роли, которую выполняет реализующий его класс по отношению к другому классу или алгоритму.
При этом роль может делиться на под-роли, т.е. интерфейс может иметь несколько базовых интерфейсов.
Можно также представить какие-то не связанные роли: ISerialiable, IObservable, IPrintable...

Оптимизация, требующая выводить несвязанные интерфейсы друг из друга, нарушает логику разделения на роли.
Даже порядок абсурден в любом случае:
class ISerializable: public IPrintable { ... };
// или
class IPrintable : public ISerializable{ ... };

Т.е. жертвуем логичным дизайном ради оптимизации размера объекта.
Re[9]: как сократить количество vtbl
От: Erop Россия  
Дата: 25.04.17 17:18
Оценка: 1 (1)
Здравствуйте, qaz77, Вы писали:

Q>Если я для какого-то IBar: public ISerializable уберу наследование ISerializable

Q>и сделаю не виртуальные функции load, save и пр. одноименные членам ISerializable,
Q>то IBar нельзя будет скормить функциям, ожидающим ISerializable.

Почему?
// SuperBar.h
struct ISerializable;
struct IClonable;
struct IDeletable
//    И т. д...

class ISuperBar {
public:
    virtual operator ISerializable*() { return 0; }
    virtual operator IClonable*() { return 0; }
    virtual operator IDeletable*() { return to<IDeletable>(); }
    //    И т. д...

protected:
    template<typename I> I* to() { return (I*)this; }
    template<typename const I> I* to() const { return (const I*)this; }

    virtual ~ISuperBar() {}

    // ISerializable
    virtual void Serialize() { assert( false ); }

    // IClonable
    virtual ISuperBar* Clone() { assert( false ); }

    // IDeletable
    virtual void Delete() { delete this; }

    //    И т. д...

};

struct IDeletable : ISuperBar {
using ISuperBar::Delete;
};

////////////////////////////
// Serializable.h

#include <SuperBar.h>
struct ISerializable : ISuperBar {
using ISuperBar::Serialize;
};

////////////////////////////
// Clonable.h

#include <SuperBar.h>
struct IClonable : ISuperBar {
using ISuperBar::Clone;
};



Теперь тот, кто выводился из IClonable, может продолжать это делать, только лучше бы при этом ещё определить operator IClonable*
Но в целом для него ничего не поменяется, и для того, кто использует интерфейс IClonable не поменяется, за исключением того, что
нельзя запрашивать поддерживаемые интерфейсы через dynamic_cast.

Единственное что поменяется, так это если кто-то поддерживает несколько интерфейсов из ISuperBar, то он может вывестись просто публично из ISuperBar и реализовать нужные методы и операторы приведения

Q>Сделать полиморфное поведение (времени компиляции) по одноименным функциям-членам можно

Q>в шаблонной функции, когда не важно виртуальные функции-члены или нет.
Это место не понял о чём тут речь, но, IMHO, это не важно.


Q>Вот я и имел в виду, что если один из многих классов переделать на NVI,

Q>то тогда все функции, куда его передавали как абстрактный интерфейс, нужно из обычных функций переделывать в шаблонные:
Не нужно

struct CBar : ISuperBar {
    // ISerializable
    operator ISerializable*() { return to<ISerializable>(); }
    void Serialize() { /*реализация*/ }

    // IClonable
    operator IClonable*() { return to<IClonable>(); }
    IClonable* Clone() { /*реализация*/ }
};

class CFoo : public IClonable {
public:
    CFoo* Clone() { return new CFoo( *this ); }
};

void foo( IClonable* p ) { p->Clone()->operator IDeletable*()->Delete(); }

CBar b;
foo( b );

CFoo f;
foo( &f );

Как видишь ничего шаблонным делать не надо.

Q>полиморфизм времени выполнения использовать не получится, т.к. общей базы не будет.

Почему? ISuperBar жеж?

ISerializable это такое view на ISuperBar, в котором в public видны только методы ISerializable и всё.

То есть для программиста он останется таким же ISerializable, за исключением того, что QueryInterface иначе далать надо, а для С++ это будет просто NVI view на интерфейс, но это будет спрятано за protected
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Отредактировано 25.04.2017 19:07 Erop . Предыдущая версия .
Re[9]: как сократить количество vtbl
От: Erop Россия  
Дата: 25.04.17 19:05
Оценка:
Здравствуйте, qaz77, Вы писали:

E>>is a к интерфейсам вообще не применим. Там другое отношение "поддерживает интерфейс"...


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

Q>При этом роль может делиться на под-роли, т.е. интерфейс может иметь несколько базовых интерфейсов.
Q>Можно также представить какие-то не связанные роли: ISerialiable, IObservable, IPrintable...


Ну да, "играет роль", реализует интерфейс" -- это другое отношение, чем "является".
Смотри, актёр Смоктуновский является человеком, и играет роль Гамлета Ы?

Q>Оптимизация, требующая выводить несвязанные интерфейсы друг из друга, нарушает логику разделения на роли.

Q>Даже порядок абсурден в любом случае:
Q>
Q>class ISerializable: public IPrintable { ... };
Q>// или
Q>class IPrintable : public ISerializable{ ... };
Q>

Q>Т.е. жертвуем логичным дизайном ради оптимизации размера объекта.

Ну да, мне решение с вьюшками нравится больше...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.