Pull to refresh

Интересные заметки по C# и CLR (v2.0)

Reading time12 min
Views33K


Пришло время выложить вторую часть заметок по .NET.

Файл конфигурации берет реванш.
*.CONFIG — действительно можно задавать любому приложению?

Тыц
Запускаю известную утилиту от Марка Р. «Procmon.exe», затем свое тестовое оконное приложение и сразу закрываю, останавливаю сбор событий. Фильтрую в полученном логе свое приложение по имени (Include). Вот что там видно:

1) Поиск файла config:
22:09:36.0364337	WindowsFormsApplication1.exe	7388	QueryOpen	S:\WindowsFormsApplication1.exe.config	NAME NOT FOUND

2) Поиск файла INI:
22:09:36.0366595	WindowsFormsApplication1.exe	7388	QueryOpen	S:\WindowsFormsApplication1.INI	NAME NOT FOUND

3) Поиска файла Local:
22:09:36.0537481	WindowsFormsApplication1.exe	7388	QueryOpen	S:\WindowsFormsApplication1.exe.Local	NAME NOT FOUND


Случайно обнаружил что PowerGUI использует файл конфигурации для скриптов PowerShell которые компилируются в EXE (можно даже запаролить или сразу сделать службу).
Сами файлы: Untitled.exe и Untitled.exe.config.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
	<startup useLegacyV2RuntimeActivationPolicy="true">
		<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0" />
		<supportedRuntime version="v2.0.50727" />
	</startup>
  <runtime>
    <loadFromRemoteSources enabled="true"/>
  </runtime>
</configuration>

.INI — может сообщить JIT-компилятору что сборку не нужно оптимизировать. Значит в Release можно оптимизировать MSIL, а JIT-ом уже управлять через этот файл, не используя два разных билда.

[.NET Framework Debugging Control]
GenerateTrackinglnfo = 1
AllowOptimize = 0

.Local — Dynamic-Link Library Redirection

Шутка от Джон Роббинса
Есть еще одно место куда заглядывает процесс.
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\Current Version\Image File Execution Options\

Создаем раздел реестра MyApp.EXE и внутри него новое строковое значение Debugger, в котором указываем полный путь к отладчику (если бы), пишем calc.exe.

Теперь при попытке запустить MyApp.EXE на самом деле будет запускаться калькулятор.

1) Немного истории C#
Что добавлялось в разных версиях
C# 2.0
Обобщения, типы допускающие null, анонимные методы и улучшения с делегатами, итеративные блоки yield. Частичные типы, статические классы, свойства с отличающимися модификаторами доступа, псевдонимы пространств имен (локально — using WinForms = System.Windows.Forms; глобально -FirstAlias::Demo и SecondAlias::Demo), дерективы pragma, буферы фиксированного размера в небезопасном коде (fixed byte data[20]).

C# 3.0
LINQ, автоматические свойства, неявная типизация массивов и локальных переменных, инициализаторы объектов и коллекций в месте объявления, анонимные типы. Лямбда выражения и деревья выражений, расширяющие методы, частичные методы.

C# 4.0
Именованные аргументы, необязательные параметры, обобщенная вариантность, тип dynamic.

C# 5.0
Async/Await, изменение в цикле foreach, атрибуты информации о вызывающем компоненте.


2) Минимальный размер экземпляра ссылочного типа в памяти.
Для x86 и x64
Создаю пустой класс:
class MyClass { }

Компилирую в 32 бита, узнаю размер в Windbg:
0:005> !do 023849bc
Name:        ConsoleApplication1.MyClass
MethodTable: 006c39d4
EEClass:     006c1904
Size:        12(0xc) bytes - вот он размер.
File:        E:\...\ConsoleApplication1.exe
Fields:
None

Компилирую в 64 бита:
0:003> !do 0000007c8d8465b8
Name:        ConsoleApplication1.MyClass
MethodTable: 00007ffa2b5c4320
EEClass:     00007ffa2b6d2548
Size:        24(0x18) bytes
File:        E:\...\ConsoleApplication1.exe
Fields:
None

Меньше не будет потому что первые 4 или 8 байт — слово заголовка объекта. Используется для синхронизации, хранения служебной информации сборщика мусора, финализации, хранения хэш-кода. Некоторые биты этого поля определяют, какая информация хранится в нем в каждый конкретный момент времени.
Вторые 4 или 8 байт — ссылка на таблицу методов.
Третьи 4 или 8 байт для данных и выравнивания, даже если в них ничего нет.
Итого минимальный размер экземпляра ссылочного типа для x86 — 12 байт, x64 — 24 байта.


3) Нестатические поля и методы экземпляра класса в памяти (x64).
Теперь добавим одно поле и автосвойство
class MyClass
{
    private string _field1 = "Some string 1";
    public string Field2 { get; set; }
}

IL видим два поля:
.field private string '<Field2>k__BackingField'
.field private string _field1

И два метода:
.method public hidebysig specialname instance string get_Field2() cil managed
.method public hidebysig specialname instance void set_Field2(string 'value') cil managed

Посмотрим кто куда попал:
0:003> !do 0000005400006600
Name:        ConsoleApplication1.MyClass
MethodTable: 00007ffa2b5c4378
EEClass:     00007ffa2b6d2548
Size:        32(0x20) bytes
File:        E:\...\ConsoleApplication1.exe
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffa89d60e08  4000002        8        System.String  0 instance 0000005400006620 _field1
00007ffa89d60e08  4000003       10        System.String  0 instance 00000054000035a0 <Field2>k__BackingField


Поля попали прямо к экземпляру, и повлияли на его минимальный размер (32 потому что с 17 по 24 бит заняла первая ссылка (ранее были пустыми), а 25-32 вторая (что бы сохранить порядок их следования есть атрибут). Но методов непосредственно в экземпляре нет, только ссылка на них, и соответственно они не повлияли на его размер.

Посмотрим таблицу методов:
0:003> !dumpmt -md 00007ffa2b5c4378
EEClass:         00007ffa2b6d2548
Module:          00007ffa2b5c2fc8
Name:            ConsoleApplication1.MyClass
mdToken:         0000000002000003
File:            E:\...\ConsoleApplication1.exe
BaseSize:        0x20
ComponentSize:   0x0
Slots in VTable: 7
Number of IFaces in IFaceMap: 0
--------------------------------------
MethodDesc Table
           Entry       MethodDesc    JIT Name
00007ffa89ae6300 00007ffa896980e8 PreJIT System.Object.ToString()
00007ffa89b2e760 00007ffa896980f0 PreJIT System.Object.Equals(System.Object)
00007ffa89b31ad0 00007ffa89698118 PreJIT System.Object.GetHashCode()
00007ffa89b2eb50 00007ffa89698130 PreJIT System.Object.Finalize()
00007ffa2b6e0390 00007ffa2b5c4358    JIT ConsoleApplication1.MyClass..ctor()
00007ffa2b5cc130 00007ffa2b5c4338   NONE ConsoleApplication1.MyClass.get_Field2()
00007ffa2b5cc138 00007ffa2b5c4348   NONE ConsoleApplication1.MyClass.set_Field2(System.String)

А вот и они, оба еще не прошли JIT-компиляцию, кроме конструктора и унаследованных экземплярных методов от System.Object которые Ngen себя при установке .NET.

В заключение этого пункта посмотрим полный размер экземпляра с размером объектов на которые указывают его поля:
MyClass mcClass = new MyClass();
mcClass.Field2 = "Some string 2";

0:003> !objsize 0000005400006600
sizeof(0000005400006600) = 144 (0x90) bytes (ConsoleApplication1.MyClass)

Проверим это посмотрев на размер полей:
0:003> !objsize 0000005400006620
sizeof(0000005400006620) = 56 (0x38) bytes (System.String)
0:003> !objsize 00000054000035a0
sizeof(00000054000035a0) = 56 (0x38) bytes (System.String)

Итого: 56 + 56 + 32 = 144.


4) Статическое поле и метод (х64).
Тыц
class MyClass
{
    private string _name = "Some string";
    public static string _STR = "I'm STATIC";
    public static void ImStaticMethod() { }
}

MyClass mcClass = new MyClass();
Console.WriteLine(MyClass._STR);

Минимальный размер экземпляра (статическое поле не учитывается):
0:003> !do 00000033ba2c65f8
Name:        ConsoleApplication1.MyClass
MethodTable: 00007ffa2b5b4370
EEClass:     00007ffa2b6c2550
Size:        24(0x18) bytes
File:        E:\...\ConsoleApplication1.exe
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffa89d60e08  4000002        8        System.String  0 instance 00000033ba2c6610 _name
00007ffa89d60e08  4000003       10        System.String  0   static 00000033ba2c35a0 _STR

Список методов:
0:003> !dumpmt -md 00007ffa2b5b4370
EEClass:         00007ffa2b6c2550
Module:          00007ffa2b5b2fc8
Name:            ConsoleApplication1.MyClass
mdToken:         0000000002000003
File:            E:\...\ConsoleApplication1.exe
BaseSize:        0x18
ComponentSize:   0x0
Slots in VTable: 7
Number of IFaces in IFaceMap: 0
--------------------------------------
MethodDesc Table
           Entry       MethodDesc    JIT Name
00007ffa89ae6300 00007ffa896980e8 PreJIT System.Object.ToString()
00007ffa89b2e760 00007ffa896980f0 PreJIT System.Object.Equals(System.Object)
00007ffa89b31ad0 00007ffa89698118 PreJIT System.Object.GetHashCode()
00007ffa89b2eb50 00007ffa89698130 PreJIT System.Object.Finalize()
00007ffa2b6d0110 00007ffa2b5b4350    JIT ConsoleApplication1.MyClass..cctor()
00007ffa2b6d03f0 00007ffa2b5b4348    JIT ConsoleApplication1.MyClass..ctor()
00007ffa2b5bc130 00007ffa2b5b4338   NONE ConsoleApplication1.MyClass.ImStaticMethod()

ConsoleApplication1.MyClass..cctor() — статический конструктор выполнился только потому что я обратился к статическому полю. Его также называют конструктором типа, и когда он точно вызывается не известно. Он создается автоматически при наличии статических полей. Если ни каких действий производить в нем не нужно то лучше не прописывать его явно, т.к. это помещает оптимизации при помощи флага beforefieldinit в метаданных. Подробней msdn.microsoft.com/ru-ru/library/dd335949.aspx.

Проверяем размеры:
0:003> !objsize 00000033ba2c65f8
sizeof(00000033ba2c65f8) = 72 (0x48) bytes (ConsoleApplication1.MyClass)
0:003> !objsize 00000033ba2c6610
sizeof(00000033ba2c6610) = 48 (0x30) bytes (System.String)

Итого: 24 + 48 = 72.
Статическое поле как и методы, не храниться копией в каждом экземпляре.


5) Найдем родителя и кто удерживает экземпляр от сборки мусора.
Для кучи
Данные и адреса с 3 пункта.
0:003> !dumpclass 00007ffa2b6c2550
Class Name:      ConsoleApplication1.MyClass
mdToken:         0000000002000003
File:            E:\...\ConsoleApplication1.exe
Parent Class:    00007ffa89684908
Module:          00007ffa2b5b2fc8
Method Table:    00007ffa2b5b4370
Vtable Slots:    4
Total Method Slots:  6
Class Attributes:    100000  
Transparency:        Critical
NumInstanceFields:   1
NumStaticFields:     1
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffa89d60e08  4000002        8        System.String  0 instance           _name
00007ffa89d60e08  4000003       10        System.String  0   static 00000033ba2c35a0 _STR

Сходим к родителю:
0:003> !dumpclass 00007ffa89684908
Class Name:      System.Object
mdToken:         0000000002000002
File:            C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Parent Class:    0000000000000000 - никого нет.
Module:          00007ffa89681000
Method Table:    00007ffa89d613e8
Vtable Slots:    4
Total Method Slots:  a
Class Attributes:    102001  
Transparency:        Transparent
NumInstanceFields:   0
NumStaticFields:     0

Кто удерживает mcClass = new MyClass():
0:003> !gcroot 00000033ba2c65f8
Thread 3310:
00000033b81fedb0 00007ffa2b6d031f ConsoleApplication1.Program.Main
rbx:
            ->  00000033ba2c65f8 ConsoleApplication1.MyClass

Похоже на правду.


6) Кто такой Foreach.
Тыц
1. При использовании foreach создается скрытая локальная переменная — итератор цикла.

2. Оператор foreach автоматически вызывает Dispose() в конце цикла, если был реализован интерфейс IDisposable.

3. Компилируется в вызов методов GetEnumerator(), MoveNext() и обращение к свойству Current.

4. Foreach как и yield return, LINQ — ленивая итерация, очень полезна когда например читаем мгогогигабайтный файл по одной строке за раз, экономя память.

5. Foreach для массива использует его свойство Length и индексатор массива, а не создает объект итератора.

6. В C# 5 захваченные переменные внутри циклов foreach теперь отрабатывают правильно, в то время как C# 3 и C# 4 захватят лишь один экземпляр переменной (последний).


7) LINQ
Клац
1. LINQ to Object исполняется в JIT как обычные делегаты (внутрипроцессный запрос), LINQ to SQL строит дерево выражений и выполняет его уже SQL, или любая другая среда. Дерево может быть переведено в делегаты.

2. OrderBy для LINQ to Objects требует загрузки всех данных.

3. При использовании Join() в LINQ to Objects правая последовательность буферизируется, но для левой организуется поток, поэтому если нужно соединить крупную последовательность с мелкой, то полезно по возможности указывать мелкую последовательность как правую.

4. EnumType.Select( x => x ) — это называется вырожденным выражением запроса, результатом является просто последовательность элементов а не сам источник, это может быть важно с точки зрения целостности данных. (Справедливо для правильно спроектированных поставщиков данных LINQ.)


8) Коллекции.
Клик
List T — внутренне хранит массив. Добавление нового элемента это либо установление значения в массиве, либо копирование существующего массива в новый, который в два раза больше (недокументированно) и только потом установке значения. Удаление элемента из List T требует копирования расположенных за ним элементов на позицию назад. По индексу RemoveAt() удалять значительно быстрее чем по значению Remove() (происходит сравнение каждого элемента где бы он не находился).

Массивы всегда фиксированы по размеру, но изменяемы в терминах элементов.

LinkedList T — связанный список, позволяет быстро удалять, вставлять новые элементы, нет индекса, но проход по нему остается эффективным.

ReadOnlyDictionary — просто оболочка, которая скрывает все изменяемые операции за явной реализацией интерфейса. Можно изменять элементы через переданную в ее основу коллекцию.


9) Необязательные параметры метода.
Спойлер!
void Method1( int x ) { x = 5; }
IL:
.method private hidebysig instance void  Method1(int32 x) cil managed
{
  // Code size       4 (0x4)
  .maxstack  8
  IL_0000:  ldc.i4.5
  IL_0001:  starg.s    x
  IL_0003:  ret
} // end of method TestClass::Method1

void Method ( int x = 5 ) { }
IL:
.method private hidebysig instance void  Method([opt] int32 x) cil managed
{
  .param [1] = int32(0x00000005)
  // Code size       1 (0x1)
  .maxstack  8
  IL_0000:  ret
} // end of method TestClass::Method

int x — это константа. А константы хранятся напрямую в метаданных, и значит что для их изменения надо перекомпилировать все использующие этот метод код. (2 источник, страница 413.)

Начиная с C# 4 переименование параметров метода может повлиять на другой код, если в нем использовались именованные аргументы.


10) Оптимизация приложений на платформе .NET
hacer clic
1. Счетчики производительности.
Счетчики обновляются не чаще нескольких раз в секунду, а сам Performance Monitor не позволяет читать значения счетчиков чаще, чем один раз в секунду.

2. Event Tracing for Windows ETW.
Это высокопроизводительный фреймворк регистрации событий.

Читают события от ETW:
а) Windows Performance Toolkit.
б) PerfMonitor. (открытый проект команды CLR в Microsoft.)
в) PerfView. (бесплатный комбайн от Microsoft.)

3. Профилировщик памяти (помимо встроенного в VS) CLR Profiler.
Может подключаться к существующим процессам (CLR не ниже 4.0) или стартовать новые, собирает все события выделения памяти и сборки мусора. Строит кучу графиков.

Общие шаблоны для неправильно работающих многопоточных приложений.


11) Синхронизация.
Тык
lock ( obj ) { }

Выполняется только по требованию, затратна по времени. CLR создает структуру «sync block» в глобальном массиве «sync block table», она имеет обратную ссылку на объект владеющий блокировкой по слабой ссылке (для возможности утилизации) и еще ссылку на monitor реализованном на событиях Win32. Числовой индекс блока синхронизации храниться в слове заголовка объекта. Если объект синхронизации был утилизирован, то его связь с блоком синхронизации затирается для возможности повторного использования на другом объекте.

Но не все так просто, существует еще тонкая блокировка (thin lock). Если блок синхронизации еще не создан и только один поток владеет объектом, то другой при попытке выполнения будет ждать короткое время когда информация о владельце исчезнет из слова заголовка объекта, если этого не произойдет то тонкая блокировка будет преобразована в обычную.


12) Упаковка.
Пакет ннада
Имеем структуру:
public struct Point
{
    public int X;
    public int Y;
}

List<Point> polygon = new List<Point>();
for ( int i = 0; i < 10000000; i++ )
{
    polygon.Add( new Point() { X = rnd.Next(), Y = rnd.Next() } );
}

Point point = new Point { X = 5, Y = 7 };
bool contains = polygon.Contains( point );

Производим запуск № 1.

Теперь добавим методы:
public override int GetHashCode()
{
    return (X & Y) ^ X; // для теста.
}

public override bool Equals( object obj )
{
    if ( !( obj is Point ) ) return false;
    Point other = ( Point ) obj;
    return X == other.X && Y == other.Y;
}

public bool Equals( Point other )
{
    return X == other.X && Y == other.Y;
}

Производим запуск № 2.

Теперь добавим реализацию интерфейса (подходящий метод уже есть):
public struct Point : IEquatable<Point>
{ ... }

Производим запуск № 3.
(List T не имеет реализации интерфейса IEquatable T )

Пробуем анонимный тип:
var someType = new { Prop1 = 2, Prop2 = 80000 };

var items = Enumerable.Range( 0, 10000000 )
                   .Select( i => new { Prop1 = i, Prop2 = i+i } )
                   .ToList();
items.Contains(someType);

Производим запуск № 4.
Компилятор выяснил что тип someType идентичен типу в методах расширения, и поэтому проблем не возникло.

Итоги
Результаты тестов:


А вот как выглядит анонимный тип в IL:


Если интересно, как выглядит someType в памяти
var someType = new { Prop1 = 2, Prop2 = 80000 };

0:005> !do 0000008da2745e08
Name:        <>f__AnonymousType0`2[[System.Int32, mscorlib],[System.Int32, mscorlib]]
MethodTable: 00007ffa2b5b4238
EEClass:     00007ffa2b6c2548
Size:        24(0x18) bytes
File:        E:\...\BoxingUnboxingPointList.exe
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
0...0  4000003        8         System.Int32  1 instance                2 <Prop1>i__Field
0...0  4000004        c         System.Int32  1 instance            80000 <Prop2>i__Field

Тип значения хранит самое значение — 2 и 80000.

Таблица методов:
0:005> !dumpmt -md 00007ffa2b5b4238
EEClass:         00007ffa2b6c2548
Module:          00007ffa2b5b2fc8
Name:            <>f__AnonymousType0`2[[System.Int32, mscorlib],[System.Int32, mscorlib]]
mdToken:         0000000002000004
File:            E:\...\BoxingUnboxingPointList.exe
BaseSize:        0x18
ComponentSize:   0x0
Slots in VTable: 7
Number of IFaces in IFaceMap: 0
--------------------------------------
MethodDesc Table
           Entry       MethodDesc    JIT Name
0...8 0...0   NONE <>f__AnonymousType0`2[[...]].ToString()
0...0 0...8   NONE <>f__AnonymousType0`2[[...]].Equals(System.Object)
0...8 0...0   NONE <>f__AnonymousType0`2[[...]].GetHashCode()
0...0 0...0 PreJIT System.Object.Finalize()
0...0 0...8   NONE <>f__AnonymousType0`2[[...]]..ctor(Int32, Int32)
0...8 0...0   NONE <>f__AnonymousType0`2[[...]].get_Prop1()
0...0 0...8   NONE <>f__AnonymousType0`2[[...]].get_Prop2()

Я тоже ожидал увидеть другое :)


13) Async/Await
Внутри он больше
Async — не имеет представления в сгенерированном коде.
Await — конечный автомат, структура. Если к моменту встречи этого типа результат работы уже будет доступен, то метод продолжит работу с полученным результатом в синхронном режиме. Метод Task TResult.ConfigureAwait со значением true попытается выполнить маршалинг продолжения обратно в исходный захваченный контекст, если этого не требуется используем значение false.

Отличный бесплатный видео урок на эту тему от Александра.

Также очень хорошо прочитать перевод статьи "SynchronizationContext — когда MSDN подводит".


14) Сборка мусора
Тык
Если играться с закрепленными объектами в поколении «0», то CLR может объявить это поколение более старшим, а себе выделить новое.

В поколение «1» можно попасть только из «0», объекты имеющие финализацию точно окажутся в нем.

Размер поколения «2» не ограничивается искусственно, используется вся доступная память (которой Windows может поделиться с CLR), но GC не ждет ее полного заполнения а использует пороговые значения (какие не знаю).

На фазе маркировки объект помеченный как живой может потерять свою ссылку, тем самым пережив одну сборку.

В Large Object Heap попадают объекты больше 85 кбайт, но это относится к одному объекту а не его графу (включенные объекты). Связан с поколением «2», собираются вместе.


Источники:
1) Джон Роббинс «Отладка приложений для Microsoft .NET и Microsoft Windows».
2) Джон Скит «C# для профессионалов.Тонкости программирования», 3 издание.
3) Саша Голдштейн, Дима Зурбалев, Идо Флатов «Оптимизация приложений на платформе .Net».

Много букв получилось, JIT-компилятор остается на потом.
Спасибо за внимание.
Tags:
Hubs:
+18
Comments30

Articles

Change theme settings