Информация об изменениях

Сообщение Re[44]: MS забило на дотнет. Питону - да, сишарпу - нет? от 06.09.2021 3:24

Изменено 06.09.2021 7:00 Sinclair

Re[44]: MS забило на дотнет. Питону - да, сишарпу - нет?
Здравствуйте, vdimas, Вы писали:

V>Были проблемы, я их обрисовал, похоже, ты не понял на словах.

V>Даю в коде.
Да, код всегда помогает.

V>Вот так было нельзя:

V>
V>struct Struct1 {
V>    int a, b;
V>}

V>Struct1* ptr = stackalloc Struct1[42];
V>

Ну, это относительно простая проблема, т.к. в таком коде достаточно привести byte* к struct*.

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 int _length;
   private fixed int _inplaceArray0;
   private fixed int _inplaceArray1;
   ...
   public ref int this[int index] => ref MemoryMarshal.CreateSpan(ref _inplaceArray0, _length)[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. Пишем точно такой же код, как выше, только никаких _inplaceArray1 и далее.

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

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

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

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

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

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

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

V>Там в среднем 2-6 чисел в буфер надо напихать за раз, где первое число код команды, потом аргументы.
V>Вызывать ради этого нейтив не имеет смысла.
Я об этом сразу писал в комментах про вулкан — набивка чисел в буфер прекрасно живёт и в менеджед. А если у нас какая-то прямо жёсткая зависимость от внешнего вызова, но их много и они занимают мало времени на анменеджед стороне — 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 информацию с боевой машины/машин и доставлять их на машины, предназначенные для компиляции/оптимизации .
Лично я не возьмусь за проектирование такой системы — уж очень хрупкой она получается
Re[44]: MS забило на дотнет. Питону - да, сишарпу - нет?
Здравствуйте, 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 информацию с боевой машины/машин и доставлять их на машины, предназначенные для компиляции/оптимизации .
Лично я не возьмусь за проектирование такой системы — уж очень хрупкой она получается