struct IA {
virtual ~IA() {}
virtual void mA1() = 0;
};
struct IB {
virtual ~IB() {}
virtual void mB1() = 0;
};
struct Impl : IA, IB {
int field;
void mA1() override {}
void mB1() override {}
};
void f(IB* a) { a->mB1(); } // {1}
int main() {
volatile auto obj = new Impl; // для подавления девиртуализации
f(obj); // этот вызов MSVC заинлайнил
}
Вызов в строке {1} (MSVC, release):
mov rcx,qword ptr [obj]
test rcx,rcx
je main+44h
mov rax,qword ptr [rcx+8]
add rcx,8
call qword ptr [rax+8]
О чем это говорит? Даже если вы не пишете в своих приложениях кастов, копилятор щедро расставляет эти невидимые касты для вас и они гораздо сложнее, чем "смещение на компайл тайм константу". Если храните базовые указатели (имеется в виду указатель на сам объект?) Будьте готовы к невидимым кастам при каждом вызове.
В таком сценарии в С++-классе все равно будут по одному vmt_ptr на каждый отдельный реализованный интерфейс. А значит будет движение this-указателя при кастах (c проверками на nullptr) и сложная настройка объекта в конструкторе с заполнением всех этих vmt_ptr-полей. Сохранится даже двойной размер всех смарт-поинтеров. Единственное чего возможно удастся избежать - это генерация thunk-функций, но и то только до первого наследования от такого класса.
копирование двух указателей уже на SSE будет одной инструкцией
SSE регистры не могут хранить указатели. Если компилятор умеет предавать fat ptr в регистрах, это будет две дополнительные инструкции и переполнение регистрового файла, как описано в статье. Если не умеет и следует ABI для структур (как например MSVC), то каждая передача параметра-ссылки в функцию превращается в резервирование стека, два movaps, lea, и учитывая, что при вызове каждого метода надо передавать this, это становится дороже подхода Аргентума на каждом вызове.
переход в другую часть кода с потерей кеша инструкций
Вызов метода - это вообще всегда переход в другую часть кода.
Кстати, удвоение размера каждого указателя в программе - это рост размера данных и сделовательно рост количества кеш-промахов, который, в зависимости от связанности структур данных может доходить до 200%. Поэтому подход с двойными указателями с кешем не дружит.
И не понятно что с кодом загружаемым из dll, какие там будут id ?
Код загружаемый из DLL должен следовать соглашениям ABI платформы. Я пока не видел ABI, стандартизирующий вызов виртуальных методов. Поэтому правильно написанная DLL/o не должна выставлять наружу ООП API, если не хочет зависимости от конкретной версии компилятора с конкретными настройками сборки.
Я правильно понимаю, что функция-диспетчер - это по факту просто хитрый switch по ID интерфейса/номеру функции?
Да. Диспетчеризация виртуальной функции это всегда динамический выбор точки входа по классу × интерфейсу × методу.
Как работает передача аргументов через функцию-деспетчер?
Насквозь. Диспетчер использует для своих целей только регистры, которые в соответстсвии с ABI целевой платформы считаются временными/мусором. Наличие диспетчера никак не влияет на вызывающую и вызываемую стороны.
Редко когда надо действительно таскать объекты вроде dyn A + B + C
Это зависит от задачи. Прямо сейчас я пишу этот текст в браузере, DOM которого состоит из групп картинок, текстов, таблиц, всяких кнопок и вложенных групп - воплне себе полиморфная структура данных.
Имеется ли возможность унаследоваться от более чем одного не-интерфейса?
Аргентум следует по пути Java/Kotlin/Swift, позволяя наследоваться от одного класса и реализовывать множество интерфейсов. Я считаю подход Rust/Go которые запрещают наследовать реализацию слишком ограничивающим, а подход С++, который позволяет наследовать реализацию и поля множества базовых классов, в том числе иметь в производом классе несколько копий одного и того же базового класса слишком эээ, как бы это помягче сказать, непрактичным и порождающим ошибки.
Действительно, другой язык, спасибо. Давайте сравним. Для ассоциации vale использует generational references, которые просто убивают приложение, если встречается ссылка на удаленный объект. Плюс нет возможности защитить объект от удаления на время использования. Для композиции vale использует особый указатель с move-семантикой. Нет защиты от закольцованной композиции - владения самим собой. Агрегация не поддержана вовсе.
Ни так ти там я не нашел ничего, что позволило бы понять, как будут организованы структуры данных в хипе. Все про стек. Вся работа основывается на Swift standard library, значит будет в хипе будет все как в Swift.
Смотря с чем сравнивать. Например в C++ у каждого объекта есть vmt_ptrs на каждую независимую базу + offsets на каждую виртуальную базу. А std::shared_ptr - это два машинных слова + каждый объект, на который он указывает должен иметь отдельную динамически аллоцированную структуру со счетчиками. Так что все относительно.
Внутреннее устройство объектов и счетчиков будет постоянно изменяться.
Сейчас в каждом объекте есть три служебных поля - ссылка на класс, счетчик стековых ссылок +1, если есть композитная ссылка или для неизменяемых объектов - счетчик агрератных ссылок и признак многопоточности, указатель на партент-объект (он поддерживается автоматически и доступен приложениям) или указатель на weak-block.
Для объектов, на которые есть ассоциативные weak-ссылки, дополнительно аллоцирован weak-block, в нем есть указатель на таргет, счетчик &-ссылок и флаг многопоточности, идентификатор треда таргета, эвакуированный из таргета указатель на партент-объект.
Кто и когда освободит память этого неизменяемого объекта?
Этим занимаются агрегатные *-ссылки.
Ну и, кстати, делать снепшот большого словаря слишком дорого как и гонять туда-сюда таски для каждого поля.
Поэтому я предложил множество решений, какие-то подойдут для одних случаев, какие-то для других.
И вообще, многопоточность - для CPU-bounded задач.
Она используется для разбиения задачи на части и передачи их воркерам. В этом случае нужно однократно передать задачу, принять задачу и иногда обеспечить совместный доступ воркеров к общему неизменяемому состоянию.
Второе применение многопоточности - микросервисная модель. Когда логгеру, графической сцене, сетевой подсистеме и другим сервисам, у которых есть свое внутреннее состояние, передаются очереди запросов.
Оба сценария использования многопоточности поддержаны ссылочной моделью Аргентума нативно. Если в вашем приложении есть какой-то другой стиль использования потоков, буду признателен, если вы опишете его, и тогда мы сможем устроить мозговой штурм, чтобы выработать хорошее решение.
Если ты хочешь передать из потока в поток какие-то данные, просто передай этот объект, вместо ссылки на него. Это дешево, это не копирование.
Если у тебя есть какой-то объект, данные которого нужны одновременно множеству потоков, сделай этот объект незименяемым, и передай *-ссылку на него всем желающим.
Если этот объект изменяемый, каждый читатель состояния должен видеть непротиворечивое состяние этого объекта и всех его подобъектов. Нужна версионность. Пусть на каждое изменение порождает неизменяемый снапшот, ссылку на который этот объект выдает другим потокам по требованию.
Если важно, чтобы объект жил в одном потоке, а использовался из другого, ак микросервис, нужно продумать API запросов-тасков к этому объекту.
Если нужно "прям тут" наколхозить вытаскивание текущего значения поля из объекта другого потока, это можно сделать посылкой асинхронного таска, вытаскиващего одно поле, с асинхронным коллбэком.
Ссылка на объект из другого потока (все равно откуда ее получили) разыменуется как null. Чтобы получить объект по этой ссылке нужно или послать ей асинхронный таск, или передать ее в тот поток, где находится ее таргет, и там разыменовать.
Если предположить, что вы имеете в виду не корневой объект в модели данных приложения, о которой идет речь в статье, а корень иерархии наследования классов, то правило единого корня дейтствует во всех managed-системах - в C#, Java, JS, Python и многих других. Все объекты независимо от их полезной нагрузки имеют общее поведение, используемое в управляемой среде исполнения, и это поведение заключено в корневом классе.
При определении объекта этого объекта еще нет. В инициализаторе поля this не доступен. После создания объекта он изменяемый. Все ссылки на него - временные стековые T, которые нельзя присваиваеть его *T полям. А если его заморозить оператором *, который вернет на него *T ссылку, ты получишь объект, который нельзя изменять. Поэтому объект не может ссылаться сам на себя ничем, кроме не-владеющей &T ссылки.
Кажется мы друг друга не поняли. Один класс корня называется "Application" и в нем есть под-объекты: система логгирования, модель данных, UI, стейт-менеджер, messaging. Где-то в модели данных есть подобъекты: локальный кеш, persistent storage, политики репликации и вытеснения. Где-то в локальном кеше будут когда надо появлятся и пропадать сотрудники, товары и прочие мелкие части модели данных. А "god object" - это совсем про другое.
Ответ на вопрос "зачем": "Для того, чтобы удалять ненужные неизменяемые объекты (точнее учитывать, что они все еще кому-то нужны) и существуют агрегатные*T ссылки. "
*T-ссылка означает: Вот этот объект я пока держу, пока не удайте его, он мне нужен. можете шарить этот объект с другими местами в программе, где он тоже нужен, его можно шарить, т.к. я обязуюсь его не изменять.
&T-ссылка означает: Вот этим объектом я буду пользоваться иногда, пока его не удалят те кто его держит.
Приведите пример, в котором по вашему мнению вместо шарено-владеющей ссылки *T можно применить невладеющую ссылку &T.
Корень модели данных приложения - это объект какого-то класса. Все локальные корни подсистем и бизнес-процессов могут быть полями в это классе. Ну не в глобальные же переменные их складывать в XXI веке.
В Аргентуме нет константных объектов. В аргентуме есть неизменяемые объекты. У неизменяемого объекта есть ограниченный временем жизненный цикл - создали, заполнили данными, сделали неизменяемым, попользовались в разных иерархиях, удалили за ненадобностью. Для того, чтобы удалять ненужные неизменяемые объекты (точнее учитывать, что они все еще кому-то нужны) и существуют агрегатные*T ссылки.
На уровне синтаксиса в Аргентуме есть константы, но это понятие ортогонально неизменяемым объектам.
Простйший тест:
Вызов в строке {1} (MSVC, release):
О чем это говорит? Даже если вы не пишете в своих приложениях кастов, копилятор щедро расставляет эти невидимые касты для вас и они гораздо сложнее, чем "смещение на компайл тайм константу". Если храните базовые указатели (имеется в виду указатель на сам объект?) Будьте готовы к невидимым кастам при каждом вызове.
В таком сценарии в С++-классе все равно будут по одному vmt_ptr на каждый отдельный реализованный интерфейс. А значит будет движение this-указателя при кастах (c проверками на nullptr) и сложная настройка объекта в конструкторе с заполнением всех этих vmt_ptr-полей. Сохранится даже двойной размер всех смарт-поинтеров. Единственное чего возможно удастся избежать - это генерация thunk-функций, но и то только до первого наследования от такого класса.
Каждый вызов виртуального метода в Rust/Swift/C++ сегодня происходит через `call [RAX+...]` процессор справляется.
SSE регистры не могут хранить указатели. Если компилятор умеет предавать fat ptr в регистрах, это будет две дополнительные инструкции и переполнение регистрового файла, как описано в статье. Если не умеет и следует ABI для структур (как например MSVC), то каждая передача параметра-ссылки в функцию превращается в резервирование стека, два movaps, lea, и учитывая, что при вызове каждого метода надо передавать this, это становится дороже подхода Аргентума на каждом вызове.
Вызов метода - это вообще всегда переход в другую часть кода.
Кстати, удвоение размера каждого указателя в программе - это рост размера данных и сделовательно рост количества кеш-промахов, который, в зависимости от связанности структур данных может доходить до 200%. Поэтому подход с двойными указателями с кешем не дружит.
Код загружаемый из DLL должен следовать соглашениям ABI платформы. Я пока не видел ABI, стандартизирующий вызов виртуальных методов. Поэтому правильно написанная DLL/o не должна выставлять наружу ООП API, если не хочет зависимости от конкретной версии компилятора с конкретными настройками сборки.
Да. Диспетчеризация виртуальной функции это всегда динамический выбор точки входа по классу × интерфейсу × методу.
Насквозь. Диспетчер использует для своих целей только регистры, которые в соответстсвии с ABI целевой платформы считаются временными/мусором. Наличие диспетчера никак не влияет на вызывающую и вызываемую стороны.
Это зависит от задачи. Прямо сейчас я пишу этот текст в браузере, DOM которого состоит из групп картинок, текстов, таблиц, всяких кнопок и вложенных групп - воплне себе полиморфная структура данных.
Аргентум следует по пути Java/Kotlin/Swift, позволяя наследоваться от одного класса и реализовывать множество интерфейсов. Я считаю подход Rust/Go которые запрещают наследовать реализацию слишком ограничивающим, а подход С++, который позволяет наследовать реализацию и поля множества базовых классов, в том числе иметь в производом классе несколько копий одного и того же базового класса слишком эээ, как бы это помягче сказать, непрактичным и порождающим ошибки.
Поэтому в Аргентуме коррекция
this
не нужна.Спасибо за хорошие вопросы.
Действительно, другой язык, спасибо. Давайте сравним. Для ассоциации vale использует generational references, которые просто убивают приложение, если встречается ссылка на удаленный объект. Плюс нет возможности защитить объект от удаления на время использования. Для композиции vale использует особый указатель с move-семантикой. Нет защиты от закольцованной композиции - владения самим собой. Агрегация не поддержана вовсе.
Язык val.
Исходники тут: https://github.com/val-lang/val
Сайт проекта тут: https://www.val-lang.dev/
Ни так ти там я не нашел ничего, что позволило бы понять, как будут организованы структуры данных в хипе. Все про стек. Вся работа основывается на Swift standard library, значит будет в хипе будет все как в Swift.
Спасибо, очень верное замечание. У воркеров (потоков без состояния) должна быть общая очередь для балансирования нагрузки.
Смотря с чем сравнивать. Например в C++ у каждого объекта есть vmt_ptrs на каждую независимую базу + offsets на каждую виртуальную базу. А std::shared_ptr - это два машинных слова + каждый объект, на который он указывает должен иметь отдельную динамически аллоцированную структуру со счетчиками. Так что все относительно.
Внутреннее устройство объектов и счетчиков будет постоянно изменяться.
Сейчас в каждом объекте есть три служебных поля - ссылка на класс, счетчик стековых ссылок +1, если есть композитная ссылка или для неизменяемых объектов - счетчик агрератных ссылок и признак многопоточности, указатель на партент-объект (он поддерживается автоматически и доступен приложениям) или указатель на weak-block.
Для объектов, на которые есть ассоциативные weak-ссылки, дополнительно аллоцирован weak-block, в нем есть указатель на таргет, счетчик &-ссылок и флаг многопоточности, идентификатор треда таргета, эвакуированный из таргета указатель на партент-объект.
Этим занимаются агрегатные *-ссылки.
Поэтому я предложил множество решений, какие-то подойдут для одних случаев, какие-то для других.
И вообще, многопоточность - для CPU-bounded задач.
Она используется для разбиения задачи на части и передачи их воркерам. В этом случае нужно однократно передать задачу, принять задачу и иногда обеспечить совместный доступ воркеров к общему неизменяемому состоянию.
Второе применение многопоточности - микросервисная модель. Когда логгеру, графической сцене, сетевой подсистеме и другим сервисам, у которых есть свое внутреннее состояние, передаются очереди запросов.
Оба сценария использования многопоточности поддержаны ссылочной моделью Аргентума нативно. Если в вашем приложении есть какой-то другой стиль использования потоков, буду признателен, если вы опишете его, и тогда мы сможем устроить мозговой штурм, чтобы выработать хорошее решение.
Если ты хочешь передать из потока в поток какие-то данные, просто передай этот объект, вместо ссылки на него. Это дешево, это не копирование.
Если у тебя есть какой-то объект, данные которого нужны одновременно множеству потоков, сделай этот объект незименяемым, и передай *-ссылку на него всем желающим.
Если этот объект изменяемый, каждый читатель состояния должен видеть непротиворечивое состяние этого объекта и всех его подобъектов. Нужна версионность. Пусть на каждое изменение порождает неизменяемый снапшот, ссылку на который этот объект выдает другим потокам по требованию.
Если важно, чтобы объект жил в одном потоке, а использовался из другого, ак микросервис, нужно продумать API запросов-тасков к этому объекту.
Если нужно "прям тут" наколхозить вытаскивание текущего значения поля из объекта другого потока, это можно сделать посылкой асинхронного таска, вытаскиващего одно поле, с асинхронным коллбэком.
Ссылка на объект из другого потока (все равно откуда ее получили) разыменуется как null. Чтобы получить объект по этой ссылке нужно или послать ей асинхронный таск, или передать ее в тот поток, где находится ее таргет, и там разыменовать.
Если предположить, что вы имеете в виду не корневой объект в модели данных приложения, о которой идет речь в статье, а корень иерархии наследования классов, то правило единого корня дейтствует во всех managed-системах - в C#, Java, JS, Python и многих других. Все объекты независимо от их полезной нагрузки имеют общее поведение, используемое в управляемой среде исполнения, и это поведение заключено в корневом классе.
При определении объекта этого объекта еще нет. В инициализаторе поля this не доступен. После создания объекта он изменяемый. Все ссылки на него - временные стековые
T
, которые нельзя присваиваеть его*T
полям. А если его заморозить оператором*
, который вернет на него*T
ссылку, ты получишь объект, который нельзя изменять. Поэтому объект не может ссылаться сам на себя ничем, кроме не-владеющей&T
ссылки.Все так.
Кажется мы друг друга не поняли. Один класс корня называется "Application" и в нем есть под-объекты: система логгирования, модель данных, UI, стейт-менеджер, messaging. Где-то в модели данных есть подобъекты: локальный кеш, persistent storage, политики репликации и вытеснения. Где-то в локальном кеше будут когда надо появлятся и пропадать сотрудники, товары и прочие мелкие части модели данных. А "god object" - это совсем про другое.
Ответ на вопрос "зачем": "Для того, чтобы удалять ненужные неизменяемые объекты (точнее учитывать, что они все еще кому-то нужны) и существуют агрегатные
*T
ссылки. "*T
-ссылка означает: Вот этот объект я пока держу, пока не удайте его, он мне нужен. можете шарить этот объект с другими местами в программе, где он тоже нужен, его можно шарить, т.к. я обязуюсь его не изменять.&T
-ссылка означает: Вот этим объектом я буду пользоваться иногда, пока его не удалят те кто его держит.Приведите пример, в котором по вашему мнению вместо шарено-владеющей ссылки
*T
можно применить невладеющую ссылку&T
.Корень модели данных приложения - это объект какого-то класса. Все локальные корни подсистем и бизнес-процессов могут быть полями в это классе. Ну не в глобальные же переменные их складывать в XXI веке.
В Аргентуме нет константных объектов. В аргентуме есть неизменяемые объекты. У неизменяемого объекта есть ограниченный временем жизненный цикл - создали, заполнили данными, сделали неизменяемым, попользовались в разных иерархиях, удалили за ненадобностью. Для того, чтобы удалять ненужные неизменяемые объекты (точнее учитывать, что они все еще кому-то нужны) и существуют агрегатные
*T
ссылки.На уровне синтаксиса в Аргентуме есть константы, но это понятие ортогонально неизменяемым объектам.