Pull to refresh

Comments 28

Меня посетила интересная мысль - а откуда берётся выигрыш в производительности? 1Кб в куче выделяет стандартный LINQ, видимо столько же, но на стеке выделяет ваша версия. Выделение в куче стоит почти столько же, сколько и на стеке. Выделенную в куче память GC, скорее всего, никогда не увидит, то есть стоимость очистки памяти тоже равна стеку.

Дальше теория - львиная доля из 4 и 8 мс это выделение памяти. Если увеличить размер коллекции, то это время станет незначительно на общем фоне и разница станет незаметной.

Дальше теория - львиная доля из 4 и 8 мс это выделение памяти

На самом деле нет, память, как вы же сами сказали, стоит очень мало. Правда все-таки побольше, чем на стеке, но все равно немного.

А вот виртуальные вызовы на каждом шагу - вот это я думаю и занимает все время. Если сделать цепочку из N операций в LINQ, то каждый Move.Next это будет N виртуальных вызовов, то есть хождений по случайным местам памяти.

А тут нет виртуальных вызовов, и многие вызовы заинлайнены.

Неужели компилятор настолько глупый, что не может заинлайнить все эти виртуальные вызовы? Он же видит всю Linq-цепочку (в вашем примере).


Также непонятно, откуда такой дикий оверхед по памяти в типе? Все заинлайненные лямбды должен же иметь нулевой размер, они не используют состояние, а размер конечной структуры по идее должен быть максимальным размером всех этих переходных структур? Они же все в одном месте памяти находятся, а после инлайнинга так вообще должна остаться только пара ссылок на источник данных и конечную функцию преобразования.

Неужели компилятор настолько глупый, что не может заинлайнить все эти виртуальные вызовы? Он же видит всю Linq-цепочку (в вашем примере).

Пока да. Чтобы девиртуализировать такую длинную цепочку, ему придется буквально выполнить весь код и убедиться, что у нас всегда создаются одни и те же типы, и заинлайнить огромное количество кода. Для подобных вещей это бессмысленно и стоит очень много времени джита (а он, конечно, должен работать очень быстро).

Все заинлайненные лямбды должен же иметь нулевой размер, они не используют состояние

В моем примере я использую тип PureValueDelegate. Это такой value delegate, только для ленивых, потому что в нем я использую обычный Func :). В идеале туда можно передавать настоящий функтор вручную реализуя IValueDelegate, но кажется это никому из пользователей LINQ-а неинтересно такой ужас городить. Вот. То есть это как минимум 8 байт на каждый такой делегат.

а размер конечной структуры по идее должен быть максимальным размером всех этих переходных структур?

Ну, размер каждой следующей - сумма ее полей, то есть "личных" полей и предыдущего енумератора.

Плюс еще инлайнер джита мог уже выйти из бюджета и перестать инлайнить некоторые методы - тогда у нас еще и копирование будет.

В идеале туда можно передавать настоящий функтор вручную реализуя
IValueDelegate, но кажется это никому из пользователей LINQ-а
неинтересно такой ужас городить.

В крайнем случае можно подумать в сторону кодогенерации в рантайме и избавиться от косвенного вызова делегата, ведь Func — это Method + Target. Заводится словарик, и для каждого метода создаётся своя структура-обёртка, но с прямым вызовом.

Это значительно увеличит время создания объекта, но в случае больших коллекций даже выигрыш. Например, я такое проворачивал для обработки изображений, и оно работало весьма неплохо.

А в чем смысл этого PureValueDelegate?

Во всех методах ожидается структа-делегат реализующая IValueDelegate.


По идее, чтобы выжать максимально скорости работы, нужно написать структуру, реализовав интерфейс и в методе Invoke написать ваш делегат.


Но так как это не слишком интересно никому, я предварительно сделал PureValueDelegate и CapturingValueDelegate, которые работают без аллокаций, но используют обычный дотнетовский делегат.

И еще вопрос, у вас структуры реализовывают интерфейсы, а боксинга и callvirt не будет?

Неа. Там отдельный type argument на них, и для них тип специализируется

Лучше приложите результат сравнения в BenchmarkDotNet для входых массивов размера 10, 100, 1000.

Сразу будет видно, сколько времени отъедают вызовы.

Хорошо. Сделал бенчмарк для сравнения простого Select + Where для RefLinq против классического.

Собственно, примерное отношение 1:2 по времени так и сохраняется, как и предполагалось ;).

Исходники бенчмарка тут.

cc @Mingun , @andreyverbin

Блин, есть же компилируемые деревья, они для этого созданы, ускорить стандартный Linq

Вовсе нет. Стандартный LINQ работает вполне "обычно", без Linq.Expression. Вот здесь можно посмотреть исходники LINQ-а.


Но для работы со всякими базами данных используется IQueryable, и вот там используются Linq.Expression, чтобы переделать лямбду на сишарпе в SQL-запрос (или как это в БД работает). Но это уже совсем другая история.

А еще знакомый пилит компилятор LINQ в обычные циклы/условия и т. д., вот там он использует компиляцию Linq.Expression.

На сколько я помню, Linq.Expression Маленько стукают по производительности, поэтому мои любители оптимизации рекомендовли использовать его только на стадии отладки, чтобы получать говорящий лог, а на бое отключать по ключу компиляции заменяя на обычный делегат.

Деталей я не знаю, но на меня они чутка поругивались за то что я в своей реализации реактивности их много куда напихал.

Так в том и дело, что собрав код в Linq.Expression, его можно собрать в делегат одним методом. А вот этот делегат будет лишь наносекунды медленнее чем если написать тот же код вручную

Если я правильно помню, то для generic struct CLR будет генерировать отдельный сконструированный (закрытый) тип с реализацией. Возникает вопрос, как приведённый подход повлияет на потребление памяти процессом в целом?

Это хороший вопрос, но я не изучал. Вообще, code bloating — это причина почему, например, CLR не генерирует типы и методы на каждый тип, а только на разные value type. То есть такая проблема явно есть. Но какое именно потребление — наверное можно попробовать померить через EventListener, отследив момент когда JIT специализирует тип

CLR не генерирует типы и методы на каждый тип, а только на разные value type.

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

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

JIT-компилятор в редких случаях всё-таки делает подставновку обобщённого типа даже для ссылочных типов.

Благодарю, очень интересно. Так же посмотрел ваше репо и был весьма удивлен - неужели один человек на такое способен? К примеру, скачал ваш проект AsmToDelegate на основе icedland/iced - весьма.

Расскажите как у вас хватает времени на это все, откуда берете мотивацию, энергию? Является ли это вашей личной инициативой или кто-то курирует? Так же является ли это вашей основной деятельностью и удалось ли найти финансирование, чтобы этим заниматься на постоянной основе?

Расскажите как у вас хватает времени на это все, откуда берете мотивацию, энергию?

Позитивный фидбек пользователей и читателей дает мотивацию.


Является ли это вашей личной инициативой или кто-то курирует?

Личная. Никто не финансирует.( Но надо сказать, что многие проекты не так-то и много заняли времени. Например, проект, про который эта статья, я сделал наверное дня за два. AsmToDelegate — там моей работы вовсе немного, по сути все, что я делаю, это пишу закодированный машинный код от Iced-а в исполняемую память и возвращаю делегат.


Хотя есть конечно и большие проекты, которые уже не первый год делаю, например этот

Еще могли бы рассказать как получили бэкграунд в низкоуровневой разработке? Помогло ли образование (наше или зарубежное?) или же достигали самостоятельно? Возможно порекомендуете какие-либо книги?

Вот реально редко встретишь людей из наших, которые так глубоко копают.

по сути все, что я делаю, это пишу закодированный машинный код от Iced-а в исполняемую память и возвращаю делегат

Да, это понятно. Но могу оценить ваш уровень компетентности, так как сам уже больше 10 лет занимаюсь разработкой. Работы не много - можно сказать один удар молотком. Но вот знать куда ударить - дорогого стоит.

Еще могли бы рассказать как получили бэкграунд в низкоуровневой разработке? Помогло ли образование (наше или зарубежное?) или же достигали самостоятельно? Возможно порекомендуете какие-либо книги?

Самостоятельно, но не без помощи других людей и проектов. В основном это

  1. C# сервер в дискорде - тут куча крутых людей, очень много чего низкоуровнего можно узнать

  2. Шарплаб

  3. Ну и несколько книжек:

    1. CLR via C# 4-th edition

    2. Pro .NET Memory Management

    3. Pro .NET Benchmarking

    4. https://en.wikibooks.org/wiki/X86_Assembly

Вот реально редко встретишь людей из наших, которые так глубоко копают.

А ведь на том сишарп сервере есть люди, которые в этом шарят на порядки больше меня! Вот это реально клад

А почему linq в F# почти не генерит мусор и работает быстро?

Чувак, прикольно! Так прикольно, что захотелось самому воспроизвести ручками из любви к красивым решениям. :) Статью в закладки, обязательно воспользую при удобной возможности.
Sign up to leave a comment.

Articles