[Из песочницы] Интересные заметки по C# и CLR
Изучая язык программирования C#, я сталкивался с особенностями как самого языка, так и его средой исполнения, *некоторые из которых, с позволения сказать, «широко известны в узких кругах». Собирая таковые день за днем в своей копилке, что бы когда-нибудь повторить, чего честно сказать еще ни разу не делал до этого момента, пришла идея поделиться ими.
Эти заметки не сделают ваш код красивее, быстрее и надежнее, для этого есть Стив Макконнелл. Но они определенно внесут свой вклад в ваш образ мышления и понимание происходящего.Что-то из приведенного ниже покажется слишком простым, другое, наоборот, сложным и не нужным, но куда без этого.
Итак, начинаем:
1) Расположение объектов и экземпляров в динамической памятиОбъекты содержат в себе статические поля и все методы. Экземпляры содержат только не статические поля. Это значит, что методы не дублируются в каждом экземпляре, и здесь применяется паттерн Flyweight.2) Передача параметров в методы Структура передает свою копию в метод, Класс передает копию своей ссылки. А вот когда мы используем ключевое слово REF — структура передает указатель на себя, а класс передает свою исходную ссылку.3) Подготовить код до выполнения В CLR есть блок CER, который говорит JIT — «подготовь код до выполнения, так что когда в нем возникнет необходимость, все будет под рукой». Для этого подключаем пространства имен System.Runtime.CompilerServices и RuntimeHelpers.PrepareConstrainedRegions.4) Регулярные выражения Regex можно создать с опцией Compiled — это генерация выражения в IL-код. Он значительно быстрее обычного, но первый запуск будет медленным.5) Массивы Одномерные массивы в IL представлены вектором, они работают быстрее многомерных. Массивы одномерных массивов используют векторы.6) Коллекции Пользовательские коллекции лучше наследовать от ICollection, реализация IEnumerable получается бесплатно. Но нет индекса (очень индивидуально).7) Расширяющие методы Если имя расширяющего метода вступает в конфликт с именем метода типа, то можно использовать полное имя расширяющего метода, и тип передать аргументом. StaticClass.ExtesionMethod (type); 8) LINQLINQ запросы выполняются сразу если в них используются слова как: orderby… by, group join, toList и т.д. Но если коллекция бесконечна, то запрос ни когда не завершится.9) Блок синхронизации У структурных типов и примитивных (byte, int, long…) нет блока синхронизации, который присутствует у объектов в управляемой куче на ряду с ссылкой. Поэтому не будет работать конструкция Monitor.() или Lock ().10) Интерфейсы Если в C# перед именем метода указано имя интерфейса, в котором определен этот метод (IDisposable.Dispose), то вы создаете явную реализацию интерфейсного метода (Explicit Interface Method Implementation, EIMI). При явной реализации интерфейсного метода в C# нельзя указывать уровень доступа (открытый или закрытый). Однако когда компилятор создает метаданные для метода, он назначает ему закрытый уровень доступа (private), что запрещает любому коду использовать экземпляр класса простым вызовом интерфейсного метода. Единственный способ вызвать интерфейсный метод — это обратиться через переменную этого интерфейсного типа.Без EIMI не обойтись (например, при реализации двух интерфейсных методов с одинаковыми именами и сигнатурами).
11) Нет в C#, но поддерживается IL Статические поля в интерфейсах, методы отличающиеся только возвращаемым значением и многое другое.12) Сериализация При сериализации графа объектов некоторые типы могут оказаться сериализуемыми, а некоторые — нет. По причинам, связанным с производительностью, модуль форматирования перед сериализацией не проверяет возможность этой операции для всех объектов. А значит, может возникнуть ситуация, когда некоторые объекты окажутся сериализованными в поток до появления исключения SerializationException. В результате в потоке ввода-вывода оказываются поврежденные данные. Этого можно избежать, например, сериализуя объекты сначала в MemoryStream.В C# внутри типов, помеченных атрибутом [Serializable], не стоит определять автоматически реализуемые свойства. Дело в том, что имена полей, генерируемые компилятором, могут меняться после каждой следующей компиляции, что сделает невозможной десериализацию экземпляров типа.
13) Константы Константы помещаются в метаданные сборки, поэтому если были изменения, нужно перекомпилировать все использующие ее сборки. Т.к. DLL с константой может даже не загружаться.Лучше использовать static readonly задавая значения в конструкторе, она постоянно загружается в использующих ее сборках, и выдает актуальное значение.14) Делегаты GetInvocationList — возвращает цепочку делегатов, можно вызывать любые, отлавливать исключения, получать все возвращаемые значения, а не только последнее.15) Сравнение строк В Microsoft Windows сравнение строк в верхнем регистре оптимизировано. *StringComparison.OrdinalIgnoreCase, на самом деле, переводит Char в верхний регистр. ToUpperInvariant. Используем string.compare (). Windows по умолчанию использует UTF-16 кодировку.16) Оптимизация для множества строк Если в приложении строки сравниваются часто методом порядкового сравнения с учетом регистра или если в приложении ожидается появление множества одинаковых строковых объектов, то для повышения производительности надо применить поддерживаемый CLR механизм интернирования строк (string interning).При инициализации CLR создает внутреннюю хеш-таблицу, в которой ключами являются строки, а значениями — ссылки на строковые объекты в управляемой куче.
17) Безопасные строки
При создании объекта SecureString его код выделяет блок неуправляемой памяти, которая содержит массив символов. Уборщику мусора об этой неуправляемой памяти ничего не известно. Очищать эту память нужно вручную.18) Безопасность
Управляемые сборки всегда используют DEP и ASLR.19) Проектирование методов
Объявляя тип параметров метода, нужно по возможности указывать «минимальные» типы, предпочитая интерфейсы базовым классам. Например, при написании метода, работающего с набором элементов, лучше всего объявить параметр метода, используя интерфейс IEnumerable.
public void ManipulateItems
23) IS и AS
IS — В этом коде CLR проверяет объект дважды:
if (obj is Person) { Person p = (Person) obj; }
AS — В этом случае CLR проверяет совместимость obj с типом Person только один раз:
Person p1 = obj as Person; if (p1!= null) { … }
24) Проверяем хватит ли памяти перед выполнением
Создание экземпляра класса MemoryFailPoint проверяет, достаточно ли памяти перед началом действия или вбрасывает исключение. Однако, учтите, что физически память еще не выделялась, и этот класс не может гарантировать, что алгоритм получит необходимую память. Но его использование определенно поможет сделать приложение более надежным.25) Немного про Null
Чтобы использовать null совместимый Int32 можно написать:
Nullable
31) Методы расширения public static class StringBuilderExtensions { public static Int32 IndexOf (this StringBuilder sb, Char char) { … } } Позволяют вам определить статический метод, который вызывается по средством синтаксиса экземплярного метода. Например, можно определить собственный метод IndexOf для StringBuilder. Сначала компилятор проверит класс StringBuilder или все его базовые классы на наличие метода IndexOf с нужными параметрами, если он не найдет такого, то будет искать любой статический класс с определенным методом IndexOf, у которого первый параметр соответствует типу выражения используемого при вызове метода.А это реализация паттерна Visitor в .Net.
32) Контексты исполнения С каждым потоком связан определенный контекст исполнения. Он включает в себя параметры безопасности, параметры хоста и контекстные данные логического вызова. По умолчанию CLR автоматически его копирует, с самого первого потока до всех вспомогательных. Это гарантирует одинаковые параметры безопасности, но в ущерб производительности. Чтобы управлять этим процессом, используйте класс ExecutionContext.33) Volatile JIT — компилятор гарантирует что доступ к полям помеченным данным ключевым словом будет происходить в режиме волатильного чтения или записи. Оно запрещает компилятору C# и JIT-компилятору кэшировать содержимое поля в регистры процессора, что гарантирует при всех операциях чтения и записи манипуляции будут производиться непосредственно с памятью.34) Классы коллекций для параллельной обработки потоков ConcurrentQueue — обработка элементов по алгоритму FIFO; ConcurrentStack — обработка элементов по алгоритму LIFO; ConcurrentBag — несортированный набор элементов, допускающий дублирование; ConcurrentDictionary — несортированный набор пар ключ-значение.35) Потоки Конструкции пользовательского режима: а) Волатильные конструкции — атомарная операция чтения или записи.VolatileWrite, VolatileRead, MemoryBarrier.б) Взаимозапирающие конструкции — атомарная операция чтения или записи.System.Threading.Interlocked (взаимозапирание) и System.Threading.SpinLock (запирание с зацикливанием).Обе конструкции требуют передачи ссылки (адрес в памяти) на переменную (вспоминаем о структурах).Конструкции режима ядра: Примерно в 80 раз медленнее конструкций пользовательского режима, но зато имеют ряд преимуществ описанных в MSDN (много текста).
Иерархия классов:
WaitHandle EventWaitHandle AutoResetEvent ManualResetEvent Semaphore Mutex 36) Поля класса Не инициализируйте поля явно, делайте это в конструкторе по умолчанию, а уже его с помощью ключевого слова this () используйте для конструкторов принимающих аргументы. Это позволит компилятору генерировать меньше IL кода, т.к. инициализация происходит 1 раз в любом из конструкторов (а не во всех сразу одинаковые значения, копии).37) Запуск только одной копии программы public static void Main () { bool IsExist; using (new Semaphore (0, 1, «MyAppUniqueString», out IsExist)) { if (IsExist) { /* Этот поток создает ядро, другие копии программы не смогут запуститься. */ } esle { /* Этот поток открывает существующее ядро с тем же именем, ничего не делаем, ждем возвращения управления от метода Main, что бы завершить вторую копию приложения. */ } }} 38) Сборка мусора В CLR реализовано два режима:1) Рабочая станция — сборщик предполагает что остальные приложения не используют ресурсы процессора. Режимы — с параллельной сборкой и без нее.2) Сервер — сборщик предполагает что на машине не запущено никаких сторонних приложений, все ресурсы CPU на сборку! Управляемая куча разбирается на несколько разделов — по одному на процессор (со всеми вытекающими, т.е. один поток на одну кучу).39) Финализатор Выполняет функцию последнего желания объекта перед удалением, не может длиться дольше 40 секунд, не стоит с этим играться. Переводит объект минимум в 1 поколение, т.к. удаляется не сразу.40) Мониторинг и управление сборщиком мусора на объекте Вызываем статический метод Alloc объекта GCHandle, передаем ссылку на объект и тип GCHandleType в котором:1) Weak — мониториг, обнаруживаем что объект более не доступен, финализатор мог выполниться.2) WeakTrackResurrection — мониторинг, обнаруживаем что объект более не доступен, финализатор точно был выполнен (при его наличии).3) Normal — контроль, заставляет оставить объект в памяти, память занятая этим объектом может быть сжата.4) Pinned — контроль, заставляет оставить объект в памяти, память занятая этим объектом не может быть сжата (т.е. перемещена).41) CLR CLR по сути является процессором для команд MSIL. В то время как традиционные процессоры для выполнения всех команд используют регистры и стеки, CLR использует только стек.42) Рекурсия Если вы когда нибудь видели приложение, которое приостанавливается на секунду другую, после чего полностью исчезает безо всякого сообщения об ошибке, почти наверняка это было вызвано бесконечной рекурсией. Переполнение стека, оно как известно, не может быть перехвачено и обработано. Почему? Читаем в книгах или блогах.43) Windbg и SOS (Son of Strike) Сколько доменов присутствуют в процессе сразу? — 3. System Domain, Shared Domain и Domain 1 (домен с кодом текущего приложения).Сколько куч (поколений) на самом деле? — 0, 1, 2 и Large Object Heap.Large Object Heap — для очень больших объектов, не сжимается по умолчанию, только через настройку в файле XML конфигурации.
Еще отличие в клиентском и серверном режиме сборки мусора (в книгах не все так подробно, возможно неточность перевода).— для каждого ядра создается свой HEAP, в каждом из которых свои 0, 1, 2 поколения и Large Object Heap.
Создание массива размером больше чем 2 Гб на 64 разрядных платформах.— gcAllowVeryLargeObjects enabled=«true|false»
Что делать, когда свободная память есть, а выделить большой непрерывный ее участок для нового объекта нельзя? — разрешить режим компакт для Large Object Heap. GCSettings.LargeObjectHeapCompactionMode; Не рекомендуется использовать, очень затратно перемещать большие объекты в памяти.
Как быстро в рантайме найти петли потоков (dead-locks)? — ! dlk
Источники (не реклама):1) Джеффри Рихтер, «CLR via C#» 3-е/4-е издание.2) Трей Нэш, «C# 2010 Ускоренный курс для профессионалов».3) Джон Роббинс, «Отладка приложений для Microsoft .NET и Microsoft Windows».4) Александр Шевчук (MCTS, MCPD, MCT) и Олег Кулыгин (MCTS, MCPD, MCT) ресурс ITVDN (https://www.youtube.com/user/CBSystematicsTV/videos? shelf_id=4&view=0&sort=dd).5) Сергей Пугачев. Инженер Microsoft (https://www.youtube.com/watch? v=XN8V9GURs6o)
Надеюсь, данный перечень пришелся по вкусу как начинающим, так и бывалым программистам на C#.
Спасибо за внимание!
Если вы нашли ошибку, прошу сообщить об этом в личном сообщении.