Здравствуйте, vdimas, Вы писали:
V>Были проблемы, я их обрисовал, похоже, ты не понял на словах.
V>Даю в коде.
Да, код всегда помогает.
V>Вот так было нельзя:
V>V>struct Struct1 {
V> int a, b;
V>}
V>Struct1* ptr = stackalloc Struct1[42];
V>
Это и есть те случаи, когда "сама структура не нужна за пределами текущего фрейма стека, и не хочется нагружать GC." Вы по-прежнему невнимательно читаете.
Во всех остальных случаях мы делаем просто var t = new Struct1[42]. Это же не Java с её отсутствием value types.
А в вашем случае все "проблемы" сводятся к
byte *tmp = stackalloc byte[sizeof(Struct1)*42];
Struct1* ptr = (struct1*) tmp
V>А вот так до сих пор нельзя в 5-м дотнете C# 9.0:
V>V>unsafe struct Struct2 {
V> // error CS1663: Fixed size buffer type must be one of the following: bool, byte, short, int, long, char, sbyte, ushort, uint, ulong, float or double
V> fixed Struct1 inplaceArray[42];
V>}
V>
V>Я не проверял еще в 6-м дотнете, ты там вроде хвастался, что дёргал C# 10.0, самое время проверить.
Нет, пока не дёргал — только анонсы читал.
А fixed array как-то вообще не очень хорошо себя ведёт в том смысле, что им пользоваться неудобно.
Ну, вот например — хочется сделать структуру относительно замкнутой, чтобы можно ей было удобно пользоваться:
public unsafe struct
{
private int _length;
private fixed int _inplaceArray[42];
public ref int this[int index] {...}
}
Вроде бы — нормальное желание. Хочется сделать структуру, которой можно пользоваться из safe-кода; внутрь индексера мы вставим проверку границ, которая должна устраняться джитом в простых случаях.
Увы — даже тут нельзя внутри индексера просто написать _inplaceArray[index]. Надо пинить this через fixed().
В свете этого удобнее получается делать примерно так:
public unsafe struct
{
private fixed int _inplaceArray00;
private fixed int _inplaceArray01;
...
private fixed int _inplaceArray41;
public ref int this[int index] => ref MemoryMarshal.CreateSpan(ref _inplaceArray00, 42)[index];
}
V>fixed struct тебе в помощь, но за границей памяти следить самостоятельно:
V>V> unsafe struct Struct2
V> {
V> public fixed byte inplaceArray[1];
V> }
V> ...
V> byte * ptr1 = stackalloc byte[10000];
V> Struct2 * ptr2 = (Struct2 *)ptr1;
V> // пишем за пределы структуры:
ptr2->>inplaceArray[9999] = 42;
V>
Теперь за границей памяти можно следить через Span. Пишем точно такой же код, как выше, только никаких _inplaceArray01 и далее.
V>Если бы еще нейтив вызывался исключительно для тривиальных вещей...
На всякий случай напомню, что вы как аргумент для недостаточного быстродействия PInvoke приводили как раз случай тривиальных вещей — невозможно делать миллион нетривиальных вещей в секунду.
V>Но за подсказку о новой фиче спасибо.
Да не за что — сам узнал в ходе дискуссии. За что и ценю вот эти форумные баталии
V>Прямо сейчас не буду утверждать, но на вскидку не помню у себя случаев, чтобы нейтивный код не делал вызовов АПИ ОС, иначе такой код давно живёт в дотнете.
V>Т.е. в этом случае SuppressGCTransition использовать нельзя.
Можно.
Нельзя если есть колбэки, и
не рекомендуется, если код работает более секунды.
V>В прошлых проектах были вызовы медиа-кодеков из дотнета, но такие вещи лучше целиком убирать в нейтив, оставляя дотнету только управляющую роль — создать канал/стрим, настроить, запустить/остановить.
Да, наверное. Не очень знаком с деталями взаимодействия с медиа-кодеками.
V>На дотнетной стороне делается, например, в том же Unity в плагинах к драйверу.
V>Там в среднем 2-6 чисел в буфер надо напихать за раз, где первое число код команды, потом аргументы.
V>Вызывать ради этого нейтив не имеет смысла.
Я об этом сразу писал в комментах про вулкан — набивка чисел в буфер прекрасно живёт и в менеджед. Но вы настаивали (в обсуждении с Serginio1) на миллионах PInvoke вызовов в секунду...
Если у нас какая-то прямо жёсткая зависимость от внешнего вызова, но их много и они занимают мало времени на анменеджед стороне — PInvoke вовсе не так уж плох. Всего лишь втрое дороже call EAX.
V>Т.е., слишком много времени работы оптимизатора надо "размазывать" в процессе уже боевой работы программы.
Повторюсь — это зависит от того, что мы считаем временем боевой работы программы.
V>Поэтому, ИМХО, альтернативы AOT нет.
V>Не зря Андроид практически полностью переехал на AOT.
Каждому своё. Мобильная платформа — это максимально короткое время исполнения приложения, чтобы сохранить батарею. Запустил — воспользовался — выкинул.
Для таких сценариев альтернативы АОТ нету. Даже если мы изобретём способ персистентной инкрементальной компиляции, то есть будем записывать на диск результаты JIT и на следующем запуске начинать не с нуля, то это будет очень плохо восприниматься пользователем — к моменту, когда такая схема "прогреется", недовольный пользователь давно уже снесёт задолбавшее тормозное приложение с устройства.
А вот в сервер-сайд у нас приложения работают неделями.
V>От мегабайт кода инициализации справочников:
V>V>FieldDescriptor field1 = new FieldDescriptor (Tag42, DataType.Int32, "Field Description")
V>
V>И вот такого кода бесконечно.
V>Джит его честно джитил ради однократного исполнения.
V>В случае хранения в ресурсах я экспериментировал — джитится только код десериализатора, загрузка одного массива байт из ресурсов быстрая всё отрабатывает на порядок с лишним быстрее.
Понятно, спасибо. Действительно, забавная ловушка.
V>В нейтиве такие вещи сидят в сегменте data, т.е. просто подгружаются в память при загрузке бинаря на исполнение, а в дотнете статические данные инициализируются по-старинке, как будто у нас всё еще есть апп-домены с динамическим выделением статических данных для каждого домена.
Да, это представляет потенциал для возможного будущего улучшения. К примеру, те же атрибуты, АФАИК, работают не так — там как раз параметры конструктора кладутся в данные; и потом конструкторы вызывает магия среды, без построения кода статических инициализаторов. Теоретически, можно сворачивать инициализаторы в аналогичный код.
V>Я плохо себе представляю сценарий, когда полностью оптимизированный код надо опять оптимизировать.
А вот я — хорошо. Вот у вас, грубо говоря, был высокоабстрактный код, который читает данные из базы в цикле и сериализовывает их при помощи настроенного снаружи сериализатора:
...
foreach(var d in dataReader)
{
_serializer.Write(_outstream, d);
}
...
Допустим, мы умеем поддерживать два сериализатора — в XML и в JSON. Выбирается тот или иной традиционно — на основе анализа хидера Accept у клиентского запроса.
В тестах у нас PGO выясняет, что dataReader у нас
обычно — это SqlDataReader, и выполняет спекулятивный инлайнинг. В тестах, на основе которых строился профайл, JsonSerializer и XmlSerializer вызывались 50%/50%, поэтому доминируюшего фактического типа не было, поэтому оставлен косвенный вызов.
Хотспот же выясняет, что в процессе реальной эксплуатации у нас 99% вызовов запрашивали XML. Он перекомпилирует код, выполняя спекулятивный инлайнинг и устраняя лишнюю косвенность.
V>PGO управляет обычно выбором — меньше кода или больше инлайна.
V>Т.е. для холодных участков код можно сделать поменьше в объёме, для горячих — больше инлайна.
V>Но при максимальной оптимизации никакая дальнейшая оптимизация лучше не сделает.
А потом у нас выкатили новую версию фронт-енда, и теперь чаще запрашивается JSON. Хотспот перекомпилирует наш цикл, инлайня на этот раз JsonSerializer.
При этом, опять же, после инлайна кода у нас появляется статистика о том, какие именно запросы делаются в dataReader. И, соответственно, возможность ещё что-то проинлайнить, выбросив лишние ветки if вместе с косвенными вызовами. В итоге, после достаточно длинного прогрева, у нас получается почти такой же код, который строит компилятор С++ в том случае, если ему известны точные типы всех участников нашего абстрактного конвеера.
V>Нельзя.
Это очень плохо
V>Нельзя.
Это очень плохо
V>Эти вещи надо выносить как стадию сборки, например, это можно делать в LinqToDb, не обязательно заниматься кодогенерацией во время работы приложения.
Для ряда задач это правильно. Но я — фанат рантайм кодогенерации. Это как раз способ во-первых, получить эффекты, сравнимые с вышеописанным хотспотом (который для дотнета пока что находится где-то между областью мечты и областью фантазий), а во-вторых, даже при его наличии подняться на уровень выше. То есть использовать семантическую оптимизацию — не на основе "лабораторных наблюдений", а на основе известных на момент исполнения вышележащей программе фактов. Ну, вот примерно как SqLServer внутри себя оптимизирует запросы с использованием Foreign key — если этот констреинт
проверен, то условия типа
where exists(select from manager where id = managerId) устраняются из предикатов. А если нет — то не устраняются. Далеко не всегда можно статически установить, будет ли этот констреинт проверен, на этапе компиляции программы.
V>Я вообще не понимаю, зачем это делать, особенно сейчас, когда всё-равно нет перезагрузки апп-доменов ввиду отсутствия оных.
V>Зачем нагружать компиляцией-оптимизацией боевую машину/машины?
V>Скомпиллировать-оптимизировать можно на машине, предназначенной для компиляции-оптимизации.
Это потребует ещё более сложной системы, которая будет собирать profile информацию с боевой машины/машин и доставлять их на машины, предназначенные для компиляции/оптимизации
.
Лично я не возьмусь за проектирование такой системы — уж очень хрупкой она получается