Так в том и дело, что собрав код в Linq.Expression, его можно собрать в делегат одним методом. А вот этот делегат будет лишь наносекунды медленнее чем если написать тот же код вручную
Во всех методах ожидается структа-делегат реализующая IValueDelegate.
По идее, чтобы выжать максимально скорости работы, нужно написать структуру, реализовав интерфейс и в методе Invoke написать ваш делегат.
Но так как это не слишком интересно никому, я предварительно сделал PureValueDelegate и CapturingValueDelegate, которые работают без аллокаций, но используют обычный дотнетовский делегат.
Тем не менее, генерация под каждый тип могла бы позволить девиртуализировать виртуальные типы и методы. То есть все равно есть цена за то, что не специализируем на каждый тип.
Еще могли бы рассказать как получили бэкграунд в низкоуровневой разработке? Помогло ли образование (наше или зарубежное?) или же достигали самостоятельно? Возможно порекомендуете какие-либо книги?
Самостоятельно, но не без помощи других людей и проектов. В основном это
C# сервер в дискорде - тут куча крутых людей, очень много чего низкоуровнего можно узнать
Расскажите как у вас хватает времени на это все, откуда берете мотивацию, энергию?
Позитивный фидбек пользователей и читателей дает мотивацию.
Является ли это вашей личной инициативой или кто-то курирует?
Личная. Никто не финансирует.( Но надо сказать, что многие проекты не так-то и много заняли времени. Например, проект, про который эта статья, я сделал наверное дня за два. AsmToDelegate — там моей работы вовсе немного, по сути все, что я делаю, это пишу закодированный машинный код от Iced-а в исполняемую память и возвращаю делегат.
Хотя есть конечно и большие проекты, которые уже не первый год делаю, например этот
Это хороший вопрос, но я не изучал. Вообще, code bloating — это причина почему, например, CLR не генерирует типы и методы на каждый тип, а только на разные value type. То есть такая проблема явно есть. Но какое именно потребление — наверное можно попробовать померить через EventListener, отследив момент когда JIT специализирует тип
Вовсе нет. Стандартный LINQ работает вполне "обычно", без Linq.Expression. Вот здесь можно посмотреть исходники LINQ-а.
Но для работы со всякими базами данных используется IQueryable, и вот там используются Linq.Expression, чтобы переделать лямбду на сишарпе в SQL-запрос (или как это в БД работает). Но это уже совсем другая история.
А еще знакомый пилит компилятор LINQ в обычные циклы/условия и т. д., вот там он использует компиляцию Linq.Expression.
Неужели компилятор настолько глупый, что не может заинлайнить все эти виртуальные вызовы? Он же видит всю Linq-цепочку (в вашем примере).
Пока да. Чтобы девиртуализировать такую длинную цепочку, ему придется буквально выполнить весь код и убедиться, что у нас всегда создаются одни и те же типы, и заинлайнить огромное количество кода. Для подобных вещей это бессмысленно и стоит очень много времени джита (а он, конечно, должен работать очень быстро).
Все заинлайненные лямбды должен же иметь нулевой размер, они не используют состояние
В моем примере я использую тип PureValueDelegate. Это такой value delegate, только для ленивых, потому что в нем я использую обычный Func :). В идеале туда можно передавать настоящий функтор вручную реализуя IValueDelegate, но кажется это никому из пользователей LINQ-а неинтересно такой ужас городить. Вот. То есть это как минимум 8 байт на каждый такой делегат.
а размер конечной структуры по идее должен быть максимальным размером всех этих переходных структур?
Ну, размер каждой следующей - сумма ее полей, то есть "личных" полей и предыдущего енумератора.
Плюс еще инлайнер джита мог уже выйти из бюджета и перестать инлайнить некоторые методы - тогда у нас еще и копирование будет.
Дальше теория - львиная доля из 4 и 8 мс это выделение памяти
На самом деле нет, память, как вы же сами сказали, стоит очень мало. Правда все-таки побольше, чем на стеке, но все равно немного.
А вот виртуальные вызовы на каждом шагу - вот это я думаю и занимает все время. Если сделать цепочку из N операций в LINQ, то каждый Move.Next это будет N виртуальных вызовов, то есть хождений по случайным местам памяти.
А тут нет виртуальных вызовов, и многие вызовы заинлайнены.
Сработает, конечно. 1 час / 12 минут будет 0.08 час/мин :).
На самом деле еще нужен метод упрощения, который упрощал бы, например, деление единиц с одной базы до просто скалярного значения. Пока что при любой операции само выражение единицы измерения будет только нарастать.
Я же говорю - нет. В твоем коде + все равно НЕ дженерик оператор. Интерфейс никак ни на что не влияет. Интерфейс нужен для того, чтобы уметь ограничить тип до того, у которого есть оператор. А мне нужен дженерик оператор. А его нет. Совсем нет.
Не, я не физик
Так в том и дело, что собрав код в Linq.Expression, его можно собрать в делегат одним методом. А вот этот делегат будет лишь наносекунды медленнее чем если написать тот же код вручную
Да генерит вроде... не мерил, если честно. Точнее мерил, вот здесь, но не сравнивал с LINQ.
Неа. Там отдельный type argument на них, и для них тип специализируется
Во всех методах ожидается структа-делегат реализующая IValueDelegate.
По идее, чтобы выжать максимально скорости работы, нужно написать структуру, реализовав интерфейс и в методе Invoke написать ваш делегат.
Но так как это не слишком интересно никому, я предварительно сделал PureValueDelegate и CapturingValueDelegate, которые работают без аллокаций, но используют обычный дотнетовский делегат.
Хорошо. Сделал бенчмарк для сравнения простого Select + Where для RefLinq против классического.
Собственно, примерное отношение 1:2 по времени так и сохраняется, как и предполагалось ;).
Исходники бенчмарка тут.
cc @Mingun , @andreyverbin
Тем не менее, генерация под каждый тип могла бы позволить девиртуализировать виртуальные типы и методы. То есть все равно есть цена за то, что не специализируем на каждый тип.
Самостоятельно, но не без помощи других людей и проектов. В основном это
C# сервер в дискорде - тут куча крутых людей, очень много чего низкоуровнего можно узнать
Шарплаб
Ну и несколько книжек:
CLR via C# 4-th edition
Pro .NET Memory Management
Pro .NET Benchmarking
https://en.wikibooks.org/wiki/X86_Assembly
А ведь на том сишарп сервере есть люди, которые в этом шарят на порядки больше меня! Вот это реально клад
Позитивный фидбек пользователей и читателей дает мотивацию.
Личная. Никто не финансирует.( Но надо сказать, что многие проекты не так-то и много заняли времени. Например, проект, про который эта статья, я сделал наверное дня за два. AsmToDelegate — там моей работы вовсе немного, по сути все, что я делаю, это пишу закодированный машинный код от Iced-а в исполняемую память и возвращаю делегат.
Хотя есть конечно и большие проекты, которые уже не первый год делаю, например этот
Это хороший вопрос, но я не изучал. Вообще, code bloating — это причина почему, например, CLR не генерирует типы и методы на каждый тип, а только на разные value type. То есть такая проблема явно есть. Но какое именно потребление — наверное можно попробовать померить через EventListener, отследив момент когда JIT специализирует тип
Вовсе нет. Стандартный LINQ работает вполне "обычно", без Linq.Expression. Вот здесь можно посмотреть исходники LINQ-а.
Но для работы со всякими базами данных используется IQueryable, и вот там используются Linq.Expression, чтобы переделать лямбду на сишарпе в SQL-запрос (или как это в БД работает). Но это уже совсем другая история.
А еще знакомый пилит компилятор LINQ в обычные циклы/условия и т. д., вот там он использует компиляцию Linq.Expression.
Пока да. Чтобы девиртуализировать такую длинную цепочку, ему придется буквально выполнить весь код и убедиться, что у нас всегда создаются одни и те же типы, и заинлайнить огромное количество кода. Для подобных вещей это бессмысленно и стоит очень много времени джита (а он, конечно, должен работать очень быстро).
В моем примере я использую тип PureValueDelegate. Это такой value delegate, только для ленивых, потому что в нем я использую обычный Func :). В идеале туда можно передавать настоящий функтор вручную реализуя IValueDelegate, но кажется это никому из пользователей LINQ-а неинтересно такой ужас городить. Вот. То есть это как минимум 8 байт на каждый такой делегат.
Ну, размер каждой следующей - сумма ее полей, то есть "личных" полей и предыдущего енумератора.
Плюс еще инлайнер джита мог уже выйти из бюджета и перестать инлайнить некоторые методы - тогда у нас еще и копирование будет.
А я все-таки не понимаю, зачем право одобрять/не одобрять комменты дали авторам. Мне кажется, это не их задача
На самом деле нет, память, как вы же сами сказали, стоит очень мало. Правда все-таки побольше, чем на стеке, но все равно немного.
А вот виртуальные вызовы на каждом шагу - вот это я думаю и занимает все время. Если сделать цепочку из N операций в LINQ, то каждый Move.Next это будет N виртуальных вызовов, то есть хождений по случайным местам памяти.
А тут нет виртуальных вызовов, и многие вызовы заинлайнены.
Это перевод моей же статьи на медиуме.
Вопросы, фидбек приветствуются!
Не в проце там дело, как оказалось, а в операционке
Честно говоря, превью статьи, встречающая меня с мутирующей лямбдой в linq, довольно оттвергает от чтения. Надеюсь, кому-то все-таки будет полезно...
Сработает, конечно. 1 час / 12 минут будет 0.08 час/мин :).
На самом деле еще нужен метод упрощения, который упрощал бы, например, деление единиц с одной базы до просто скалярного значения. Пока что при любой операции само выражение единицы измерения будет только нарастать.
Я же говорю - нет. В твоем коде + все равно НЕ дженерик оператор. Интерфейс никак ни на что не влияет. Интерфейс нужен для того, чтобы уметь ограничить тип до того, у которого есть оператор. А мне нужен дженерик оператор. А его нет. Совсем нет.
Это приватная реализация, или как она там называется, а не дженерик оператор.