Как стать автором
Обновить

Комментарии 46

В чем польза от этих примеров?
Все примеры, кроме может быть Linq — какие то бесполезные.
Особенно растроила структура с IDisposable — структуры и так непростые к применению, так надо было добавить ещё.
Про Linq вообще чисто на внимательность(=

Объяснение к первому примеру — неверное. Дело не в том, что в блоке finally происходит упаковка, а в том, что компилятор создает "защитную" копию переменной, поскольку переменные в блоке 'using' очень похожи на readonly-поля (ее нельзя переприсвоить) и компилятор пытается гарантировать "неизменяемость" состояния.


Посмотрите во что компилятор разворачивает код:


SDummy sDummy = default(SDummy);
        SDummy sDummy2 = sDummy;
        try
        {
            Console.WriteLine(sDummy.GetDispose());
        }
        finally
        {
            ((IDisposable)sDummy2).Dispose();
        }
        Console.WriteLine(sDummy.GetDispose());

И упаковки — не просиходит в этом коде вообще! И это очень, повторюсь, очень важно. Если бы она была, то енумераторы в коллекциях не были бы структурами (какой смысл делать их структурами, если блок 'using', который генерирует компилятор для foreach все равно приведет к упаковке?


Ну и главное, эти примеры нужно всегда давать в контексте: структуры — это оптимизация и к их примненеию нужно подходить соответственно — использовать тогда, когда профилирование показало их необхоидмость.


Потом нужно объяснять семантическую разницу между классами и структурами и говорить о том, что структуры должны быть неизменяемыми (должны быть readonly struct). Если структура изменяемая, то компилятор генерирует огромную кучу защитных компий, которые совершенно не очевидны никому в этом мире, кроме пары десятков человек. А эти копии могут а) негативно повлиять на производительность (тут идет напоминание, что структуры — это оптимизация) и б) вы можете очень легко отстрелить себе ногу, поскольку изменение состояние произойдет на компии.


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


Это очень advanced знания, которые нужны либо а) энтузиастам-задротам или б) людям, которые разрабатываеют очень высоконагруженные приложения на .net.

Да, и хочется добавить.

Вообще, меня очень смущает первый вопрос. Он показывает, что автор этого вопроса сам не совсем понимает, что происходит у дот-нетика под капотом.

Упаковка и защитные копии структур — это две очень сильно разные вещи. Упоковки приводят к засорению кучи и влияют на производительность засчет притормаживания сборки мусора (добиться которого в простом Ынтырпрайз приложении, кстати, очень сложно).

Защитные же копии — это другой зверь. Их и было много, а с появлением 'in' модфикаторов и ref-return-ов стало еще больше. И я бы рассчитывал, что в голове у автора подобных вопросов эти два кейса лежат на разных полках, поскольку они приводят к разным проблемам, по разному проявляются, по разному ищутся и по разному решаются.
А разве в случае «in» компилятор не просто проверяет попытки прямых изменений полей, из-за чего появляется возможность передавать структуры не по значению, а по ссылке? Тесты показывают хорошее ускорение в случае сложных структур, если бы создавались защитные копии — было бы наоборот.

Там не всё так просто. Да, вы можете передавать структуры по ссылке, но если у компилятора не будет 100% уверенности, что операция не приведёт к изменению структуры, то будет создана её копия, причём без всяких предупреждений.


Если вы не используете вложенные структуры, не используете дженерики, не вызываете у структуры методы, то передача по ссылке in будет оправдана. В остальных случаях нужно быть предельно аккуратным, чтобы не угробить производительность.


У меня уже было несколько случаев, когда компилятор не справлялся с анализом кода и делал копирование даже в совсем очевидных случаях, замедляя производительность. Этот анализ вообще достоин отдельного поста.

Ну генерики в структах вообще зло и должны быть запрещены из-за непредсказуемых аллокаций на ровном месте. Вложенные структуры — да, не использую, надо будет обновить тесты.

Идите нафик с подобными предложениями и не мешайте писать высокопроизводительный код. Генерики в сочетании со структурами хороши тем, что JIT-компилятор для каждой комбинации параметров генерирует свой код. Если что-то позволяет выстрелить в ногу, то это не означает, что от этого нужно отказываться. Я ещё, о ужас, иногда использую System.Runtime.CompilerServices.Unsafe.

Ансейф я и сам использую. Проблема как раз в неконтролируемых аллокациях памяти в довольно неожиданных местах:
jacksondunstan.com/articles/3453
jacksondunstan.com/articles/3468

Идите нафик с подобными предложениями и не мешайте писать высокопроизводительный код.

Надеюсь, что никогда не придется пользоваться подобным «высокопроизводительным» кодом.

Это проблема не с генерик-структурами, а с конкретной кривой реализацией JIT-компилятора или среды выполнения. Конкретно в том случае использовался Mono довольно старой версии.


Я много времени потратил на сравнение производительности в совершенно различных сценариях и не заметил абсолютно никаких проблем что в MS .NET, что в Mono последних версий.


Хотя не, немного вру: у меня есть пример кода, где в случае использования структуры T выражения new T() и default(T) оказывались неэквивалетными. Но там совсем непотребство творилось. Пост, что ли, запилить...

Это проблема не с генерик-структурами, а с конкретной кривой реализацией JIT-компилятора или среды выполнения.

«У меня все работает» ©. Игнорирование потенциально возможных проблем на разных рантаймах — это не решение. Раньше подобное приходилось слышать про mono.
не заметил абсолютно никаких проблем

Отличный аргумент. Но это не значит, что их там нет.
То есть вместо того, чтобы поправить баг в Mono или поспособствовать решению проблемы (завести тикет и подробно описать в нём баг), вы предлагаете запретить криво работающий в нём функционал, сделав плохо всем. Отличная идея!
Я предлагаю крайне осторожно использовать генерики для структур или вообще запретить их к использованию. Разумеется, в командах и административными мерами, а не на уровне языка. Пусть кто хочет, тот себе стреляет во все конечности.

Отлично. Запретите тогда тот же KeyValuePair<,>.

Штатная правильная реализация != «ухты, мы теперь можем и так!» По ссылкам выше как раз показаны особенности, обходя которые, можно использовать данный функционал. Другое дело — кто будет разбираться в этих тонкостях в общем случае? Работает и ладно.

И как же вы их обойдёте для KeyValuePair<,>, где его нельзя сконстрировать напрямую через поля, а можно только через конструктор?

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

Представьте себе, этот тип ещё и в пользовательском коде можно конструировать и использовать. И это далеко не единственный тип генерик-структуры, который используется. Разработчики ещё всякие ValueTuple, ValueTask выкатывают.
У меня специализация — мобильный геймдев, процессинг данных в рантайме без фризов на сборку мусора, т.е нулевые аллокации в основном цикле апдейта, причем на несравнимо более дохлом железе, чем десктоп. Кейсы с выделением памяти всегда выносятся на этап инициализации и не допускаются в процессинге (и не потому, что рантайм такое не позволяет). Где общая производительность и непрерывность по времени исполнения не важна — там допускаю использование всего, чего душа пожелает.
Нет, все сложнее. Полное описание можно найти здесь: The ‘in’-modifier and the readonly structs in C#.

Если коротко, то in-модификатор очень похож на readonly поля: такие параметры нельзя переприсвоить и для структур компилятор генерирует защитные копии для обращений к свофствам и методам, чтобы обеспечить неизменяемость. А вот эти защитные копии и будут ломать нам ноги, поскольку они не явные.

С доступом к полю на чтение проблем никаких нет, так как компилятор в этом случае не порождает защитной копии, в этом нет никакой необходимости. Другое дело — это вызов метода или обращение к свойству на чтение/запись для аргументов с модификатор 'in' и для readonly-полей в случае структур не объявленных как readonly struct, здесь компилятор вынужден создавать защитную копию, так как не знает, приведет ли вызов метода к изменению значения или нет. Для структур объявленных как readonly struct при вызове методов/свойств компилятор уже не будет создавать защитной копии, так как гарантируется, что вызов не приведет к изменениям.

Нет, не гарантируется. Пример:


struct NonReadonlyStruct
{
    public int f;

    public void SetField(int v) => f = v;
}

readonly struct ReadonlyStruct
{
    readonly NonReadonlyStruct s;
    readonly int c;

    // Тут будет ошибка компиляции
    public void SetField1(int v) => c = v;

    // А тут - копирование структуры
    public void SetField2(int v) => s.SetField(v);
}

Для readonly-структур именно что гарантируется, потому как такая структура не может содержать не readonly-поля, компилятор просто не даст объявить такую структуру. Поэтому в случае вызова метода у readonly-структуры C# не создает защитную копию.


В вашем же примере, в методе SetField вы вызываете метод SetField2 у не readonly-структуры, инстанс которого записан в readonly-поле, именно поэтому и создается защитная копия, потому что C# не знает, изменит вызов метода внутреннее состояние или нет. Но сам вызов SetField у ReadonlyStruct не приводит к созданию копии инстанса ReadonlyStruct .


Другими словами:


public void Test_0(in SomeReadonyStruct a, in SomeMutableStruct b) {
    // Защитная копия не создается
    a.SomeMethod();

    // Здесь C# создает защитную копию
    b.SomeMethod();
}

public void Test_1(SomeReadonyStruct a, SomeMutableStruct b) {
    // Защитная копия не создается
    a.SomeMethod();

    // Защитная копия не создается
    b.SomeMethod();
}

ЗЫ. Я в предыдущем комментарии оговорился на счет доступа к полю/свойству на запись для структур у readonly-полей и аргументов с модификатором 'in', так как C# запрещает их изменять, но, к сожалению, исправить возможности уже не было, ждал, когда комментарий модерацию пройдет.

Для readonly-структур именно что гарантируется, потому как такая структура не может содержать не readonly-поля, компилятор просто не даст объявить такую структуру.

Всё, что может гарантировать компилятор — это запретить прямое изменение readonly полей. А вызывать методы у readonly полей он не запрещает.


Но сам вызов SetField у ReadonlyStruct не приводит к созданию копии инстанса ReadonlyStruct

Тем не менее, защитная копия дочерней структуры создаётся. Это вам не const-методы в C++.

Тем не менее, защитная копия дочерней структуры создаётся.

Еще раз, защитная копия создается для инстанса, метод которого ты вызываешь, если это не readonly-структура, и инстанс которого записан в readonly-поле или это аргумент объявленный с модификатор 'in'.


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


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

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

Так в этом и заключается коварство in. Многие почему-то считают, что этого происходить не будет.

Так в этом и заключается коварство in. Многие почему-то считают, что этого происходить не будет.

'in' здесь совершенно не причем. Защитная копия при вызове s.SetField(v) из твоего примера будет создаваться в любом случае, что с 'in' что без 'in', или будь это даже локальной переменной. Как говорится, записал мутабельную структуру в readonly-поле, сам себе злобный буратино, будь готов к копиям при вызове его методов.


Для информации: C# не делает межпроцедурный анализ, и решать делать ли защитную копию или нет, решает в точке вызова метода структуры. Если программист указал, что память, занятая моей структурой должна остаться неизменной (readonly-поле, аргумент с модификатором 'in'), то C# это и будет обеспечивать. Дальше точки вызова он не заглядывает. Программист там хоть диск пусть форматирует.


И readonly-поле и аргумент с модификатором 'in' как раз и служат для обеспечения гарантии неизменности памяти, ничего более, и штатными средствами, не обманывая компилятор, никак не удастся изменить память, занятую этой структурой. Причем, до появления readonly struct не было никакой возможности сказать, что не стоит делать побитную копию, перед тем как вызвать метод структуры, потому что раньше хоть по факту структура могла быть иммутабельной, для компилятора она все равно оставалась мутабельной, но теперь, в случае readonly struct, компилятор знает, что структура иммутабельная, и вызов метода не приведет к изменению памяти занятую этой структурой.


Правило ведь простое как 5 копеек: вызов метода мутабельной структуры в readonly-семантике приводит к созданию защитной копий памяти, занимаемую этой самой структурой для гарантии ее неизменности.

Всё, что может гарантировать компилятор — это запретить прямое изменение readonly полей. А вызывать методы у readonly полей он не запрещает.

А кто говорил, что запрещено вызывать методы?

Вообще, меня очень смущает первый вопрос. Он показывает, что автор этого вопроса сам не совсем понимает, что происходит у дот-нетика под капотом.

Это нормально.
Я когда придумываю задания, часто ошибаюсь с ответом. Тут главное, проверить ответ. И поблагодарить человека, если он дал более полное объяснение.
НЛО прилетело и опубликовало эту надпись здесь
Тривиальный вопрос "можно ли получить доступ к приватному полю" хакается 5 способами. "Чем отличается абстрактный класс от интерфейса" может обернуться обстоятельным рассказом про особенности сериализаторов. Однажды мне заявили, что в интерфейсе можно объявить константу — язык С# такого не позволяет, но если сгенерировать интерфейс не шарпом, .NET его подцепит…

И задавая себе любой нетривиальный вопрос, ты понимаешь: «по идее, X работает так, но если задаться целью...». На базовом уровне один ответ, копнешь глубже — уже другой.
SergeyT Спасибо за комментарий и полезные замечания.
Действительно, в объяснении первого примера допущена ошибка.
̶И̶ ̶м̶ы̶ ̶у̶ж̶е̶ ̶у̶в̶о̶л̶и̶л̶и̶ ̶е̶г̶о̶ ̶а̶в̶т̶о̶р̶а̶ :)

Вы правы. Действительно, компилятор разворачивает блок using для значимых типов в другую конструкцию, нарушая спецификацию языка C# ради оптимизации (согласно комментариям Э. Липперта https://stackoverflow.com/questions/2412981/if-my-struct-implements-idisposable-will-it-be-boxed-when-used-in-a-using-statem).
Таким образом, упаковки (boxing) не происходит, однако создается скрытая копия переменной.
Подробности с описанием очень похожего примера можно найти у Эрика Липперта (https://ericlippert.com/2011/03/14/to-box-or-not-to-box/)
Общий вывод такой, что изменяемые значимые типы (mutable value types) — это зло, которое лучше избегать.

Соответствующие изменения в статью постараемся внести в ближайшее время.

Мы особенно рады, что этот пост не оставил равнодушным автора одной из наиболее популярных книг нашей библиотеки (https://habr.com/ru/company/veeam/blog/417691/).
Вы правы. Действительно, компилятор разворачивает блок using для значимых типов в другую конструкцию, нарушая спецификацию языка C# ради оптимизации


И снова я с этим не совсем согласен. Эрик, ведь, — жук, да он пишет вот что: «You’d be perfectly justified in thinking that there is a boxing performed in the finally because that’s what the spec says.». Но ведь он пишет лишь то, что «да, в спеке есть пример, который говорит, что using блок выворачивается в каст», но спека не говорит, что каст там должен быть.

Спека (вот здесь, например) вообще хитро написана, там есть вот что:

An implementation is permitted to implement a given using-statement differently, e.g. for performance reasons, as long as the behavior is consistent with the above expansion.


Так что никто никого не нарушает;), не стоит спешить с выводами.
Эрик сам признается, что это техническое нарушение:
«This is technically a violation of the C# specification. The spec states that the finally block should have the semantics of ((IDisposable)resource).Dispose(); which is clearly a boxing conversion. We do not actually generate the boxing conversion»

С чем трудно не согласиться, читая спеку C# о using statement:

When ResourceType is a non-nullable value type, the expansion is

{
    ResourceType resource = expression;
    try {
        statement;
    }
    finally {
        ((IDisposable)resource).Dispose();
    }
}


Но спека написана ветиевато. И, видимо, неспроста :)

Комментарий Эрика датирован 2011-м, и после этого, скорее всего, спека была изменена, чтобы убрать эту неоднозначность. Я же привел кусок спеки:

An implementation is permitted to implement a given using-statement differently, e.g. for performance reasons, as long as the behavior is consistent with the above expansion.


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

Так что *сегодня* никакого нарушения спеки нет. Не стоит вводить себя и читателей в заблуждение;)
Посмотрите во что компилятор разворачивает код

Если взять развернутый код и получить из него IL, то какая-то упаковка в блоке finally есть. Но при этом ее нет, если получить IL из оригинального кода.
Не могли бы вы пояснить, почему не происходит упаковки копии sDummy2 в случае оригинального кода? И почему развернутый и оригинальный код дают разный IL?
Проблема в том, что нет никакого смысла в том, чтобы «взять развернутый код и получить из него IL». Есть IL-код, генерируемый компилятором в котором упаоквки нет, а есть 'constrained' вызов метода Dispose.

«Развернутый код» же — это лишь пример (псевдокод, по сути) того, что делает компилятор. На самом же деле, поведение несколько иное — более эффективное.

Добавил бы, вызов причем идет через callvirt, предваренный опкодом constrained, так при этом, в случае если метод определен в struct, то упаковки не будет, а если метод не определен(вернее переопределен), т.е. это методы ValueType — ToString, Equals, то будет неявная упаковка и вызов виртуального метода.

Код
using System;
struct First{
    public override string ToString()
    {
        return "It is First";
    }
}
struct Second {
}

public class C {
    public void M() {
        var f=new First();
        var s=new Second();
        
        Console.WriteLine(f.ToString());
        Console.WriteLine(s.ToString());
    }
}


в первом случае упаковки не будет вообще, а вот во втором случае, даже отсутствие явного опкода box, будет неявная упаковка (это видно по коду после JIT, вызов конструктора, копирование и вызов виртуального метода )

А во втором примере если поле вынести в другой класс, то последовательность так же отработает?

Тоже недавно искал разные забавные штуки, например:
1) Как поменять переменные в замыкании
2) Как подменить сам метод у замыкания
3) Каст объектов через Unsafe

Если кому интересно:
gist.github.com/dbr176/5e8768ead9b9c8311b24c56d694876df

Зачем же вы так дезинформируете общественность объяснением первого примера? Сами же даёте ссылку на объяснения Эрика Липперта, где он последовательно объясняет, что никакого боксинга нет. А в статье говорите "потому что боксинг". А имеет место быть "embedded statement", когда значение структуры копируется/захватывается блоком using и манипуляции над копией не видны снаружи этого юзинга. Это огромная разница с боксингом.


Сам постоянно использую disposable struct (не mutable, а именно disposable), потому что это удобное средство простого профилирования участков кода аля


using (new Perf("Считаем интеграл методом №7"))
{
    // some code
}

....

public readonly struct Perf
{
    private readonly string _text;
    private readonly long _startTime;
    // etc

    public Perf(string text)
    {
        _text = text;
        _startTime = ... // засечь текущий момент времени и сохранить в поле
    }

    public void Dispose()
    {
        // засечь текущий момент времени, вычислить дельту, сбросить дельту в лог вместе с текстовкой _text (консоль/etc.)
    }
}

И эта конструкция помогает естественным образом видеть тот кусок кода, для которого производится замер — визуально все в блок using завернуто красиво. При этом она не несет никаких накладных расходов на выделение памяти в куче.


Даже пришлось перепроверять это после прочтения, напугали. Но все хорошо — 0 bytes allocated )).

sungam3r в разборе первого примера у нас действительно была ошибка. Теперь исправились.
Спасибо, что тоже заметили неточность. Зато теперь все знают про disposable struct ))
Ждал 2 дня одобрения комментария. Впервые решил отписать что-нибудь.
Касательно примера 4 — объяснение внутренней работы хорошее, но правильный вариант все таки такой(вдруг кто-то из новичков будет читать данную статью):
int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 };

int summResult = dataArray.Sum();

Console.WriteLine(summResult);

Также расширение .Sum() позволяет задать селектор, если нужно(например .Sum(item => item.Value1)). Тип будет выведен из типа перечисления(в данном случае int[] — это
IEnumerable<int>
и будет выведен тип int) или типа, который получает селектор.
мы решили приоткрыть немного внутренней кухни и предлагаем вашему вниманию несколько примеров на C#, которые мы разбираем с нашими студентами.
Не забивайте студентам голову мусором, учите писать простой и понятный код, в большистве случаев не сложно понять что делает программа, бывает очень сложно понять почему она делает именно это.
В глубинах языка разобраться значительно проще: доступны спеки, отладчики, дампы ит.д.
Куда сложнее писать концептуально целостный софт, где выбранные абстрации использются по назначению и в точности реализуют заданные кейсы из бизнеса.
Потом получается, что на собеседовании человек знает за сколько тактов GC собирает кучу, а код на выходе по прежнему плохо читаемый и не поддерживаемый.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий