Сообщение Re[47]: MS забило на дотнет. Питону - да, сишарпу - нет? от 08.09.2021 12:45
Изменено 08.09.2021 14:10 vdimas
Re[47]: MS забило на дотнет. Питону - да, сишарпу - нет?
Здравствуйте, Sinclair, Вы писали:
V>>Структуру можно вернуть и по-значению.
V>>Унутре вызывающая сторона подготавливает место под структуру и передаёт ссылку на это место в кач-ве аргумента.
S>Так по ссылке или по значению?
А как, по-твоему, возращается по-значению структура, размер которой больше ширины регистра?
Для инфы, в этом сценарии существует т.н. return value optimization (гугл).
V>>Получить ref или readonly ref-ссылку на поле структуры через метод самой структуры нельзя, можно только через метод-расширение, где саму структуру передавать через ref this.
S>Вот удивительно, но можно. То ли новый дотнет, то ли я в прошлый раз как-то не так смотрел:
S>https://sharplab.io/#v2:C4LghgzgtgPgAgJgIwFgBQcDMACR2DC2A3utmbjgK4B2EYAZgKbYTABOlAxsNgJLXAAgmzZgAnqXIk05WdgAObAJYA3MMGZKB2APoAbRtQDmwABYBuSXIXK1G7PSUAPRgBNsWnq/VgA2piQAXUsZawpsNkZ6D20zJQhfTxjXRidA7ABeAD4IqOxvYD8tFLSQsPCkgBlDE1NMnP0aszKwrD4BYVExAAokg2MzAEp63X7azOwx5qtsAF8Zmba4ABZsAFlu4elytTZsMAnqRgB3dqERcW6kAAZBlus/AFZAjOWEe7n0WaA=
Офигеть. ))
Ты только что нашёл баг компилятора:
Ключевое — readonly Struct1 field.
Надо проверить в 10-м C# и зарепортить, если еще не починили.
V>>В случае flexible structs всё-равно надо создать Span от inplace-массива с "вручную" отмеренным нужным размером, т.к. при создании Span никаких проверок не делается.
S>Проверки делаются при доступе к структуре, что позволяет передавать её в гражданский код.
В любых flexible-структурах фактическая длина известна после получения данных из некоторого АПИ.
Некий публичный метод или св-во могли бы возвращать Span.
S>Остаётся проблема с честностью выделения — если мы выделим такую структуру напрямую или нечаянно в результате боксинга, то всё взорвётся.
S>Можно попробовать сделать оператор приведения примерно так: https://sharplab.io/#v2:C4LghgzgtgPgAgJgIwFgBQcAMACOSB0ASgK4B2wAllAKb4CS51ATgPYAOAyswG4UDG1CAG506OAGZcSAGy4E2BsACCTJmACeACWoAbNs3QBvdNlO5JeWU2oAzBeRVr12JREWONACmAALChGwONjBSAB4AI3VgagA+bAATMGAwABpsCnJsHWpSAEoTM2M0MxL0u09s0gAqCAoAL2oWG08M4FyAalqGppbyXOw4xOT8ABkcgHNffOLS2d9WAHdsUmollXHiGnIAUQAPATZKFlJPUjAaHqGwXNyRGdmAWWooFiZ1B7AmCB8wHXwAdSYFGiniuaWsdkqtwKszgAHZsBDsE8Xm8Pl8fn9XIRbKF3KoNDFQUlrncSgBfdCUtBiSRkCBgGzUbAQYBMYh8YD2ZQE9RGGHYNhA7hJZmtbAAfUqkx8ZLMQooIui6UyEqumDlpgkiNsKq5vn8AG1xRl4tRdgBdbAAXjiSJRr3en2+v3wAGFrKKgiFPEi1STMGkpRMpsbSGbLZrzHrsGNSDKbXFg/HfHdqbS5Ng3dgiiVtfTGcy4AAWZGefq52bYEVMBI27CeVlgPgAa1+OhYfGwkWihqQmAQxYtuXwrnxTk8/cw0PuJQF1PJQA=
1. Дотнет не гарантирует фактический порядок расположение полей, необходимо задать StructLayout хотя бы Sequential
2. Продолжение того же бага:
Суть бага в следующем:
V>>Для интеропа не подойдёт, т.к. сломает разметку ожидаемой структуры.
S>Вот тут не понял.
Мне показалось, что ты хотел сделать Span как часть описания структуры.
В любом случае, Span лучше использовать в дизайне подобных объектов так:
Тогда for-циклы по полному Span-у соптимизируют одну проверку за выход за диапазон, как это делается для обычных массивов.
V>>Возможно.
V>>TCPDirect дают примерно 20ns задержку от прихода пакета в сетевую карточку, поток тупо в цикле опрашивает диспетчер.
V>>Но вызов туда нельзя помечать SuppressGCTransition, т.к. в этом вызове иногда может быть и вызов ядра (как повезёт).
S>Как я понял из их доки, почти все вызовы там (включая поллинг) — неблокирующие, поэтому можно.
Неблокирующий вызов не означает не использование примитивов синхронзации, а там сказано:
V>>Надо переносить в дотнет приличную часть драйвера — всё что касается набивки буфера команд.
S>На первый взгляд, там всё должно быть достаточно примитивно.
Ес-но.
Стиль "черного ящика" в плане подробностей АПИ используют, чтобы не вешать себе гири на ноги в деле совместимости.
Т.е., чтобы иметь возможность что-то менять унутре в любой следующей версии.
S>>>Если у нас какая-то прямо жёсткая зависимость от внешнего вызова, но их много и они занимают мало времени на анменеджед стороне — PInvoke вовсе не так уж плох. Всего лишь втрое дороже call EAX.
V>>В 17 раз дороже.
S> Бенчмарк с вами не согласен. Я же привёл результаты. В 10 раз дороже — если нет SuppressGCTransition, и в три раза, если он есть.
Если есть, то ОК.
S>17 раз нет ни в каком варианте.
На моей машине в 17 раз.
И у тебя там замеры вообще странные были, если по твоим замерам простой вызов выходил 7 ns.
S>>>А вот в сервер-сайд у нас приложения работают неделями.
V>>Но при обновлении должны будут тормозить несколько первых минут?
S>При рестарте.
Т.е., при каждом обновлении.
V>>Я не вижу причин, по которым сервер-сайд должен отказываться от АОТ.
V>>Т.е. вообще не вижу.
S>Я тоже не вижу причин отказываться от AOT, но только в том случае, если он собирается работать совместно c хот-споттингом.
S>Если же поставить выбор между АОТ и возможностью динамического кода + хотспот, то я выберу второе.
Выглядит так, что у тебя своеобразные представления как о возможностях AOT, так и о возможностях hot-spot оптимизаций.
V>>Боюсь, вычислительная сложность сериализации в XML такая, что никакой оптимизатор инлайнить это не будет.
V>>У оптимизаторов в любом случае стоят пороги срабатывания от сложности методов, в т.ч. транзитивной/вложенной сложности.
S>Ну, так это как раз потому, что у оптимизаторов ограничено время работы и нет информации о статистике выполнения.
У оффлайновых оптимизаторов время работы не ограничено, ограничен баланс м/у размером кода и оптимизацией.
Зато у хот-спот оптимизатора время ограничено.
V>>Скорее, оптимизатор заинлайнит что-то в кишках XML-сериализатора, но верхние уровни пойдут как есть.
S>Ну, в нашем случае мы написали высокопроизводительный XML-сериализатор на $"<flightData><flightNo>{Escape(d.GetString(0))}</flightNo><departureDate>{d.GetDate(1).ToString(r)}</departureDate></flightData>"
Вручную писаный сериализатор будет оптимизирован и в АОТ, в отличие от обещприкладного, который и со схемами общается, и биндинг динамически внутри строит и что только не делает.
V>>Современные джавовские хот-спот оптимизаторы имеют еще большие ограничения на сложность, чем нейтивные оптимизаторы.
V>>Т.е. на таком верхнем уровне вряд ли что-то будет сделано.
S>Это был умозрительный пример. На каком бы уровне мы ни работали, в ООП-коде всегда есть какой-то косвенный вызов.
Верно, АОТ потенциально способно убирать лишнюю косвенность.
Например, "выпрямлять" в памяти банальный List<T>.
S>И всегда есть статистика реального исполнения этого кода, которая меняется от инсталляции к инсталляции и даже в процессе работы одной и той же инсталляции.
Статистика в основном нужна для определения мест, где требуется оптимизация.
АОТ тупо может оптимизировать всё подряд.
И да, атрибуты оптимизации над методами никто не отменял.
S>>>При этом, опять же, после инлайна кода у нас появляется статистика о том, какие именно запросы делаются в dataReader. И, соответственно, возможность ещё что-то проинлайнить, выбросив лишние ветки if вместе с косвенными вызовами.
V>>Оптимизации выполняются на уровне конкретного типа.
V>>Плохо представляю себе приложение, где в DataReader в любой точке приложения приходит одна и та же схема данных. ))
S>Ну так в том-то и дело: пока мы делаем косвенный вызов метода DataReader, в call target мы вынуждены обрабатывать общий случай. Ведь VMT для всех пользователей этого DataReader одинаковая.
S>А вот когда мы встроили тело DataReader.GetDate(x) в конкретное место вызова, то внезапно оказывается, что для данной конкретной копии кода исходного GetDate в конкретном if/switch чаще всего (== всегда) попадается ровно один вариант. Ведь конкретно в этом месте всегда используется одна и та же схема данных.
Угу.
В рекордсете из 1001 элемента первые 1000 будут прочитаны без оптимизации.
Затем будет выполнена дорогостоящая оптимизация и последний элемент прочитают оптимальным образом.
Утрирую, но суть понятна.
У тебя в "одном и том же месте" каждый раз будут разные экземпляры DataReader и потенциально хотя бы немного отличающиеся схемы, хотя бы в плане nullable-полей, особенно если со стороны базы позвали какой-нить union. И даже если схемы будут те же — это будут другие экземпляры схем, вот в чём прикол.
S>Вроде бы не такая сложная конструкция, чего тут не понять? Погуглите любую статью со словами speculative inlining.
Который в той же JS-машинке требует предварительной проверки соответствия устройства объекта ожидаемому и работает с некоторым ограничением вложенности иерархии такой проверки, т.е. в относительно простых случаях.
В случае DataReader в нынешнем виде не прокатит — слишком большая иерархия объектов, всю её проверять на соответствие текущему хот-спот-коду будет накладней получаемых от хот-спот плюшек.
Надо упрощать архитектуру объектов.
Делать таблицу диспетчеризации, как я показывал.
Тогда динамически (по индексу) будет обращение только к строке такой таблицы, а столбец известен статически.
Тогда проверка соответствия лейаута объекта будет сводиться к проверке только ссылок на метаинформацию столбцов рекодрсета, где эти столбцы ссылаются на одни и те же статические-заготовленные конвертеры.
Т.е. сравни — проверить только равенство ссылок в объекте верхнего уровня или рекурсивно пройтись по кучерявым объектам, проверяя равенство всех полей всех дочерних элементов.
Счётчик сложности в последнем случае скажет "извините, в другой раз".
V>>Для DataReader "типы" полей динамические, зависят от схемы принятого рекордсета.
V>>Тут и С++ ничего не сделает.
S>Про С++ ничего сказать не могу. Способен ли он выполнять PGO-оптимизацию уже PGO-оптимизированного кода? А для хотспота это, насколько я знаю, норма.
"Норма" там в простейших случаях.
Я разбирал примеры работы джавовского хот-спота — покрывает только тривиальнейшие случаи и уже даже про них трубят как о победе.
V>>Надо тупо писать эффективный код.
V>>И в то же самое время читабельный и поддерживаемый. ))
S>Ну, так эти две цели в известной мере друг другу противоречат.
Распространённая ошибка.
Я показал устройство таблицы конвертеров в сообщении, на которое уже давал ссылку.
Читабельность и поддерживаемость (то бишь расширяемость) прекрасная.
А в варианте с новыми указателями на ф-ии — еще и максимально-эффективная, т.е. даже, грубо, на асме или IL быстрее не сделаешь.
И хотспоту проще ввиду низкой вычислительной сложности, т.е. мог бы заинлайнить тела вызываемых по указателю конвертеров, т.е. убрать косвенный вызов.
ОК, про гипотетический хотспот пока спорить не буду, по крайней мере пока его нет в природе для .Net.
V>>Всю эту кодогенерацию, которую ты показывал для linq, можно было выполнить и для AOT.
S>Ну, это всего лишь небольшой маленький пример. Реальные задачи не исчерпываются linq выражениями.
V>>Просто тут нужно соотв. плагинное АПИ к АОТ.
V>>Т.е., AOT даёт тебе конкретные типы, т.к. в закрытой системе типов они известны (даже если заведомо абстрактны — это тоже известно), а "плагин" генерит код, который опять же компиляется AOT.
S>В теории — интересно. На практике — где посмотреть на такой AOT?
Нигде.
Даже существующий полноценный доступен только для iOS на основе mono (ХЗ какого он там качества).
А виндовые UWP-приложения на .Net Core UWP компиляются серверами магазина Windows под сетку устройств, тот код публично недоступен.
V>>2. В обычных компиллируемых как минимум бета-редуцируемых языках (шаблоны С++ в пример) через генерирование двух версий — для проверенного и непроверенного констрейна.
S>Не двух, а 2количество констреинтов.
1. Так и есть, порой шаблоны порождают сетку-произведение воплощённых реализаций.
2. Опять включается счётчик сложности в случае хот-спота.
V>>Даже в языках с зависимыми типами всё-равно в какой-то точке программы идёт проверка и ветвление/диспетчеризация кода.
V>>Так же и в твоём примере с SQL-сервером, для данной сущности берётся одна из готовых реализаций, соотв. констрейнам этой сущности.
S>Нет. Просто при построении плана учитывается текущая статистика и актуальные метаданные.
Ес-но.
И затем берется одна из уже готовых реализаций способа скана.
В этом суть.
S>Напомню: в прошлый раз дискуссия про "повторное использование компонентов плана выполнения запроса" кончилась тем, что вы тихо слились, как только дело дошло до кода.
Я помню обратное — ты так и не прошёл понимание про повторно-используемые "кубики" при построении плана запроса.
S>И в этот раз будет то же самое.
Псевдокод давался.
V>>То бишь, у тебя может быть уже сетка неких алгоритмов под разные сценарии. При изменении сценария ты подставляешь указатель на нужный алгоритм.
S>Ага. Размер этой сетки для баз данных быстро начинает превышать разумные пределы. Как, впрочем, и для любых комбинаторных задач.
Это для динамических вещей.
Для статических еще в момент компиляции известны наличествующие индексы.
Поэтому, комбинаторика будет только по различиям в статистике, где для многих типов индексов, которые я обозвал enum (разновидность справочных данных эдакого системного плана, т.е. которые жестко привязаны к версии приложения) — статистика известна на момент компиляции. То бишь, кол-во уникальных значений в индексе.
V>>Хотспот еще более хрупкий и непредсказуемый.
S> Пока что он демонстрирует впечатляющие результаты
Увы.
Чтобы он демонстрировал "впечатляющие результаты" в нашей области, например, ребята раскладывают объекты на составяющие ручками.
Т.е., вместо
У них идёт:
А в самых критических случаях и вовсе:
Смысл примерно должен быть понятен.
А если поля разных типов (по ширине разных), то всё еще забавнее происходит над массивом байт.
S>И вытягивает её как раз JIT и хотспот.
Вытягивают её ручками, помогая хот-споту.
V>>Структуру можно вернуть и по-значению.
V>>Унутре вызывающая сторона подготавливает место под структуру и передаёт ссылку на это место в кач-ве аргумента.
S>Так по ссылке или по значению?
А как, по-твоему, возращается по-значению структура, размер которой больше ширины регистра?
Для инфы, в этом сценарии существует т.н. return value optimization (гугл).
V>>Получить ref или readonly ref-ссылку на поле структуры через метод самой структуры нельзя, можно только через метод-расширение, где саму структуру передавать через ref this.
S>Вот удивительно, но можно. То ли новый дотнет, то ли я в прошлый раз как-то не так смотрел:
S>https://sharplab.io/#v2:C4LghgzgtgPgAgJgIwFgBQcDMACR2DC2A3utmbjgK4B2EYAZgKbYTABOlAxsNgJLXAAgmzZgAnqXIk05WdgAObAJYA3MMGZKB2APoAbRtQDmwABYBuSXIXK1G7PSUAPRgBNsWnq/VgA2piQAXUsZawpsNkZ6D20zJQhfTxjXRidA7ABeAD4IqOxvYD8tFLSQsPCkgBlDE1NMnP0aszKwrD4BYVExAAokg2MzAEp63X7azOwx5qtsAF8Zmba4ABZsAFlu4elytTZsMAnqRgB3dqERcW6kAAZBlus/AFZAjOWEe7n0WaA=
Офигеть. ))
Ты только что нашёл баг компилятора:
public unsafe struct Struct1 {
internal int _someField;
internal fixed int inplaceArray[1];
// ошибка компиляции, как и ожидалось
// error CS8170: Struct members cannot return 'this' or other instance members by reference
public ref int SomeField => ref _someField;
// нет ошибки компиляции, хотя ожидается
public ref int this[int index] => ref inplaceArray[index];
}
public static unsafe class Struct1Ext {
public static ref int Item(ref this Struct1 @this, int index) => ref @this.inplaceArray[index];
}
internal class Class1 {
public readonly Struct1 field;
}
internal class Program {
private static void Main(string[] args) {
var c = new Class1();
// ошибка компиляции, как и ожидалось
// error CS0192: A readonly field cannot be used as a ref or out value (except in a constructor)
ref var ptr2 = ref c.field.Item(0);
// нет ошибки компиляции, хотя ожидается
ref var ptr1 = ref c.field[0];
var areSame = Unsafe.AreSame(ref ptr1, ref ptr2);
}
}
Ключевое — readonly Struct1 field.
Надо проверить в 10-м C# и зарепортить, если еще не починили.
V>>В случае flexible structs всё-равно надо создать Span от inplace-массива с "вручную" отмеренным нужным размером, т.к. при создании Span никаких проверок не делается.
S>Проверки делаются при доступе к структуре, что позволяет передавать её в гражданский код.
В любых flexible-структурах фактическая длина известна после получения данных из некоторого АПИ.
Некий публичный метод или св-во могли бы возвращать Span.
S>Остаётся проблема с честностью выделения — если мы выделим такую структуру напрямую или нечаянно в результате боксинга, то всё взорвётся.
S>Можно попробовать сделать оператор приведения примерно так: https://sharplab.io/#v2:C4LghgzgtgPgAgJgIwFgBQcAMACOSB0ASgK4B2wAllAKb4CS51ATgPYAOAyswG4UDG1CAG506OAGZcSAGy4E2BsACCTJmACeACWoAbNs3QBvdNlO5JeWU2oAzBeRVr12JREWONACmAALChGwONjBSAB4AI3VgagA+bAATMGAwABpsCnJsHWpSAEoTM2M0MxL0u09s0gAqCAoAL2oWG08M4FyAalqGppbyXOw4xOT8ABkcgHNffOLS2d9WAHdsUmollXHiGnIAUQAPATZKFlJPUjAaHqGwXNyRGdmAWWooFiZ1B7AmCB8wHXwAdSYFGiniuaWsdkqtwKszgAHZsBDsE8Xm8Pl8fn9XIRbKF3KoNDFQUlrncSgBfdCUtBiSRkCBgGzUbAQYBMYh8YD2ZQE9RGGHYNhA7hJZmtbAAfUqkx8ZLMQooIui6UyEqumDlpgkiNsKq5vn8AG1xRl4tRdgBdbAAXjiSJRr3en2+v3wAGFrKKgiFPEi1STMGkpRMpsbSGbLZrzHrsGNSDKbXFg/HfHdqbS5Ng3dgiiVtfTGcy4AAWZGefq52bYEVMBI27CeVlgPgAa1+OhYfGwkWihqQmAQxYtuXwrnxTk8/cw0PuJQF1PJQA=
1. Дотнет не гарантирует фактический порядок расположение полей, необходимо задать StructLayout хотя бы Sequential
2. Продолжение того же бага:
[StructLayout(LayoutKind.Sequential)]
public struct IntArray {
private int _length;
private int _data0;
// нет ошибки компиляции, хотя ожидается
public ref int this[int index] => ref MemoryMarshal.CreateSpan(ref _data0, Length)[index];
public int Length => _length;
}
[StructLayout(LayoutKind.Sequential)]
public struct IntArray2 {
private int _length;
private int _data0;
// ошибка компиляции, как и ожидалось
// error CS8347: Cannot use a result of 'Unsafe.Add<int>(ref int, int)' in this context because it may expose variables referenced by parameter 'source' outside of their declaration scope
public ref int this[int index] => ref Unsafe.Add(ref _data0, Length);
public int Length => _length;
}
[StructLayout(LayoutKind.Sequential)]
public unsafe struct IntArray4 {
private int _length;
private fixed int _data0[1];
// нет ошибки компиляции, хотя ожидается
public ref int this[int index] => ref Unsafe.Add(ref _data0[0], index);
public int Length => _length;
}
Суть бага в следующем:
internal class С2 {
private ref int Bar2() {
var value = 0;
// ошибка компиляции, как и ожидалось
// error CS8168: Cannot return local 'value' by reference because it is not a ref local
return ref value;
}
private unsafe struct SomeStruct {
public fixed int inplaceArray[42];
}
private unsafe ref int Foo() {
var s = new SomeStruct();
// нет ошибки компиляции, хотя ожидается
return ref s.inplaceArray[0];
}
}
V>>Для интеропа не подойдёт, т.к. сломает разметку ожидаемой структуры.
S>Вот тут не понял.
Мне показалось, что ты хотел сделать Span как часть описания структуры.
В любом случае, Span лучше использовать в дизайне подобных объектов так:
[StructLayout(LayoutKind.Sequential)]
public unsafe struct IntArray3
{
private int _length;
private fixed int _data0[1];
// нет ошибки компиляции, хотя ожидается
public Span<int> AsSpan() => MemoryMarshal.CreateSpan(ref _data0[0], Length);
public int Length => _length;
}
Тогда for-циклы по полному Span-у соптимизируют одну проверку за выход за диапазон, как это делается для обычных массивов.
V>>Возможно.
V>>TCPDirect дают примерно 20ns задержку от прихода пакета в сетевую карточку, поток тупо в цикле опрашивает диспетчер.
V>>Но вызов туда нельзя помечать SuppressGCTransition, т.к. в этом вызове иногда может быть и вызов ядра (как повезёт).
S>Как я понял из их доки, почти все вызовы там (включая поллинг) — неблокирующие, поэтому можно.
Неблокирующий вызов не означает не использование примитивов синхронзации, а там сказано:
* Not manipulate locks or other concurrency primitives
V>>Надо переносить в дотнет приличную часть драйвера — всё что касается набивки буфера команд.
S>На первый взгляд, там всё должно быть достаточно примитивно.
Ес-но.
Стиль "черного ящика" в плане подробностей АПИ используют, чтобы не вешать себе гири на ноги в деле совместимости.
Т.е., чтобы иметь возможность что-то менять унутре в любой следующей версии.
S>>>Если у нас какая-то прямо жёсткая зависимость от внешнего вызова, но их много и они занимают мало времени на анменеджед стороне — PInvoke вовсе не так уж плох. Всего лишь втрое дороже call EAX.
V>>В 17 раз дороже.
S> Бенчмарк с вами не согласен. Я же привёл результаты. В 10 раз дороже — если нет SuppressGCTransition, и в три раза, если он есть.
Если есть, то ОК.
S>17 раз нет ни в каком варианте.
На моей машине в 17 раз.
И у тебя там замеры вообще странные были, если по твоим замерам простой вызов выходил 7 ns.
S>>>А вот в сервер-сайд у нас приложения работают неделями.
V>>Но при обновлении должны будут тормозить несколько первых минут?
S>При рестарте.
Т.е., при каждом обновлении.
V>>Я не вижу причин, по которым сервер-сайд должен отказываться от АОТ.
V>>Т.е. вообще не вижу.
S>Я тоже не вижу причин отказываться от AOT, но только в том случае, если он собирается работать совместно c хот-споттингом.
S>Если же поставить выбор между АОТ и возможностью динамического кода + хотспот, то я выберу второе.
Выглядит так, что у тебя своеобразные представления как о возможностях AOT, так и о возможностях hot-spot оптимизаций.
V>>Боюсь, вычислительная сложность сериализации в XML такая, что никакой оптимизатор инлайнить это не будет.
V>>У оптимизаторов в любом случае стоят пороги срабатывания от сложности методов, в т.ч. транзитивной/вложенной сложности.
S>Ну, так это как раз потому, что у оптимизаторов ограничено время работы и нет информации о статистике выполнения.
У оффлайновых оптимизаторов время работы не ограничено, ограничен баланс м/у размером кода и оптимизацией.
Зато у хот-спот оптимизатора время ограничено.
V>>Скорее, оптимизатор заинлайнит что-то в кишках XML-сериализатора, но верхние уровни пойдут как есть.
S>Ну, в нашем случае мы написали высокопроизводительный XML-сериализатор на $"<flightData><flightNo>{Escape(d.GetString(0))}</flightNo><departureDate>{d.GetDate(1).ToString(r)}</departureDate></flightData>"
Вручную писаный сериализатор будет оптимизирован и в АОТ, в отличие от обещприкладного, который и со схемами общается, и биндинг динамически внутри строит и что только не делает.
V>>Современные джавовские хот-спот оптимизаторы имеют еще большие ограничения на сложность, чем нейтивные оптимизаторы.
V>>Т.е. на таком верхнем уровне вряд ли что-то будет сделано.
S>Это был умозрительный пример. На каком бы уровне мы ни работали, в ООП-коде всегда есть какой-то косвенный вызов.
Верно, АОТ потенциально способно убирать лишнюю косвенность.
Например, "выпрямлять" в памяти банальный List<T>.
S>И всегда есть статистика реального исполнения этого кода, которая меняется от инсталляции к инсталляции и даже в процессе работы одной и той же инсталляции.
Статистика в основном нужна для определения мест, где требуется оптимизация.
АОТ тупо может оптимизировать всё подряд.
И да, атрибуты оптимизации над методами никто не отменял.
S>>>При этом, опять же, после инлайна кода у нас появляется статистика о том, какие именно запросы делаются в dataReader. И, соответственно, возможность ещё что-то проинлайнить, выбросив лишние ветки if вместе с косвенными вызовами.
V>>Оптимизации выполняются на уровне конкретного типа.
V>>Плохо представляю себе приложение, где в DataReader в любой точке приложения приходит одна и та же схема данных. ))
S>Ну так в том-то и дело: пока мы делаем косвенный вызов метода DataReader, в call target мы вынуждены обрабатывать общий случай. Ведь VMT для всех пользователей этого DataReader одинаковая.
S>А вот когда мы встроили тело DataReader.GetDate(x) в конкретное место вызова, то внезапно оказывается, что для данной конкретной копии кода исходного GetDate в конкретном if/switch чаще всего (== всегда) попадается ровно один вариант. Ведь конкретно в этом месте всегда используется одна и та же схема данных.
Угу.
В рекордсете из 1001 элемента первые 1000 будут прочитаны без оптимизации.
Затем будет выполнена дорогостоящая оптимизация и последний элемент прочитают оптимальным образом.
Утрирую, но суть понятна.
У тебя в "одном и том же месте" каждый раз будут разные экземпляры DataReader и потенциально хотя бы немного отличающиеся схемы, хотя бы в плане nullable-полей, особенно если со стороны базы позвали какой-нить union. И даже если схемы будут те же — это будут другие экземпляры схем, вот в чём прикол.
S>Вроде бы не такая сложная конструкция, чего тут не понять? Погуглите любую статью со словами speculative inlining.
Который в той же JS-машинке требует предварительной проверки соответствия устройства объекта ожидаемому и работает с некоторым ограничением вложенности иерархии такой проверки, т.е. в относительно простых случаях.
В случае DataReader в нынешнем виде не прокатит — слишком большая иерархия объектов, всю её проверять на соответствие текущему хот-спот-коду будет накладней получаемых от хот-спот плюшек.
Надо упрощать архитектуру объектов.
Делать таблицу диспетчеризации, как я показывал.
Тогда динамически (по индексу) будет обращение только к строке такой таблицы, а столбец известен статически.
Тогда проверка соответствия лейаута объекта будет сводиться к проверке только ссылок на метаинформацию столбцов рекодрсета, где эти столбцы ссылаются на одни и те же статические-заготовленные конвертеры.
Т.е. сравни — проверить только равенство ссылок в объекте верхнего уровня или рекурсивно пройтись по кучерявым объектам, проверяя равенство всех полей всех дочерних элементов.
Счётчик сложности в последнем случае скажет "извините, в другой раз".
V>>Для DataReader "типы" полей динамические, зависят от схемы принятого рекордсета.
V>>Тут и С++ ничего не сделает.
S>Про С++ ничего сказать не могу. Способен ли он выполнять PGO-оптимизацию уже PGO-оптимизированного кода? А для хотспота это, насколько я знаю, норма.
"Норма" там в простейших случаях.
Я разбирал примеры работы джавовского хот-спота — покрывает только тривиальнейшие случаи и уже даже про них трубят как о победе.
V>>Надо тупо писать эффективный код.
V>>И в то же самое время читабельный и поддерживаемый. ))
S>Ну, так эти две цели в известной мере друг другу противоречат.
Распространённая ошибка.
Я показал устройство таблицы конвертеров в сообщении, на которое уже давал ссылку.
Читабельность и поддерживаемость (то бишь расширяемость) прекрасная.
А в варианте с новыми указателями на ф-ии — еще и максимально-эффективная, т.е. даже, грубо, на асме или IL быстрее не сделаешь.
И хотспоту проще ввиду низкой вычислительной сложности, т.е. мог бы заинлайнить тела вызываемых по указателю конвертеров, т.е. убрать косвенный вызов.
ОК, про гипотетический хотспот пока спорить не буду, по крайней мере пока его нет в природе для .Net.
V>>Всю эту кодогенерацию, которую ты показывал для linq, можно было выполнить и для AOT.
S>Ну, это всего лишь небольшой маленький пример. Реальные задачи не исчерпываются linq выражениями.
V>>Просто тут нужно соотв. плагинное АПИ к АОТ.
V>>Т.е., AOT даёт тебе конкретные типы, т.к. в закрытой системе типов они известны (даже если заведомо абстрактны — это тоже известно), а "плагин" генерит код, который опять же компиляется AOT.
S>В теории — интересно. На практике — где посмотреть на такой AOT?
Нигде.
Даже существующий полноценный доступен только для iOS на основе mono (ХЗ какого он там качества).
А виндовые UWP-приложения на .Net Core UWP компиляются серверами магазина Windows под сетку устройств, тот код публично недоступен.
V>>2. В обычных компиллируемых как минимум бета-редуцируемых языках (шаблоны С++ в пример) через генерирование двух версий — для проверенного и непроверенного констрейна.
S>Не двух, а 2количество констреинтов.
1. Так и есть, порой шаблоны порождают сетку-произведение воплощённых реализаций.
2. Опять включается счётчик сложности в случае хот-спота.
V>>Даже в языках с зависимыми типами всё-равно в какой-то точке программы идёт проверка и ветвление/диспетчеризация кода.
V>>Так же и в твоём примере с SQL-сервером, для данной сущности берётся одна из готовых реализаций, соотв. констрейнам этой сущности.
S>Нет. Просто при построении плана учитывается текущая статистика и актуальные метаданные.
Ес-но.
И затем берется одна из уже готовых реализаций способа скана.
В этом суть.
S>Напомню: в прошлый раз дискуссия про "повторное использование компонентов плана выполнения запроса" кончилась тем, что вы тихо слились, как только дело дошло до кода.
Я помню обратное — ты так и не прошёл понимание про повторно-используемые "кубики" при построении плана запроса.
S>И в этот раз будет то же самое.
Псевдокод давался.
V>>То бишь, у тебя может быть уже сетка неких алгоритмов под разные сценарии. При изменении сценария ты подставляешь указатель на нужный алгоритм.
S>Ага. Размер этой сетки для баз данных быстро начинает превышать разумные пределы. Как, впрочем, и для любых комбинаторных задач.
Это для динамических вещей.
Для статических еще в момент компиляции известны наличествующие индексы.
Поэтому, комбинаторика будет только по различиям в статистике, где для многих типов индексов, которые я обозвал enum (разновидность справочных данных эдакого системного плана, т.е. которые жестко привязаны к версии приложения) — статистика известна на момент компиляции. То бишь, кол-во уникальных значений в индексе.
V>>Хотспот еще более хрупкий и непредсказуемый.
S> Пока что он демонстрирует впечатляющие результаты
Увы.
Чтобы он демонстрировал "впечатляющие результаты" в нашей области, например, ребята раскладывают объекты на составяющие ручками.
Т.е., вместо
class SomeClass { int a, b, c; };
SomeClass c1 = new SomeClass[42];
У них идёт:
int[] aFields = new int[42];
int[] bFields = new int[42];
int[] cFields = new int[42];
А в самых критических случаях и вовсе:
class SomeClassHelper {
const int FIELD_COUNT = 3;
const int FIELD_A_OFFSET = 0;
const int FIELD_B_OFFSET = 1;
const int FIELD_C_OFFSET = 2;
public void write(int[] array, int index, int a, int b, int c) {
index = index * FIELD_COUNT;
array[index + FIELD_A_OFFSET] = a;
...
}
int[] data = ...
SomeClassHelper.Write(data, index, a, b, c);
Смысл примерно должен быть понятен.
А если поля разных типов (по ширине разных), то всё еще забавнее происходит над массивом байт.
S>И вытягивает её как раз JIT и хотспот.
Вытягивают её ручками, помогая хот-споту.
Re[47]: MS забило на дотнет. Питону - да, сишарпу - нет?
Здравствуйте, Sinclair, Вы писали:
V>>Структуру можно вернуть и по-значению.
V>>Унутре вызывающая сторона подготавливает место под структуру и передаёт ссылку на это место в кач-ве аргумента.
S>Так по ссылке или по значению?
А как, по-твоему, возращается по-значению структура, размер которой больше ширины регистра?
Для инфы, в этом сценарии существует т.н. return value optimization (гугл).
V>>Получить ref или readonly ref-ссылку на поле структуры через метод самой структуры нельзя, можно только через метод-расширение, где саму структуру передавать через ref this.
S>Вот удивительно, но можно. То ли новый дотнет, то ли я в прошлый раз как-то не так смотрел:
S>https://sharplab.io/#v2:C4LghgzgtgPgAgJgIwFgBQcDMACR2DC2A3utmbjgK4B2EYAZgKbYTABOlAxsNgJLXAAgmzZgAnqXIk05WdgAObAJYA3MMGZKB2APoAbRtQDmwABYBuSXIXK1G7PSUAPRgBNsWnq/VgA2piQAXUsZawpsNkZ6D20zJQhfTxjXRidA7ABeAD4IqOxvYD8tFLSQsPCkgBlDE1NMnP0aszKwrD4BYVExAAokg2MzAEp63X7azOwx5qtsAF8Zmba4ABZsAFlu4elytTZsMAnqRgB3dqERcW6kAAZBlus/AFZAjOWEe7n0WaA=
Офигеть. ))
Ты только что нашёл баг компилятора:
Ключевое — readonly Struct1 field.
Надо проверить в 10-м C# и зарепортить, если еще не починили.
V>>В случае flexible structs всё-равно надо создать Span от inplace-массива с "вручную" отмеренным нужным размером, т.к. при создании Span никаких проверок не делается.
S>Проверки делаются при доступе к структуре, что позволяет передавать её в гражданский код.
В любых flexible-структурах фактическая длина известна после получения данных из некоторого АПИ.
Некий публичный метод или св-во могли бы возвращать Span.
S>Остаётся проблема с честностью выделения — если мы выделим такую структуру напрямую или нечаянно в результате боксинга, то всё взорвётся.
S>Можно попробовать сделать оператор приведения примерно так: https://sharplab.io/#v2:C4LghgzgtgPgAgJgIwFgBQcAMACOSB0ASgK4B2wAllAKb4CS51ATgPYAOAyswG4UDG1CAG506OAGZcSAGy4E2BsACCTJmACeACWoAbNs3QBvdNlO5JeWU2oAzBeRVr12JREWONACmAALChGwONjBSAB4AI3VgagA+bAATMGAwABpsCnJsHWpSAEoTM2M0MxL0u09s0gAqCAoAL2oWG08M4FyAalqGppbyXOw4xOT8ABkcgHNffOLS2d9WAHdsUmollXHiGnIAUQAPATZKFlJPUjAaHqGwXNyRGdmAWWooFiZ1B7AmCB8wHXwAdSYFGiniuaWsdkqtwKszgAHZsBDsE8Xm8Pl8fn9XIRbKF3KoNDFQUlrncSgBfdCUtBiSRkCBgGzUbAQYBMYh8YD2ZQE9RGGHYNhA7hJZmtbAAfUqkx8ZLMQooIui6UyEqumDlpgkiNsKq5vn8AG1xRl4tRdgBdbAAXjiSJRr3en2+v3wAGFrKKgiFPEi1STMGkpRMpsbSGbLZrzHrsGNSDKbXFg/HfHdqbS5Ng3dgiiVtfTGcy4AAWZGefq52bYEVMBI27CeVlgPgAa1+OhYfGwkWihqQmAQxYtuXwrnxTk8/cw0PuJQF1PJQA=
1. Дотнет не гарантирует фактический порядок расположение полей, необходимо задать StructLayout хотя бы Sequential
2. Продолжение того же бага:
Суть бага в следующем:
V>>Для интеропа не подойдёт, т.к. сломает разметку ожидаемой структуры.
S>Вот тут не понял.
Мне показалось, что ты хотел сделать Span как часть описания структуры.
В любом случае, Span лучше использовать в дизайне подобных объектов так:
Тогда for-циклы по полному Span-у соптимизируют одну проверку за выход за диапазон, как это делается для обычных массивов.
V>>Возможно.
V>>TCPDirect дают примерно 20ns задержку от прихода пакета в сетевую карточку, поток тупо в цикле опрашивает диспетчер.
V>>Но вызов туда нельзя помечать SuppressGCTransition, т.к. в этом вызове иногда может быть и вызов ядра (как повезёт).
S>Как я понял из их доки, почти все вызовы там (включая поллинг) — неблокирующие, поэтому можно.
Неблокирующий вызов не означает не использование примитивов синхронзации, а там сказано:
V>>Надо переносить в дотнет приличную часть драйвера — всё что касается набивки буфера команд.
S>На первый взгляд, там всё должно быть достаточно примитивно.
Ес-но.
Стиль "черного ящика" в плане подробностей АПИ используют, чтобы не вешать себе гири на ноги в деле совместимости.
Т.е., чтобы иметь возможность что-то менять унутре в любой следующей версии.
S>>>Если у нас какая-то прямо жёсткая зависимость от внешнего вызова, но их много и они занимают мало времени на анменеджед стороне — PInvoke вовсе не так уж плох. Всего лишь втрое дороже call EAX.
V>>В 17 раз дороже.
S> Бенчмарк с вами не согласен. Я же привёл результаты. В 10 раз дороже — если нет SuppressGCTransition, и в три раза, если он есть.
Если есть, то ОК.
S>17 раз нет ни в каком варианте.
На моей машине в 17 раз.
И у тебя там замеры вообще странные были, если по твоим замерам простой вызов выходил 7 ns.
S>>>А вот в сервер-сайд у нас приложения работают неделями.
V>>Но при обновлении должны будут тормозить несколько первых минут?
S>При рестарте.
Т.е., при каждом обновлении.
V>>Я не вижу причин, по которым сервер-сайд должен отказываться от АОТ.
V>>Т.е. вообще не вижу.
S>Я тоже не вижу причин отказываться от AOT, но только в том случае, если он собирается работать совместно c хот-споттингом.
S>Если же поставить выбор между АОТ и возможностью динамического кода + хотспот, то я выберу второе.
Выглядит так, что у тебя своеобразные представления как о возможностях AOT, так и о возможностях hot-spot оптимизаций.
V>>Боюсь, вычислительная сложность сериализации в XML такая, что никакой оптимизатор инлайнить это не будет.
V>>У оптимизаторов в любом случае стоят пороги срабатывания от сложности методов, в т.ч. транзитивной/вложенной сложности.
S>Ну, так это как раз потому, что у оптимизаторов ограничено время работы и нет информации о статистике выполнения.
У оффлайновых оптимизаторов время работы не ограничено, ограничен баланс м/у размером кода и оптимизацией.
Зато у хот-спот оптимизатора время ограничено.
V>>Скорее, оптимизатор заинлайнит что-то в кишках XML-сериализатора, но верхние уровни пойдут как есть.
S>Ну, в нашем случае мы написали высокопроизводительный XML-сериализатор на $"<flightData><flightNo>{Escape(d.GetString(0))}</flightNo><departureDate>{d.GetDate(1).ToString(r)}</departureDate></flightData>"
Вручную писаный сериализатор будет оптимизирован и в АОТ, в отличие от обещприкладного, который и со схемами общается, и биндинг динамически внутри строит и что только не делает.
V>>Современные джавовские хот-спот оптимизаторы имеют еще большие ограничения на сложность, чем нейтивные оптимизаторы.
V>>Т.е. на таком верхнем уровне вряд ли что-то будет сделано.
S>Это был умозрительный пример. На каком бы уровне мы ни работали, в ООП-коде всегда есть какой-то косвенный вызов.
Верно, АОТ потенциально способно убирать лишнюю косвенность.
Например, "выпрямлять" в памяти банальный List<T>.
S>И всегда есть статистика реального исполнения этого кода, которая меняется от инсталляции к инсталляции и даже в процессе работы одной и той же инсталляции.
Статистика в основном нужна для определения мест, где требуется оптимизация.
АОТ тупо может оптимизировать всё подряд.
И да, атрибуты оптимизации над методами никто не отменял.
S>>>При этом, опять же, после инлайна кода у нас появляется статистика о том, какие именно запросы делаются в dataReader. И, соответственно, возможность ещё что-то проинлайнить, выбросив лишние ветки if вместе с косвенными вызовами.
V>>Оптимизации выполняются на уровне конкретного типа.
V>>Плохо представляю себе приложение, где в DataReader в любой точке приложения приходит одна и та же схема данных. ))
S>Ну так в том-то и дело: пока мы делаем косвенный вызов метода DataReader, в call target мы вынуждены обрабатывать общий случай. Ведь VMT для всех пользователей этого DataReader одинаковая.
S>А вот когда мы встроили тело DataReader.GetDate(x) в конкретное место вызова, то внезапно оказывается, что для данной конкретной копии кода исходного GetDate в конкретном if/switch чаще всего (== всегда) попадается ровно один вариант. Ведь конкретно в этом месте всегда используется одна и та же схема данных.
Угу.
В рекордсете из 1001 элемента первые 1000 будут прочитаны без оптимизации.
Затем будет выполнена дорогостоящая оптимизация и последний элемент прочитают оптимальным образом.
Утрирую, но суть понятна.
У тебя в "одном и том же месте" каждый раз будут разные экземпляры DataReader и потенциально хотя бы немного отличающиеся схемы, хотя бы в плане nullable-полей, особенно если со стороны базы позвали какой-нить union. И даже если схемы будут те же — это будут другие экземпляры схем, вот в чём прикол.
S>Вроде бы не такая сложная конструкция, чего тут не понять? Погуглите любую статью со словами speculative inlining.
Который в той же JS-машинке требует предварительной проверки соответствия устройства объекта ожидаемому и работает с некоторым ограничением вложенности иерархии такой проверки, т.е. в относительно простых случаях.
В случае DataReader в нынешнем виде не прокатит — слишком большая иерархия объектов, всю её проверять на соответствие текущему хот-спот-коду будет накладней получаемых от хот-спот плюшек.
Надо упрощать архитектуру объектов.
Делать таблицу диспетчеризации, как я показывал.
Тогда динамически (по индексу) будет обращение только к строке такой таблицы, а столбец известен статически.
Тогда проверка соответствия лейаута объекта будет сводиться к проверке только ссылок на метаинформацию столбцов рекодрсета, где эти столбцы ссылаются на одни и те же статические-заготовленные конвертеры.
Т.е. сравни — проверить только равенство ссылок в объекте верхнего уровня или рекурсивно пройтись по кучерявым объектам, проверяя равенство всех полей всех дочерних элементов.
Счётчик сложности в последнем случае скажет "извините, в другой раз".
V>>Для DataReader "типы" полей динамические, зависят от схемы принятого рекордсета.
V>>Тут и С++ ничего не сделает.
S>Про С++ ничего сказать не могу. Способен ли он выполнять PGO-оптимизацию уже PGO-оптимизированного кода? А для хотспота это, насколько я знаю, норма.
"Норма" там в простейших случаях.
Я разбирал примеры работы джавовского хот-спота — покрывает только тривиальнейшие случаи и уже даже про них трубят как о победе.
V>>Надо тупо писать эффективный код.
V>>И в то же самое время читабельный и поддерживаемый. ))
S>Ну, так эти две цели в известной мере друг другу противоречат.
Распространённая ошибка.
Я показал устройство таблицы конвертеров в сообщении, на которое уже давал ссылку.
Читабельность и поддерживаемость (то бишь расширяемость) прекрасная.
А в варианте с новыми указателями на ф-ии — еще и максимально-эффективная, т.е. даже, грубо, на асме или IL быстрее не сделаешь.
И хотспоту проще ввиду низкой вычислительной сложности, т.е. мог бы заинлайнить тела вызываемых по указателю конвертеров, т.е. убрать косвенный вызов.
ОК, про гипотетический хотспот пока спорить не буду, по крайней мере пока его нет в природе для .Net.
V>>Всю эту кодогенерацию, которую ты показывал для linq, можно было выполнить и для AOT.
S>Ну, это всего лишь небольшой маленький пример. Реальные задачи не исчерпываются linq выражениями.
V>>Просто тут нужно соотв. плагинное АПИ к АОТ.
V>>Т.е., AOT даёт тебе конкретные типы, т.к. в закрытой системе типов они известны (даже если заведомо абстрактны — это тоже известно), а "плагин" генерит код, который опять же компиляется AOT.
S>В теории — интересно. На практике — где посмотреть на такой AOT?
Нигде.
Даже существующий полноценный доступен только для iOS на основе mono (ХЗ какого он там качества).
А виндовые UWP-приложения на .Net Core UWP компиляются серверами магазина Windows под сетку устройств, тот код публично недоступен.
V>>2. В обычных компиллируемых как минимум бета-редуцируемых языках (шаблоны С++ в пример) через генерирование двух версий — для проверенного и непроверенного констрейна.
S>Не двух, а 2количество констреинтов.
1. Так и есть, порой шаблоны порождают сетку-произведение воплощённых реализаций.
2. Опять включается счётчик сложности в случае хот-спота.
V>>Даже в языках с зависимыми типами всё-равно в какой-то точке программы идёт проверка и ветвление/диспетчеризация кода.
V>>Так же и в твоём примере с SQL-сервером, для данной сущности берётся одна из готовых реализаций, соотв. констрейнам этой сущности.
S>Нет. Просто при построении плана учитывается текущая статистика и актуальные метаданные.
Ес-но.
И затем берется одна из уже готовых реализаций способа скана.
В этом суть.
S>Напомню: в прошлый раз дискуссия про "повторное использование компонентов плана выполнения запроса" кончилась тем, что вы тихо слились, как только дело дошло до кода.
Я помню обратное — ты так и не прошёл понимание про повторно-используемые "кубики" при построении плана запроса.
S>И в этот раз будет то же самое.
Псевдокод давался.
V>>То бишь, у тебя может быть уже сетка неких алгоритмов под разные сценарии. При изменении сценария ты подставляешь указатель на нужный алгоритм.
S>Ага. Размер этой сетки для баз данных быстро начинает превышать разумные пределы. Как, впрочем, и для любых комбинаторных задач.
Это для динамических вещей.
Для статических еще в момент компиляции известны наличествующие индексы.
Поэтому, комбинаторика будет только по различиям в статистике, где для многих типов индексов, которые я обозвал enum (разновидность справочных данных эдакого системного плана, т.е. которые жестко привязаны к версии приложения) — статистика известна на момент компиляции. То бишь, кол-во уникальных значений в индексе.
V>>Хотспот еще более хрупкий и непредсказуемый.
S> Пока что он демонстрирует впечатляющие результаты
Увы.
Чтобы он демонстрировал "впечатляющие результаты" в нашей области, например, ребята раскладывают объекты на составяющие ручками.
Т.е., вместо
У них идёт:
А в самых критических случаях и вовсе:
Смысл примерно должен быть понятен.
А если поля разных типов (по ширине разных), то всё еще забавнее происходит над массивом байт.
S>И вытягивает её как раз JIT и хотспот.
Вытягивают её ручками, помогая хот-споту.
V>>Структуру можно вернуть и по-значению.
V>>Унутре вызывающая сторона подготавливает место под структуру и передаёт ссылку на это место в кач-ве аргумента.
S>Так по ссылке или по значению?
А как, по-твоему, возращается по-значению структура, размер которой больше ширины регистра?
Для инфы, в этом сценарии существует т.н. return value optimization (гугл).
V>>Получить ref или readonly ref-ссылку на поле структуры через метод самой структуры нельзя, можно только через метод-расширение, где саму структуру передавать через ref this.
S>Вот удивительно, но можно. То ли новый дотнет, то ли я в прошлый раз как-то не так смотрел:
S>https://sharplab.io/#v2:C4LghgzgtgPgAgJgIwFgBQcDMACR2DC2A3utmbjgK4B2EYAZgKbYTABOlAxsNgJLXAAgmzZgAnqXIk05WdgAObAJYA3MMGZKB2APoAbRtQDmwABYBuSXIXK1G7PSUAPRgBNsWnq/VgA2piQAXUsZawpsNkZ6D20zJQhfTxjXRidA7ABeAD4IqOxvYD8tFLSQsPCkgBlDE1NMnP0aszKwrD4BYVExAAokg2MzAEp63X7azOwx5qtsAF8Zmba4ABZsAFlu4elytTZsMAnqRgB3dqERcW6kAAZBlus/AFZAjOWEe7n0WaA=
Офигеть. ))
Ты только что нашёл баг компилятора:
public unsafe struct Struct1 {
internal int _someField;
internal fixed int inplaceArray[1];
// ошибка компиляции, как и ожидалось
// error CS8170: Struct members cannot return 'this' or other instance members by reference
public ref int SomeField => ref _someField;
// нет ошибки компиляции, хотя ожидается
public ref int this[int index] => ref inplaceArray[index];
}
public static unsafe class Struct1Ext {
public static ref int Item(ref this Struct1 @this, int index) => ref @this.inplaceArray[index];
}
internal class Class1 {
public readonly Struct1 field;
}
internal class Program {
private static void Main(string[] args) {
var c = new Class1();
// ошибка компиляции, как и ожидалось
// error CS0192: A readonly field cannot be used as a ref or out value (except in a constructor)
ref var ptr2 = ref c.field.Item(0);
// нет ошибки компиляции, хотя ожидается
ref var ptr1 = ref c.field[0];
var areSame = Unsafe.AreSame(ref ptr1, ref ptr2);
}
}
Ключевое — readonly Struct1 field.
Надо проверить в 10-м C# и зарепортить, если еще не починили.
V>>В случае flexible structs всё-равно надо создать Span от inplace-массива с "вручную" отмеренным нужным размером, т.к. при создании Span никаких проверок не делается.
S>Проверки делаются при доступе к структуре, что позволяет передавать её в гражданский код.
В любых flexible-структурах фактическая длина известна после получения данных из некоторого АПИ.
Некий публичный метод или св-во могли бы возвращать Span.
S>Остаётся проблема с честностью выделения — если мы выделим такую структуру напрямую или нечаянно в результате боксинга, то всё взорвётся.
S>Можно попробовать сделать оператор приведения примерно так: https://sharplab.io/#v2:C4LghgzgtgPgAgJgIwFgBQcAMACOSB0ASgK4B2wAllAKb4CS51ATgPYAOAyswG4UDG1CAG506OAGZcSAGy4E2BsACCTJmACeACWoAbNs3QBvdNlO5JeWU2oAzBeRVr12JREWONACmAALChGwONjBSAB4AI3VgagA+bAATMGAwABpsCnJsHWpSAEoTM2M0MxL0u09s0gAqCAoAL2oWG08M4FyAalqGppbyXOw4xOT8ABkcgHNffOLS2d9WAHdsUmollXHiGnIAUQAPATZKFlJPUjAaHqGwXNyRGdmAWWooFiZ1B7AmCB8wHXwAdSYFGiniuaWsdkqtwKszgAHZsBDsE8Xm8Pl8fn9XIRbKF3KoNDFQUlrncSgBfdCUtBiSRkCBgGzUbAQYBMYh8YD2ZQE9RGGHYNhA7hJZmtbAAfUqkx8ZLMQooIui6UyEqumDlpgkiNsKq5vn8AG1xRl4tRdgBdbAAXjiSJRr3en2+v3wAGFrKKgiFPEi1STMGkpRMpsbSGbLZrzHrsGNSDKbXFg/HfHdqbS5Ng3dgiiVtfTGcy4AAWZGefq52bYEVMBI27CeVlgPgAa1+OhYfGwkWihqQmAQxYtuXwrnxTk8/cw0PuJQF1PJQA=
1. Дотнет не гарантирует фактический порядок расположение полей, необходимо задать StructLayout хотя бы Sequential
2. Продолжение того же бага:
[StructLayout(LayoutKind.Sequential)]
public struct IntArray {
private int _length;
private int _data0;
// нет ошибки компиляции, хотя ожидается
public ref int this[int index] => ref MemoryMarshal.CreateSpan(ref _data0, Length)[index];
public int Length => _length;
}
[StructLayout(LayoutKind.Sequential)]
public struct IntArray2 {
private int _length;
private int _data0;
// ошибка компиляции, как и ожидалось
// error CS8347: Cannot use a result of 'Unsafe.Add<int>(ref int, int)' in this context because it may expose variables referenced by parameter 'source' outside of their declaration scope
public ref int this[int index] => ref Unsafe.Add(ref _data0, Length);
public int Length => _length;
}
[StructLayout(LayoutKind.Sequential)]
public unsafe struct IntArray4 {
private int _length;
private fixed int _data0[1];
// нет ошибки компиляции, хотя ожидается
public ref int this[int index] => ref Unsafe.Add(ref _data0[0], index);
public int Length => _length;
}
Суть бага в следующем:
internal class С2 {
private ref int Bar2() {
var value = 0;
// ошибка компиляции, как и ожидалось
// error CS8168: Cannot return local 'value' by reference because it is not a ref local
return ref value;
}
private ref int Bar3()
{
var value = 0;
ref var valueRef = ref value;
// ошибка компиляции, как и ожидалось
// error CS8157: Cannot return 'valueRef' by reference because it was initialized to a value that cannot be returned by reference
return ref valueRef;
}
private unsafe struct SomeStruct {
public fixed int inplaceArray[42];
}
private unsafe ref int Foo() {
var s = new SomeStruct();
// нет ошибки компиляции, хотя ожидается
return ref s.inplaceArray[0];
}
}
V>>Для интеропа не подойдёт, т.к. сломает разметку ожидаемой структуры.
S>Вот тут не понял.
Мне показалось, что ты хотел сделать Span как часть описания структуры.
В любом случае, Span лучше использовать в дизайне подобных объектов так:
[StructLayout(LayoutKind.Sequential)]
public unsafe struct IntArray3
{
private int _length;
private fixed int _data0[1];
// нет ошибки компиляции, хотя ожидается
public Span<int> AsSpan() => MemoryMarshal.CreateSpan(ref _data0[0], Length);
public int Length => _length;
}
Тогда for-циклы по полному Span-у соптимизируют одну проверку за выход за диапазон, как это делается для обычных массивов.
V>>Возможно.
V>>TCPDirect дают примерно 20ns задержку от прихода пакета в сетевую карточку, поток тупо в цикле опрашивает диспетчер.
V>>Но вызов туда нельзя помечать SuppressGCTransition, т.к. в этом вызове иногда может быть и вызов ядра (как повезёт).
S>Как я понял из их доки, почти все вызовы там (включая поллинг) — неблокирующие, поэтому можно.
Неблокирующий вызов не означает не использование примитивов синхронзации, а там сказано:
* Not manipulate locks or other concurrency primitives
V>>Надо переносить в дотнет приличную часть драйвера — всё что касается набивки буфера команд.
S>На первый взгляд, там всё должно быть достаточно примитивно.
Ес-но.
Стиль "черного ящика" в плане подробностей АПИ используют, чтобы не вешать себе гири на ноги в деле совместимости.
Т.е., чтобы иметь возможность что-то менять унутре в любой следующей версии.
S>>>Если у нас какая-то прямо жёсткая зависимость от внешнего вызова, но их много и они занимают мало времени на анменеджед стороне — PInvoke вовсе не так уж плох. Всего лишь втрое дороже call EAX.
V>>В 17 раз дороже.
S> Бенчмарк с вами не согласен. Я же привёл результаты. В 10 раз дороже — если нет SuppressGCTransition, и в три раза, если он есть.
Если есть, то ОК.
S>17 раз нет ни в каком варианте.
На моей машине в 17 раз.
И у тебя там замеры вообще странные были, если по твоим замерам простой вызов выходил 7 ns.
S>>>А вот в сервер-сайд у нас приложения работают неделями.
V>>Но при обновлении должны будут тормозить несколько первых минут?
S>При рестарте.
Т.е., при каждом обновлении.
V>>Я не вижу причин, по которым сервер-сайд должен отказываться от АОТ.
V>>Т.е. вообще не вижу.
S>Я тоже не вижу причин отказываться от AOT, но только в том случае, если он собирается работать совместно c хот-споттингом.
S>Если же поставить выбор между АОТ и возможностью динамического кода + хотспот, то я выберу второе.
Выглядит так, что у тебя своеобразные представления как о возможностях AOT, так и о возможностях hot-spot оптимизаций.
V>>Боюсь, вычислительная сложность сериализации в XML такая, что никакой оптимизатор инлайнить это не будет.
V>>У оптимизаторов в любом случае стоят пороги срабатывания от сложности методов, в т.ч. транзитивной/вложенной сложности.
S>Ну, так это как раз потому, что у оптимизаторов ограничено время работы и нет информации о статистике выполнения.
У оффлайновых оптимизаторов время работы не ограничено, ограничен баланс м/у размером кода и оптимизацией.
Зато у хот-спот оптимизатора время ограничено.
V>>Скорее, оптимизатор заинлайнит что-то в кишках XML-сериализатора, но верхние уровни пойдут как есть.
S>Ну, в нашем случае мы написали высокопроизводительный XML-сериализатор на $"<flightData><flightNo>{Escape(d.GetString(0))}</flightNo><departureDate>{d.GetDate(1).ToString(r)}</departureDate></flightData>"
Вручную писаный сериализатор будет оптимизирован и в АОТ, в отличие от обещприкладного, который и со схемами общается, и биндинг динамически внутри строит и что только не делает.
V>>Современные джавовские хот-спот оптимизаторы имеют еще большие ограничения на сложность, чем нейтивные оптимизаторы.
V>>Т.е. на таком верхнем уровне вряд ли что-то будет сделано.
S>Это был умозрительный пример. На каком бы уровне мы ни работали, в ООП-коде всегда есть какой-то косвенный вызов.
Верно, АОТ потенциально способно убирать лишнюю косвенность.
Например, "выпрямлять" в памяти банальный List<T>.
S>И всегда есть статистика реального исполнения этого кода, которая меняется от инсталляции к инсталляции и даже в процессе работы одной и той же инсталляции.
Статистика в основном нужна для определения мест, где требуется оптимизация.
АОТ тупо может оптимизировать всё подряд.
И да, атрибуты оптимизации над методами никто не отменял.
S>>>При этом, опять же, после инлайна кода у нас появляется статистика о том, какие именно запросы делаются в dataReader. И, соответственно, возможность ещё что-то проинлайнить, выбросив лишние ветки if вместе с косвенными вызовами.
V>>Оптимизации выполняются на уровне конкретного типа.
V>>Плохо представляю себе приложение, где в DataReader в любой точке приложения приходит одна и та же схема данных. ))
S>Ну так в том-то и дело: пока мы делаем косвенный вызов метода DataReader, в call target мы вынуждены обрабатывать общий случай. Ведь VMT для всех пользователей этого DataReader одинаковая.
S>А вот когда мы встроили тело DataReader.GetDate(x) в конкретное место вызова, то внезапно оказывается, что для данной конкретной копии кода исходного GetDate в конкретном if/switch чаще всего (== всегда) попадается ровно один вариант. Ведь конкретно в этом месте всегда используется одна и та же схема данных.
Угу.
В рекордсете из 1001 элемента первые 1000 будут прочитаны без оптимизации.
Затем будет выполнена дорогостоящая оптимизация и последний элемент прочитают оптимальным образом.
Утрирую, но суть понятна.
У тебя в "одном и том же месте" каждый раз будут разные экземпляры DataReader и потенциально хотя бы немного отличающиеся схемы, хотя бы в плане nullable-полей, особенно если со стороны базы позвали какой-нить union. И даже если схемы будут те же — это будут другие экземпляры схем, вот в чём прикол.
S>Вроде бы не такая сложная конструкция, чего тут не понять? Погуглите любую статью со словами speculative inlining.
Который в той же JS-машинке требует предварительной проверки соответствия устройства объекта ожидаемому и работает с некоторым ограничением вложенности иерархии такой проверки, т.е. в относительно простых случаях.
В случае DataReader в нынешнем виде не прокатит — слишком большая иерархия объектов, всю её проверять на соответствие текущему хот-спот-коду будет накладней получаемых от хот-спот плюшек.
Надо упрощать архитектуру объектов.
Делать таблицу диспетчеризации, как я показывал.
Тогда динамически (по индексу) будет обращение только к строке такой таблицы, а столбец известен статически.
Тогда проверка соответствия лейаута объекта будет сводиться к проверке только ссылок на метаинформацию столбцов рекодрсета, где эти столбцы ссылаются на одни и те же статические-заготовленные конвертеры.
Т.е. сравни — проверить только равенство ссылок в объекте верхнего уровня или рекурсивно пройтись по кучерявым объектам, проверяя равенство всех полей всех дочерних элементов.
Счётчик сложности в последнем случае скажет "извините, в другой раз".
V>>Для DataReader "типы" полей динамические, зависят от схемы принятого рекордсета.
V>>Тут и С++ ничего не сделает.
S>Про С++ ничего сказать не могу. Способен ли он выполнять PGO-оптимизацию уже PGO-оптимизированного кода? А для хотспота это, насколько я знаю, норма.
"Норма" там в простейших случаях.
Я разбирал примеры работы джавовского хот-спота — покрывает только тривиальнейшие случаи и уже даже про них трубят как о победе.
V>>Надо тупо писать эффективный код.
V>>И в то же самое время читабельный и поддерживаемый. ))
S>Ну, так эти две цели в известной мере друг другу противоречат.
Распространённая ошибка.
Я показал устройство таблицы конвертеров в сообщении, на которое уже давал ссылку.
Читабельность и поддерживаемость (то бишь расширяемость) прекрасная.
А в варианте с новыми указателями на ф-ии — еще и максимально-эффективная, т.е. даже, грубо, на асме или IL быстрее не сделаешь.
И хотспоту проще ввиду низкой вычислительной сложности, т.е. мог бы заинлайнить тела вызываемых по указателю конвертеров, т.е. убрать косвенный вызов.
ОК, про гипотетический хотспот пока спорить не буду, по крайней мере пока его нет в природе для .Net.
V>>Всю эту кодогенерацию, которую ты показывал для linq, можно было выполнить и для AOT.
S>Ну, это всего лишь небольшой маленький пример. Реальные задачи не исчерпываются linq выражениями.
V>>Просто тут нужно соотв. плагинное АПИ к АОТ.
V>>Т.е., AOT даёт тебе конкретные типы, т.к. в закрытой системе типов они известны (даже если заведомо абстрактны — это тоже известно), а "плагин" генерит код, который опять же компиляется AOT.
S>В теории — интересно. На практике — где посмотреть на такой AOT?
Нигде.
Даже существующий полноценный доступен только для iOS на основе mono (ХЗ какого он там качества).
А виндовые UWP-приложения на .Net Core UWP компиляются серверами магазина Windows под сетку устройств, тот код публично недоступен.
V>>2. В обычных компиллируемых как минимум бета-редуцируемых языках (шаблоны С++ в пример) через генерирование двух версий — для проверенного и непроверенного констрейна.
S>Не двух, а 2количество констреинтов.
1. Так и есть, порой шаблоны порождают сетку-произведение воплощённых реализаций.
2. Опять включается счётчик сложности в случае хот-спота.
V>>Даже в языках с зависимыми типами всё-равно в какой-то точке программы идёт проверка и ветвление/диспетчеризация кода.
V>>Так же и в твоём примере с SQL-сервером, для данной сущности берётся одна из готовых реализаций, соотв. констрейнам этой сущности.
S>Нет. Просто при построении плана учитывается текущая статистика и актуальные метаданные.
Ес-но.
И затем берется одна из уже готовых реализаций способа скана.
В этом суть.
S>Напомню: в прошлый раз дискуссия про "повторное использование компонентов плана выполнения запроса" кончилась тем, что вы тихо слились, как только дело дошло до кода.
Я помню обратное — ты так и не прошёл понимание про повторно-используемые "кубики" при построении плана запроса.
S>И в этот раз будет то же самое.
Псевдокод давался.
V>>То бишь, у тебя может быть уже сетка неких алгоритмов под разные сценарии. При изменении сценария ты подставляешь указатель на нужный алгоритм.
S>Ага. Размер этой сетки для баз данных быстро начинает превышать разумные пределы. Как, впрочем, и для любых комбинаторных задач.
Это для динамических вещей.
Для статических еще в момент компиляции известны наличествующие индексы.
Поэтому, комбинаторика будет только по различиям в статистике, где для многих типов индексов, которые я обозвал enum (разновидность справочных данных эдакого системного плана, т.е. которые жестко привязаны к версии приложения) — статистика известна на момент компиляции. То бишь, кол-во уникальных значений в индексе.
V>>Хотспот еще более хрупкий и непредсказуемый.
S> Пока что он демонстрирует впечатляющие результаты
Увы.
Чтобы он демонстрировал "впечатляющие результаты" в нашей области, например, ребята раскладывают объекты на составяющие ручками.
Т.е., вместо
class SomeClass { int a, b, c; };
SomeClass c1 = new SomeClass[42];
У них идёт:
int[] aFields = new int[42];
int[] bFields = new int[42];
int[] cFields = new int[42];
А в самых критических случаях и вовсе:
class SomeClassHelper {
const int FIELD_COUNT = 3;
const int FIELD_A_OFFSET = 0;
const int FIELD_B_OFFSET = 1;
const int FIELD_C_OFFSET = 2;
public void write(int[] array, int index, int a, int b, int c) {
index = index * FIELD_COUNT;
array[index + FIELD_A_OFFSET] = a;
...
}
int[] data = ...
SomeClassHelper.Write(data, index, a, b, c);
Смысл примерно должен быть понятен.
А если поля разных типов (по ширине разных), то всё еще забавнее происходит над массивом байт.
S>И вытягивает её как раз JIT и хотспот.
Вытягивают её ручками, помогая хот-споту.