Re[45]: MS забило на дотнет. Питону - да, сишарпу - нет?
От: vdimas Россия  
Дата: 08.09.21 01:30
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Это и есть те случаи, когда "сама структура не нужна за пределами текущего фрейма стека, и не хочется нагружать GC." Вы по-прежнему невнимательно читаете.


В своём репертуаре. ))
С двух раз не понял, о чём речь, но виноваты окружающие.


S>Во всех остальных случаях мы делаем просто var t = new Struct1[42]. Это же не Java с её отсутствием value types.


Структуру можно вернуть и по-значению.
Унутре вызывающая сторона подготавливает место под структуру и передаёт ссылку на это место в кач-ве аргумента.
(или использует уже существующее такое место)


S>А в вашем случае все "проблемы" сводятся к

S>
S>byte *tmp = stackalloc byte[sizeof(Struct1)*42];
S>Struct1* ptr = (struct1*) tmp
S>


Нарушение типизации, почва для ошибок.
Сейчас можно типизированно.
А если помещать в Span<Struct1>, то и без unsafe.


S>А fixed array как-то вообще не очень хорошо себя ведёт в том смысле, что им пользоваться неудобно.


Иногда это киллер-фича.


S>Ну, вот например — хочется сделать структуру относительно замкнутой, чтобы можно ей было удобно пользоваться:

S>
S>public unsafe struct
S>{
S>   private int _length;
S>   private fixed int _inplaceArray[42];
S>   public ref int this[int index] {...}
S>}
S>

S>Вроде бы — нормальное желание. Хочется сделать структуру, которой можно пользоваться из safe-кода; внутрь индексера мы вставим проверку границ, которая должна устраняться джитом в простых случаях.

Опять что-то странное пишешь.
Это не работает:
public ref int this[int index] {...}

Получить ref или readonly ref-ссылку на поле структуры через метод самой структуры нельзя, можно только через метод-расширение, где саму структуру передавать через ref this.
public unsafe static class Struct1Ext 
    public static ref byte Item(ref this Struct1 @this, int index) => ref @this.inplaceArray[index];
}

S>Увы — даже тут нельзя внутри индексера просто написать _inplaceArray[index]. Надо пинить this через fixed().

А, ясно в чём "неудобство".
fixed-массивы не при чём, никакое поле структуры нельзя так вернуть.

И да, пинить поля структуры в её методах — плохая идея.
Структура не имеет информации — был ли вызван её метод со ссылкой this на значение целевой переменной (или поля с типом этой структуры) или рантайм создал временную копию структуры и вызвал метод со ссылкой на копию. Т.е., по выходу из такого метода ты можешь получить ссылку на несуществующую более память.


S>В свете этого удобнее получается делать примерно так:


Так не работает, т.е. не в fixed-массиве дело.


V>>fixed struct тебе в помощь, но за границей памяти следить самостоятельно:

S>Теперь за границей памяти можно следить через Span.

А какая разница?
В случае flexible structs всё-равно надо создать Span от inplace-массива с "вручную" отмеренным нужным размером, т.к. при создании Span никаких проверок не делается.


S>Пишем точно такой же код, как выше, только никаких _inplaceArray01 и далее.


Для интеропа не подойдёт, т.к. сломает разметку ожидаемой структуры.


V>>Если бы еще нейтив вызывался исключительно для тривиальных вещей...

S>На всякий случай напомню, что вы как аргумент для недостаточного быстродействия PInvoke приводили как раз случай тривиальных вещей — невозможно делать миллион нетривиальных вещей в секунду.

Возможно.
TCPDirect дают примерно 20ns задержку от прихода пакета в сетевую карточку, поток тупо в цикле опрашивает диспетчер.
Но вызов туда нельзя помечать SuppressGCTransition, т.к. в этом вызове иногда может быть и вызов ядра (как повезёт).


V>>Прямо сейчас не буду утверждать, но на вскидку не помню у себя случаев, чтобы нейтивный код не делал вызовов АПИ ОС, иначе такой код давно живёт в дотнете.

V>>Т.е. в этом случае SuppressGCTransition использовать нельзя.
S>Можно. Нельзя если есть колбэки, и не рекомендуется, если код работает более секунды.

Более микросекунды.
И если есть вызовы примитивов синхронизации и вообще АПИ ОС.


V>>В прошлых проектах были вызовы медиа-кодеков из дотнета, но такие вещи лучше целиком убирать в нейтив, оставляя дотнету только управляющую роль — создать канал/стрим, настроить, запустить/остановить.

S>Да, наверное. Не очень знаком с деталями взаимодействия с медиа-кодеками.

Это просто числомолотилки, которым данные подаются порциями.
Эти числомолотилки не трогают АПИ ОС.


V>>На дотнетной стороне делается, например, в том же Unity в плагинах к драйверу.

V>>Там в среднем 2-6 чисел в буфер надо напихать за раз, где первое число код команды, потом аргументы.
V>>Вызывать ради этого нейтив не имеет смысла.
S>Я об этом сразу писал в комментах про вулкан — набивка чисел в буфер прекрасно живёт и в менеджед. Но вы настаивали (в обсуждении с Serginio1) на миллионах PInvoke вызовов в секунду...

Потому что при вызове drawing operations буфер команд в OpenGL, например, наполняет сам драйвер в стиле чёрного ящика, периодически скидывая накопленные команды драйверу.
Да еще текущий буфер один на процесс.
Вот рисуется прямоугольник:
glBegin(GL_POLYGON);
glVertex2(x1, y1);
glVertex2(x2, y1);
glVertex2(x2, y2);
glVertex2(x1, y2);
glEnd();


В Vulkan вынесли CommandBuffer наружу (двумя способами — можно копировать подготовленный буфер в карточку, а можно маппить часть памяти графической подсистемы в обычное адресное пространство и набивать прямо память графики — в случае встроенной графики это в разы эффективней) и позволили самим выбирать момет flush этого буфера в карточку или unmap, но конкретные форматы команд тоже скрыты за АПИ типа такого:
https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/vkCmdSetViewport.html

Поэтому, дёргать эти АПИ из дотнета смысла нет.
Надо переносить в дотнет приличную часть драйвера — всё что касается набивки буфера команд.


S>Если у нас какая-то прямо жёсткая зависимость от внешнего вызова, но их много и они занимают мало времени на анменеджед стороне — PInvoke вовсе не так уж плох. Всего лишь втрое дороже call EAX.


В 17 раз дороже.


V>>Т.е., слишком много времени работы оптимизатора надо "размазывать" в процессе уже боевой работы программы.

S>Повторюсь — это зависит от того, что мы считаем временем боевой работы программы.

От здравого смысла это зависит. ))
Меня порой удивляет твоё ожесточение в плане попыток оправдать чьи-то решения.


V>>Не зря Андроид практически полностью переехал на AOT.

S>Каждому своё. Мобильная платформа — это максимально короткое время исполнения приложения, чтобы сохранить батарею.

Это просто проблема явно засветилась в мобильном секторе.
Но проблема одна — энергоэффективность вычислений.
Т.е., грубо, сколько "сжигается деревьев" на каждый чих.


S>А вот в сервер-сайд у нас приложения работают неделями.


Но при обновлении должны будут тормозить несколько первых минут?
Я не вижу причин, по которым сервер-сайд должен отказываться от АОТ.
Т.е. вообще не вижу.
Вижу рассуждения из разряда "но можно и по-старинке".
Можно.
Но не нужно.


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

S>Да, это представляет потенциал для возможного будущего улучшения. К примеру, те же атрибуты, АФАИК, работают не так — там как раз параметры конструктора кладутся в данные; и потом конструкторы вызывает магия среды, без построения кода статических инициализаторов. Теоретически, можно сворачивать инициализаторы в аналогичный код.

Разумеется.
Как раз АОТ занимается и этими вещами тоже.


V>>Я плохо себе представляю сценарий, когда полностью оптимизированный код надо опять оптимизировать.

S>А вот я — хорошо. Вот у вас, грубо говоря, был высокоабстрактный код, который читает данные из базы в цикле и сериализовывает их при помощи настроенного снаружи сериализатора:
S>
S>...
S>foreach(var d in dataReader)
S>{
S>   _serializer.Write(_outstream, d);
S>}
S>...
S>

S>Допустим, мы умеем поддерживать два сериализатора — в XML и в JSON. Выбирается тот или иной традиционно — на основе анализа хидера Accept у клиентского запроса.
S>В тестах у нас PGO выясняет, что dataReader у нас обычно — это SqlDataReader, и выполняет спекулятивный инлайнинг. В тестах, на основе которых строился профайл, JsonSerializer и XmlSerializer вызывались 50%/50%, поэтому доминируюшего фактического типа не было, поэтому оставлен косвенный вызов.

S>Хотспот же выясняет, что в процессе реальной эксплуатации у нас 99% вызовов запрашивали XML. Он перекомпилирует код, выполняя спекулятивный инлайнинг и устраняя лишнюю косвенность.


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


V>>PGO управляет обычно выбором — меньше кода или больше инлайна.

V>>Т.е. для холодных участков код можно сделать поменьше в объёме, для горячих — больше инлайна.
V>>Но при максимальной оптимизации никакая дальнейшая оптимизация лучше не сделает.
S>А потом у нас выкатили новую версию фронт-енда, и теперь чаще запрашивается JSON. Хотспот перекомпилирует наш цикл, инлайня на этот раз JsonSerializer.

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


S>При этом, опять же, после инлайна кода у нас появляется статистика о том, какие именно запросы делаются в dataReader. И, соответственно, возможность ещё что-то проинлайнить, выбросив лишние ветки if вместе с косвенными вызовами.


Оптимизации выполняются на уровне конкретного типа.
Плохо представляю себе приложение, где в DataReader в любой точке приложения приходит одна и та же схема данных. ))


S>В итоге, после достаточно длинного прогрева, у нас получается почти такой же код, который строит компилятор С++ в том случае, если ему известны точные типы всех участников нашего абстрактного конвеера.


Для DataReader "типы" полей динамические, зависят от схемы принятого рекордсета.
Тут и С++ ничего не сделает.

Поэтому, полагаться на всегомогутные оптимизаторы в этой реальности в этих десятилетиях бесполезно.
Надо тупо писать эффективный код.
И в то же самое время читабельный и поддерживаемый. ))


V>>Нельзя.

S>Это очень плохо

Это принцип работы.


V>>Эти вещи надо выносить как стадию сборки, например, это можно делать в LinqToDb, не обязательно заниматься кодогенерацией во время работы приложения.

S>Для ряда задач это правильно. Но я — фанат рантайм кодогенерации.

Всю эту кодогенерацию, которую ты показывал для linq, можно было выполнить и для AOT.
Просто тут нужно соотв. плагинное АПИ к АОТ.
Т.е., AOT даёт тебе конкретные типы, т.к. в закрытой системе типов они известны (даже если заведомо абстрактны — это тоже известно), а "плагин" генерит код, который опять же компиляется AOT.


S>Ну, вот примерно как SqLServer внутри себя оптимизирует запросы с использованием Foreign key — если этот констреинт проверен, то условия типа where exists(select from manager where id = managerId) устраняются из предикатов. А если нет — то не устраняются. Далеко не всегда можно статически установить, будет ли этот констреинт проверен, на этапе компиляции программы.


Еще как можно.
1. В языках с зависимыми типами.
2. В обычных компиллируемых как минимум бета-редуцируемых языках (шаблоны С++ в пример) через генерирование двух версий — для проверенного и непроверенного констрейна.


V>>Скомпиллировать-оптимизировать можно на машине, предназначенной для компиляции-оптимизации.

S>Это потребует ещё более сложной системы, которая будет собирать profile информацию с боевой машины/машин и доставлять их на машины, предназначенные для компиляции/оптимизации .

Не.
Даже в языках с зависимыми типами всё-равно в какой-то точке программы идёт проверка и ветвление/диспетчеризация кода.
Так же и в твоём примере с SQL-сервером, для данной сущности берётся одна из готовых реализаций, соотв. констрейнам этой сущности.
Для привычных языков это эквивалентно вызову вирт.функции (или делегата, или ф-ии по указателю и т.д.) на верхнем уровне.

То бишь, любое "переключение" функциональности достигается через +1 косвенность.
То бишь, у тебя может быть уже сетка неких алгоритмов под разные сценарии. При изменении сценария ты подставляешь указатель на нужный алгоритм.

В виде виртуальных ф-ий этому трюку соответствуют паттерны GoF "стратегия" и/или "состояние".
(второе — разновидность первого)


S>Лично я не возьмусь за проектирование такой системы — уж очень хрупкой она получается


Хотспот еще более хрупкий и непредсказуемый.
Отредактировано 08.09.2021 1:45 vdimas . Предыдущая версия . Еще …
Отредактировано 08.09.2021 1:44 vdimas . Предыдущая версия .
Отредактировано 08.09.2021 1:40 vdimas . Предыдущая версия .
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.