Я провожу несколько тестов распределения и меня смущают результаты распределения.
Предположим, что это тест, в котором класс создается несколько раз, его ссылка никогда не выходит за пределы метода.
[Benchmark]
public void AllocateClass()
{
for (int i = 0; i < count; i++)
{
var s = new SampleClass();
}
}
Когда тесты завершаются, общий объем выделенной памяти равен нулю. Теперь второй тест, в котором ссылка выходит за пределы метода.
SampleClass s;
[Benchmark]
public void AllocateClass()
{
for (int i = 0; i < count; i++)
{
s = new SampleClass();
}
}
При втором тесте память распределяется как положено. Поскольку класс является ссылочным типом, я ожидаю, что он будет выделен в куче независимо от того, экранируется его ссылка или нет, это какая-то оптимизация компилятора?
^^ Я тоже так думаю. Но я думаю, это не ваш настоящий эталон, не так ли?
Возможно, вы захотите вернуть коллекцию и избежать отложенного выполнения: benchmarkdotnet.org/articles/samples/… чтобы получить правильные тесты...
Имейте в виду, что объект можно собрать, даже если его собственный конструктор все еще работает. При условии, что можно доказать, что ничто не пытается разыменовать его позже.
Вместо использования эталонного теста (и нераскрытых параметров сборки проекта и времени выполнения...) вам следует использовать счетчики производительности GC для получения фактических данных.
@Fildor Не уверен, что вы подразумеваете под реальным тестом, я просто пытаюсь лучше понять распределение. Я вижу, что как только любому из созданных свойств класса присваивается значение, происходит фактическое распределение.
@Damien_The_Unbeliever «Имейте в виду, что объект можно собрать, даже если его собственный конструктор все еще работает». - Пожалуйста, поподробнее...
«Не уверен, что вы подразумеваете под реальным тестом» - мне было интересно, не является ли это всего лишь упрощением другого теста, который дал неожиданные цифры, поскольку это и так не имеет особого смысла. Создание объекта и немедленное выход его за рамки - я ожидаю, что произойдет всякая магия компилятора/JIT, которую будет трудно отследить. Если вам нужно, чтобы его не собирали до завершения измерения, сохраните (жесткий) эталон.
@canton7 canton7 Спасибо, так что это действительно связано с оптимизацией компилятора.
@NullReference Оптимизация времени выполнения: компилятор не делает подобных вещей. Переключите раскрывающийся список вверху на «IL», чтобы увидеть, что выдает компилятор.
Также классы не обязательно размещать в куче. Традиционно так и делается, но это не гарантия. Среда выполнения начинает экспериментировать с ограниченными формами escape-анализа, которые могут выделить класс в стеке в определенных ситуациях, если можно доказать, что ссылка на него никогда не выходит за пределы текущего метода.
@Dai Класс может быть освобожден/завершен, как только он достигнет точки, в которой он больше никогда не будет использоваться, независимо от того, находятся ли какие-либо ссылки на него в области видимости (область действия не имеет значения), в том числе когда выполняется один из его собственных методов. В том числе и строительство. Смотрите здесь. Это, конечно, сопряжено с множеством сложностей, связанных с освобождением неуправляемых ресурсов, пока они еще используются или еще не инициализированы, поэтому мы рекомендуем людям избегать финализаторов и использовать SafeHandles
.
Я предполагаю, что вы запускаете свой тест в режиме выпуска, и наблюдаемое вами поведение, скорее всего, является оптимизацией компилятора.
Вы можете использовать https://sharplab.io, чтобы провести несколько экспериментов и посмотреть сгенерированные IL и asm.
Например, JIT Asm от SharpLab в режиме Release:
Метод с исключенным распределением:
C.AllocateClass(Int32)
L0000: push ebp
L0001: mov ebp, esp
L0003: xor eax, eax
L0005: test edx, edx
L0007: jle short L000e
L0009: inc eax
L000a: cmp eax, edx
L000c: jl short L0009
L000e: pop ebp
L000f: ret
Метод без удаленного кода:
C.AllocateClass(Int32)
L0000: push ebp
L0001: mov ebp, esp
L0003: sub esp, 0x18
L0006: xor eax, eax
L0008: mov [ebp-0xc], eax
L000b: mov [ebp-0x10], eax
L000e: mov [ebp-0x14], eax
L0011: mov [ebp-0x18], eax
L0014: mov [ebp-4], ecx
L0017: mov [ebp-8], edx
L001a: cmp dword ptr [0x2bb6c18c], 0
L0021: je short L0028
L0023: call 0x71f90f30
L0028: nop
L0029: xor ecx, ecx
L002b: mov [ebp-0xc], ecx
L002e: nop
L002f: jmp short L0067
L0031: nop
L0032: mov ecx, 0x2bb6ca04
L0037: call 0x0704300c
L003c: mov [ebp-0x14], eax
L003f: mov ecx, [ebp-4]
L0042: mov ecx, [ecx+4]
L0045: mov [ebp-0x18], ecx
L0048: mov ecx, [ebp-0x14]
L004b: call dword ptr [0x2bb6ca24]
L0051: push dword ptr [ebp-0x14]
L0054: mov ecx, [ebp-0x18]
L0057: mov edx, [ebp-0xc]
L005a: call System.Runtime.CompilerServices.CastHelpers.StelemRef(System.Array, IntPtr, System.Object)
L005f: nop
L0060: mov eax, [ebp-0xc]
L0063: inc eax
L0064: mov [ebp-0xc], eax
L0067: mov ecx, [ebp-0xc]
L006a: cmp ecx, [ebp-8]
L006d: setl cl
L0070: movzx ecx, cl
L0073: mov [ebp-0x10], ecx
L0076: cmp dword ptr [ebp-0x10], 0
L007a: jne short L0031
L007c: nop
L007d: mov esp, ebp
L007f: pop ebp
L0080: ret
«Полагаю, вы запускаете свой тест в режиме выпуска» — BenchmarkDotnet выйдет из строя с сообщением об ошибке, если вы не работаете в режиме выпуска.
ваш первый фрагмент по сути ничего не делает. Я предполагаю, что JIT-компилятор просто пропускает все выделение.