Daily MaverickWorkers’ trillions should rebuild SA for all, not feed financial speculation for the fewThe Jerusalem PostFormer hostages blast 'NAZA' documentary, accuse filmmakers of erasing their sufferingESPN DeportesBroncos y Chiefs prometen gran espectáculo en el primer "Monday Night Football" de la temporadaInquirerRomualdez now behind barsESPNLamar Jackson looks like old self in Ravens' new offenseRFIUkraine: Volodymyr Zelensky suspend le procureur général accusé de corruptionCBS NewsMitch McConnell returns to Capitol Hill after monthslong absenceABC NewsKennedy Center warns of bankruptcy unless Trump’s name is added to buildingThe Guardian AustraliaAustralia politics live: Labor defends Catherine King’s top-tier Qantas membership gift; Hanson accused of ‘Americanising’ politicsالشرقخلاف بشأن "عثمانوف" يؤجل تجديد عقوبات الاتحاد الأوروبي على روسياABC News (Australia)Fitzroy Crossing calls for stronger FASD supportThe Japan TimesJapanese lawmakers seek out renewed dialogue in Beijing
The Daily Newsstand · Free, Always
Monday, September 14, 2026

А чё, так можно было? Три недооценённых атрибута .NET

Translate

Три атрибута, которые меняют результат компиляции. Один запускает метод раньше Main. Второй передаёт текст выражения вместо результата. Третий ускоряет stackalloc в 50 раз.

Все три описаны в документации и применяются внутри .NET. За его пределами их практически не найти. 

Процессор

Система

Intel Core i9-10900KF 3.70GHz, 10 ядер

Windows 10 22H2

AMD Ryzen 9 5950X 3.39GHz, 16 ядер

Windows 10 1809

Intel Xeon W-2255 3.70GHz, 10 ядер

Windows Server 2022

Intel Xeon Silver 4314 2.40GHz, 2 CPU, 32 ядра

Windows Server 2022

Все машины x64

Рантаймы .NET 8, 9 и 10 — все три в одном запуске BenchmarkDotNet 0.15.8.

1. Код, который выполняется до Main

Исходник

Метод помечен атрибутом и больше нигде не встречается:

internal static class Startup
{
    [ModuleInitializer]
    internal static void Init()
    {
        Order.Add("инициализатор модуля");
    }
}

В Main его нет. Там только своя отметка первой строкой:

private static int Main(string[] args)
{
    Startup.Order.Add("точка входа");
    // ...
}

Что выводит отчёт

Порядок событий:
 
  1. инициализатор модуля
  2. точка входа

Одинаково на четырёх машинах и трёх рантаймах.

Причина

Компилятор находит все методы с этим атрибутом и вызывает их из инициализатора модуля. До этого момента ни одна строка кода сборки не выполняется.

К методу есть требования: статический, без параметров, возвращает void, не обобщённый и не внутри обобщённого типа, доступность internal или public. Любое нарушение — ошибка сборки.

Где встречается

В библиотеке классов такой метод один, в EventSource. Основное применение — генераторы исходного кода: настройка выполняется до Main.

Где легко ошибиться

Рассчитывать на порядок, если инициализаторов несколько. Три метода в разных классах вызвались в порядке объявления и от прогона к прогону не менялись, но в спецификации этот порядок не закреплён.

Бросить исключение. Программа падает до точки входа, и Main не выполняется:

Unhandled exception. System.TypeInitializationException:
  The type initializer for '<Module>' threw an exception.
 ---> System.InvalidOperationException: из инициализатора

Применять в библиотеке. Начиная с .NET 10 включено правило CA2255: библиотека меняет порядок запуска приложения и мешает выбросить неиспользуемый код при публикации.

2. Текст выражения вместо значения

Исходник

Второй параметр помечен атрибутом и получает значение по умолчанию:

static void Check(bool condition,
    [CallerArgumentExpression(nameof(condition))] string? text = null)
{
    Console.WriteLine(text + " = " + condition);
}

Что выводит отчёт

Что попадает в text при разных вызовах:
 
  a + b > 10                            ложь
  a * b == 6                            истина
  empty is null                         истина
  !string.IsNullOrEmpty(empty)          ложь

В параметр приходит не результат, а то, что написано в месте вызова.

Причина

Подстановка происходит при сборке: компилятор берёт исходный текст аргумента и передаёт его строковой константой. Программа получает готовую строку и ничего с ней не делает.

Замер это подтверждает. Библиотечная проверка и такая же, написанная явно, исключений в замере нет: 

Способ

i9-10900KF

Ryzen 9 5950X

Xeon W-2255

Xeon Silver 4314

из библиотеки

0,3944

0,2656

0,7058

0,6679

вручную

0,3969

0,2646

0,6427

0,7231

Наносекунды, .NET 10

Где встречается

В 21-м месте библиотеки классов. Например, ArgumentNullException.ThrowIfNull:

public static void ThrowIfNull([NotNull] object? argument,
    [CallerArgumentExpression(nameof(argument))] string? paramName = null)
{
    if (argument is null)
    {
        Throw(paramName);
    }
}

Поэтому исключение показывает имя переменной из места вызова, а не имя параметра argument. Отчёт это подтверждает:

  ArgumentNullException             имя аргумента: empty
  ArgumentOutOfRangeException       имя аргумента: a - 5

Во втором случае именем аргумента стало целое выражение — то, что передали.

Где легко ошибиться

Передать текст вторым аргументом. Тогда компилятор ничего не подставляет и берёт то, что написано:

Check(a + b > 10);                       // текст: a + b > 10
Check(a + b > 10, "передано вручную");   // текст: передано вручную

Забыть про размер сборки. Текст каждого аргумента строкой попадает в метаданные: после шести вызовов сборка увеличилась с 4608 байт до 5120.

3. Отказ от обнуления памяти

Исходник

Два одинаковых метода, различаются одной строкой:

[MethodImpl(MethodImplOptions.NoInlining)]
internal static int StackWithInit(int size)
{
    Span<byte> buffer = stackalloc byte[size];
    buffer[0] = 1;
    buffer[^1] = 2;
 
    return buffer[0] + buffer[^1];
}
 
[SkipLocalsInit]
[MethodImpl(MethodImplOptions.NoInlining)]
internal static int StackWithoutInit(int size)
{
    // тело то же самое
}

Что показывает замер

Буфер

i9-10900KF

Ryzen 9 5950X

Xeon W-2255

Xeon Silver 4314

64 байта

2,452 / 1,765

2,082 / 4,695

2,793 / 2,419

5,409 / 4,512

256 байт

6,475 / 1,729

5,741 / 4,691

8,530 / 2,208

13,015 / 4,501

1024 байта

25,505 / 1,769

17,712 / 4,703

31,281 / 2,574

48,282 / 4,566

4096 байт

103,474 / 2,043

71,811 / 4,939

126,066 / 2,685

189,264 / 4,827

Наносекунды, с обнулением и без, .NET 10 

На четырёх килобайтах разница от 14,5 до 50,6 раза. С обнулением время растёт вместе с буфером, без обнуления — не меняется.

Причина

У метода есть флаг от компилятора, и по нему джит заполняет нулями всю память под локальные переменные, включая ту, что выделена через stackalloc. Атрибут его убирает.

Отсюда и числа в таблице: чем больше буфер, тем дольше заполнение, а без него размер не важен.

Где встречается

Атрибут прописан не в коде, а в общем файле сборки: свойство SkipLocalsInit включено для каждого проекта, входящего в .NET.

Где легко ошибиться

Поставить атрибут ради небольшого буфера. На 64 байтах выигрыш в лучшем случае 1,39 раза, а на Ryzen 9 5950X версия без обнуления медленнее: 4,695 против 2,082.

На этой машине результат отличается от остальных: без заполнения нулями с ростом буфера время не меняется и составляет около 4,7 наносекунды, а на других процессорах оно в пределах 1,7–2,7.

Прочитать буфер раньше, чем в него что-то записали. Атрибут не заполняет память нулями и не требует этого от рантайма — в буфере будут данные от предыдущих вызовов.

Забыть про файл проекта. Без небезопасного контекста атрибут не работает: сборка упадёт с ошибкой CS0227.

Границы замеров

Все замеры сняты на x64 под Windows, на .NET 8, 9 и 10.

У всех измеряемых методов запрещено встраивание. Иначе компилятор перенесёт код метода в замер, а вместе с ним заполнение памяти нулями.

SkipLocalsInit проставлен на отдельных методах, а не на классе: на классе он подействует и на тот вариант, который заполняет память нулями.

В замере на вход всегда передаётся непустая строка, поэтому проверка на null ни разу не срабатывает. Так в замер попадает только сама проверка, без обработки исключения.

Код из статьи

  • AttributeProof — замеры, отчёты и выгрузки с четырёх машин

Ссылки

Всем удачи и до новых встреч!

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.