tgoop.com/csharp_gepard/139
Last Update:
Аллокация объектов на стеке #память
Наверное, многие слышали, что .NET 9 теперь старается не размещать короткоживущие объекты в куче, если они являются boxed value-типами. Теперь результат "боксинга" таких типов располагается на стеке, что позволяет разгрузить GC и увеличить производительность.
Конечно же, есть "но". Дело в том, что runtime должен быть уверен, что результат боксинга не выходит за границы метода. В этом и только в этом случае, результат боксинга value-типа (фактически object) будет располагаться на стеке. Это очень похоже на Rust, где есть встроенная в язык функция наблюдения за временем жизни переменной. Выход из метода "удаляет" все сущности, которые были созданы внутри него, но не используются вне него.
В примере ниже, как и раньше до .NET 9, произойдёт боксинг цифр "3" и "4" при вызове метода Compare. Однако runtime (JIT) "видит", что эти объекты не выходят за пределы метода RunIt. Следовательно, результат боксинга можно разместить на стеке.
static bool Compare(object? x, object? y)
{
if (x == null || y == null)
{
return x == y;
}
return x.Equals(y);
}
public static int RunIt()
{
bool result = Compare(3, 4);
return result ? 0 : 100;
}
Понимание механики работы этой оптимизации важно, так как, например, позволяет решить проблему следующего кода:
var result = 0;
foreach ((int a, int b) in _values)
{
result += Compare(a, b) ? 1 : -1;
}
return result;
Дело в том, что в конкретно этом случае, по мнению JIT, в методе может быть создано слишком много "забокшеных" value-типов, что, в свою очередь, значит, что оптимизация применена не будет.
Есть и иное предположение. Оно основано на том, что функция
Compare
принимает, условно, всё, что угодно. Это, в свою очередь, значит, что JIT справедливо полагает: размер данных в аргументах функции может быть разным на любой итерации for
. А это означает, что невозможно вызывать Compare с уравниванием всех возможных типов аргументов по размеру (см. вот этот комментарий). Кажется, что решение очевидно: нам нужно создать промежуточный метод, который будет определять типы (для понимания размера) и границы создаваемых временных переменных. Некий контекст, который подскажет JIT'у, что боксинг временный и нужен только на одну итерацию for.
bool CloseContext(int a, int b) => Compare(a, b);
Но, увы, это не сработает, так как в дело включается другая оптимизация - method inlining. Для JIT'a этот метод - прекрасный случай для автоматического инлайнинга. Увы, это ломает нашу прекрасную идею с обозначением контекста, в рамках которого будут жить наши boxed value-типы.
Значит, мы должны не только создать отдельный метод, но и прямо указать, что делать ему "инлайн" не нужно. Благо, у нас есть специальный атрибут для подобных указаний -
[MethodImpl(MethodImplOptions.NoInlining)]
. В этом случае, боксинг происходит, но его результат остаётся на стеке. Результаты хорошо видны на бенчмарке.Код бенчмарка тут. Если нужно больше подробностей, то я написал этот пост под впечатлениями вот отсюда.
BY C# Heppard

Share with your friend now:
tgoop.com/csharp_gepard/139