Подготовка библиотек .NET для тримминга

Пакет SDK для .NET позволяет уменьшить размер автономных приложений путем обрезки. Обрезка удаляет неиспользуемый код из приложения и его зависимостей. Не все коды совместимы с обрезкой. .NET предоставляет предупреждения о результатах анализа обрезки для обнаружения шаблонов, которые могут причинить проблемы в обрезанных приложениях. В этой статье:

Предварительные условия

Пакет SDK для .NET 8 или более поздней версии.

Включите предупреждения об усечении в библиотеке

Предупреждения о сокращении в библиотеке можно найти любым из следующих способов:

  • Включение обрезки для конкретного проекта с помощью свойства IsTrimmable.
  • Создание тестового приложения с поддержкой тримминга, использующего библиотеку, и включение тримминга для тестового приложения. Не обязательно ссылаться на все API в библиотеке.

Мы рекомендуем использовать оба подхода. Обрезка для конкретного проекта удобна и отображает предупреждения об обрезке для одного проекта, но полагается на ссылки, помеченные как совместимые с обрезкой, чтобы просмотреть все предупреждения. Оптимизация тестового приложения более трудоемка, но показывает все предупреждения.

Включение обрезки для конкретного проекта

Задайте <IsTrimmable>true</IsTrimmable> в файле проекта.

<PropertyGroup>
    <IsTrimmable>true</IsTrimmable>
</PropertyGroup>

Задание свойства IsTrimmable в MSBuild на true помечает сборку как "обрезаемую" и включает предупреждения о ее обрезке. Термин "Trimmable" в этом контексте означает некий проект, который можно подстраивать или изменять.

  • Считается совместимым с обрезкой.
  • При сборке не следует создавать предупреждения, связанные с обрезками. При использовании в обрезаном приложении сборка имеет неиспользуемые элементы, обрезанные в окончательных выходных данных.

Свойство IsTrimmable по умолчанию имеет значение true при настройке проекта как совместимого с AOT при помощи <IsAotCompatible>true</IsAotCompatible>. Дополнительные сведения см. в анализаторах совместимости AOT.

Чтобы создать предупреждения о тримировании без маркировки проекта как совместимого с тримом, используйте <EnableTrimAnalyzer>true</EnableTrimAnalyzer> вместо <IsTrimmable>true</IsTrimmable> этого.

Проверьте, совместимы ли ссылочные сборки с обрезкой

Если включить анализ обрезки для библиотеки, можно включить проверку того, что все ссылочные сборки также аннотированы для совместимости обрезки, задав свойству VerifyReferenceTrimCompatibility значение true:

<PropertyGroup>
  <IsTrimmable>true</IsTrimmable>
  <VerifyReferenceTrimCompatibility>true</VerifyReferenceTrimCompatibility>
</PropertyGroup>

Если это свойство включено, анализатор предупреждает о любых указанных сборках, у которых нет IsTrimmable метаданных. Это помогает гарантировать, что все зависимости в проекте помечены для обеспечения совместимости обрезки. Предупреждение, которое выдается, — IL2125.

Эта проверка выполняется по выбору, так как:

  • Не все библиотеки, поддерживающие обрезку, были обновлены для включения метаданных IsTrimmable.
  • Предупреждение может быть шумным, если у вас есть много зависимостей, которые работают правильно с обрезкой, но не помечены явным образом как таковые.

Рекомендуется включить эту проверку, если вы хотите убедиться, что все зависимости явно помечены их авторами как совместимые с обрезкой.

Отображение всех предупреждений с помощью тестового приложения

Чтобы отобразить все предупреждения анализа для библиотеки, триммер должен проанализировать реализацию библиотеки и всех зависимостей, которые использует библиотека.

При создании и публикации библиотеки:

  • Реализации зависимостей недоступны.
  • Доступные референсные сборки содержат недостаточно сведений для триммера, чтобы определить, совместимы ли они с триммингом.

Из-за ограничений зависимостей необходимо создать автономное тестовое приложение, которое использует библиотеку и его зависимости. Тестовое приложение содержит всю информацию, которая необходима триммеру для выдачи предупреждений о несовместимости с триммингом в:

  • Код библиотеки.
  • Код, на который библиотека ссылается из своих зависимостей.

Примечание.

Если библиотека имеет другое поведение в зависимости от целевой платформы, создайте тестовое приложение обрезки для каждой из целевых платформ, поддерживающих обрезку. Например, если библиотека использует условную компиляцию , например #if NET7_0 для изменения поведения.

Чтобы создать тестовое приложение для обрезки, выполните следующие действия.

  • Создайте отдельный проект консольного приложения.
  • Добавьте ссылку на библиотеку.
  • Измените проект, аналогичный проекту, приведенному ниже, с помощью следующего списка:

Если библиотека нацелена на TFM, который не совместим с обрезкой, например net472 или netstandard2.0, нет преимуществ для создания тестового приложения обрезки. Обрезка поддерживается только для .NET 6 и более поздних версий.

  • Добавьте <PublishTrimmed>true</PublishTrimmed>.
  • Добавьте ссылку на проект библиотеки с помощью <ProjectReference Include="/Path/To/YourLibrary.csproj" />.
  • Укажите библиотеку в качестве корневой сборки триммера.<TrimmerRootAssembly Include="YourLibraryName" />
    • TrimmerRootAssembly обеспечивает анализ каждой части библиотеки. Он сообщает триммеру, что эта сборка является корнем. Сборка root означает, что триммер анализирует каждый вызов в библиотеке и проходит все пути кода, исходящие из этой сборки.

CSPROJ-файл

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <PublishTrimmed>true</PublishTrimmed>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
    <TrimmerRootAssembly Include="MyLibrary" />
  </ItemGroup>

</Project>

После обновления файла проекта запустите dotnet publish с помощью целевого идентификатора среды выполнения (RID).

dotnet publish -c Release -r <RID>

Следуйте приведенному выше шаблону для нескольких библиотек. Чтобы отображать предупреждения анализа обрезки одновременно для нескольких библиотек, добавьте их все в один проект в качестве элементов ProjectReference и TrimmerRootAssembly. Добавление всех библиотек в один и тот же проект с ProjectReferenceTrimmerRootAssembly элементами предупреждает о зависимостях, если какая-либо из корневых библиотек использует тримминг-непригодный API в зависимости. Чтобы просмотреть предупреждения, связанные только с определенной библиотекой, следует ссылаться только на эту библиотеку.

Примечание.

Результаты анализа зависят от сведений о реализации зависимостей. Обновление до новой версии зависимости может привести к предупреждениям анализа:

  • Если новая версия добавила неясные шаблоны отражения.
  • Даже если не было изменений в API.
  • Введение предупреждений об анализе обрезки является критическим изменением при использовании библиотеки с PublishTrimmed.

Требования к целевой платформе

При подготовке библиотек к обрезке нацелитесь на последнюю поддерживаемую платформу TFM. Это поможет вам воспользоваться последними улучшениями анализатора. По крайней мере нацелиться на net6.0 или более позднюю версию. Эта версия необходима для предупреждений об анализе обрезки.

Если ваша библиотека также предназначена для платформ, выпущенных до net6.0 (например, netstandard2.0 или net472), добавьте несколько целевых объектов, включая net6.0. Это гарантирует, что приложения, нацеленные на net6.0 или более поздние версии, получают библиотеку, которая поддерживает анализ обрезки.

Используйте функцию MSBuild для условного включения IsTargetFrameworkCompatible для IsTrimmable и более поздних версий:

<PropertyGroup>
  <TargetFrameworks>netstandard2.0;net6.0;net10.0</TargetFrameworks>
  <IsTrimmable Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">true</IsTrimmable>
</PropertyGroup>

Дополнительные сведения см. в разделе "Обрезка" не может использоваться с .NET Standard или .NET Framework.

Устранение предупреждений об обрезке

Описанные выше действия создают предупреждения о коде, которые могут вызвать проблемы при использовании в обрезаном приложении. В следующих примерах показаны наиболее распространенные предупреждения с рекомендациями по их устранению.

ТребуетСяUnreferencedCode

Рассмотрим следующий код, который используется [RequiresUnreferencedCode] для указания того, что указанный метод требует динамического доступа к коду, который не ссылается статически, например через System.Reflection.

public class MyLibrary
{
    public static void MyMethod()
    {
        // warning IL2026 :
        // MyLibrary.MyMethod: Using 'MyLibrary.DynamicBehavior'
        // which has [RequiresUnreferencedCode] can break functionality
        // when trimming app code.
        DynamicBehavior();
    }

    [RequiresUnreferencedCode(
        "DynamicBehavior is incompatible with trimming.")]
    static void DynamicBehavior()
    {
    }
}

Предыдущий выделенный код указывает, что библиотека вызывает метод, явно помеченный как несовместимый с обрезкой. Чтобы избавиться от предупреждения, подумайте, действительно ли MyMethod необходимо вызывать DynamicBehavior. Если да, аннотируйте вызывающий код MyMethod с [RequiresUnreferencedCode], который распространяет предупреждение, чтобы вызывающие MyMethod получили предупреждение:

public class MyLibrary
{
    [RequiresUnreferencedCode("Calls DynamicBehavior.")]
    public static void MyMethod()
    {
        DynamicBehavior();
    }

    [RequiresUnreferencedCode(
        "DynamicBehavior is incompatible with trimming.")]
    static void DynamicBehavior()
    {
    }
}

После того как вы распространили атрибут вплоть до публичного API, приложения, вызывающие библиотеку:

  • Получение предупреждений только для общедоступных методов, которые не являются оптимизируемыми.
  • Не получайте предупреждения, как IL2104: Assembly 'MyLibrary' produced trim warnings.

Динамически доступные члены

public class MyLibrary3
{
    static void UseMethods(Type type)
    {
        // warning IL2070: MyLibrary.UseMethods(Type): 'this' argument does not satisfy
        // 'DynamicallyAccessedMemberTypes.PublicMethods' in call to
        // 'System.Type.GetMethods()'.
        // The parameter 't' of method 'MyLibrary.UseMethods(Type)' doesn't have
        // matching annotations.
        foreach (var method in type.GetMethods())
        {
            // ...
        }
    }
}

В приведенном выше коде UseMethods вызывает метод рефлексии, который имеет требование [DynamicallyAccessedMembers]. Требование указывает, что общедоступные методы типа доступны. Удовлетворить требование путем добавления того же требования к параметру UseMethods.

static void UseMethods(
   // State the requirement in the UseMethods parameter.
   [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    // ...
}

Теперь любые вызовы к UseMethods вызывают предупреждения, если передают значения, не соответствующие требованию PublicMethods. Как и [RequiresUnreferencedCode], как только вы распространили такие предупреждения на общедоступные API, работа завершена.

В следующем примере неизвестный Тип передается в параметр аннотированного метода. Неизвестный элемент Type поступает из поля:

static Type type;
static void UseMethodsHelper()
{
    // warning IL2077: MyLibrary.UseMethodsHelper(Type): 'type' argument does not satisfy
    // 'DynamicallyAccessedMemberTypes.PublicMethods' in call to
    // 'MyLibrary.UseMethods(Type)'.
    // The field 'System.Type MyLibrary::type' does not have matching annotations.
    UseMethods(type);
}

Аналогичным образом здесь проблема заключается в том, что поле type передается в параметр с этими требованиями. Исправлено путем добавления [DynamicallyAccessedMembers] в поле. [DynamicallyAccessedMembers] предупреждает о коде, который назначает несовместимые значения полю. Иногда этот процесс продолжается до тех пор, пока общедоступный API не будет аннотирован, а в других случаях он заканчивается, когда конкретный тип переходит в место, соответствующее требованиям. Например:

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
static Type type;

static void UseMethodsHelper()
{
    MyLibrary.type = typeof(System.Tuple);
}

В этом случае анализ обрезки сохраняет общедоступные методы Tupleи создает дополнительные предупреждения.

Рекомендации

  • Избегайте отражения, когда это возможно. При использовании рефлексии минимизируйте область рефлексии, чтобы она была доступна только из небольшой части библиотеки.
  • Аннотируйте код с помощью DynamicallyAccessedMembers, чтобы статически указать требования к оптимизации, когда это возможно.
  • Рекомендуется переорганизовать код, чтобы он соответствовал отанализируемому шаблону, который можно ознамещать с помощью DynamicallyAccessedMembers
  • Если код несовместим с обрезкой, аннотируйте его с помощью RequiresUnreferencedCode и распространяйте эту аннотацию на все вызывающие методы до тех пор, пока соответствующие общедоступные API не будут аннотированы.
  • Избегайте использования кода, который использует рефлексию таким образом, что не понимается статическим анализом. Например, следует избегать использования рефлексии в статических конструкторах. Использование статически неанализируемого отражения в статических конструкторах приводит к тому, что предупреждение распространяется на все члены класса.
  • Избегайте аннотирования виртуальных методов или методов интерфейса. Для аннотирования виртуальных или интерфейсных методов требуется, чтобы все переопределения имели совпадающие аннотации.
  • Если API в основном несовместим с триммингом, может потребоваться рассмотрение альтернативных подходов к программированию. Распространенным примером являются сериализаторы на основе отражения. В таких случаях рассмотрите возможность внедрения других технологий, таких как генераторы источников, для создания кода, который проще анализировать статически. Например, см. как использовать генерацию кода в System.Text.Json.

Устранение предупреждений для шаблонов, которые невозможно проанализировать

Лучше исправлять предупреждения, выражая намерение вашего кода с помощью [RequiresUnreferencedCode] и DynamicallyAccessedMembers, когда это возможно. Однако в некоторых случаях может возникнуть необходимость включить сокращение библиотеки, которая использует шаблоны, не поддающиеся выражению с помощью данных атрибутов или без рефакторинга существующего кода. В этом разделе описаны некоторые продвинутые способы устранения предупреждений анализа обрезки.

Предупреждение

Эти методы могут изменить поведение или код или привести к исключениям среды выполнения при неправильном использовании.

БезусловнаяSuppressMessage

Рассмотрим код, который:

  • Намерение не может быть выражено с аннотациями.
  • Создает предупреждение, но не представляет реальную проблему во время выполнения.

Предупреждения можно отключить с помощью UnconditionalSuppressMessageAttribute. Это аналогично SuppressMessageAttribute, но сохраняется в IL и учитывается во время анализа обрезки.

Предупреждение

При подавлении предупреждений вы несете ответственность за обеспечение совместимости кода с операцией "обрезки" на основе инвариантов, которые вы знаете как истинные в результате проверки и тестирования. Используйте осторожность с этими аннотациями, так как если они неверны или если инварианты вашего кода изменяются, они могут скрывать ошибки в коде.

Например:

class TypeCollection
{
    Type[] types;

    // Ensure that only types with preserved constructors are stored in the array
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
    public Type this[int i]
    {
        // warning IL2063: TypeCollection.Item.get: Value returned from method
        // 'TypeCollection.Item.get' can't be statically determined and may not meet
        // 'DynamicallyAccessedMembersAttribute' requirements.
        get => types[i];
        set => types[i] = value;
    }
}

class TypeCreator
{
    TypeCollection types;

    public void CreateType(int i)
    {
        types[i] = typeof(TypeWithConstructor);
        Activator.CreateInstance(types[i]); // No warning!
    }
}

class TypeWithConstructor
{
}

В приведенном выше коде свойство индексатора было аннотировано таким образом, чтобы возвращаемое Type соответствовало требованиям CreateInstance. Это гарантирует, что TypeWithConstructor конструктор сохранён и что вызов CreateInstance не вызывает предупреждений. Аннотация установки индексатора гарантирует, что все типы, хранящиеся в Type[], имеют конструктор. Однако анализ не может увидеть это и создает предупреждение для геттера, так как он не знает, что возвращаемый тип сохраняет свой конструктор.

Если вы уверены, что выполнены требования, вы можете подавить это предупреждение, добавив [UnconditionalSuppressMessage] в геттер:

class TypeCollection
{
    Type[] types;

    // Ensure that only types with preserved constructors are stored in the array
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
    public Type this[int i]
    {
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2063",
            Justification = "The list only contains types stored through the annotated setter.")]
        get => types[i];
        set => types[i] = value;
    }
}

class TypeCreator
{
    TypeCollection types;

    public void CreateType(int i)
    {
        types[i] = typeof(TypeWithConstructor);
        Activator.CreateInstance(types[i]); // No warning!
    }
}

class TypeWithConstructor
{
}

Важно подчеркнуть, что это допустимо только для подавления предупреждения, если есть заметки или код, которые гарантируют, что отраженные элементы являются видимыми целевыми объектами отражения. Недостаточно, чтобы член был объектом доступа к вызову, полю или свойству. Иногда это может быть так, но такой код неизбежно сломается в конечном итоге по мере добавления дополнительных оптимизаций обрезки. Свойства, поля и методы, которые не являются видимыми целевыми объектами отражения, могут быть удалены, удалены их имена, перемещаются в разные типы или иначе оптимизированы таким образом, чтобы отражать их. При подавлении предупреждения можно выполнять отражение только тех целевых объектов, которые являются видимыми для анализатора обрезки в другом расположении.

// Invalid justification and suppression: property being non-reflectively
// used by the app doesn't guarantee that the property will be available
// for reflection. Properties that are not visible targets of reflection
// are already optimized away with Native AOT trimming and may be
// optimized away for non-native deployment in the future as well.
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2063",
    Justification = "*INVALID* Only need to serialize properties that are used by"
                    + "the app. *INVALID*")]
public string Serialize(object o)
{
    StringBuilder sb = new StringBuilder();
    foreach (var property in o.GetType().GetProperties())
    {
        AppendProperty(sb, property, o);
    }
    return sb.ToString();
}

Динамическая Зависимость

Атрибут [DynamicDependency] можно использовать для указания того, что член имеет динамическую зависимость от других членов. В результате указанные элементы сохраняются всякий раз, когда сохраняется элемент с атрибутом, но это само по себе не отключает предупреждения. В отличие от других атрибутов, которые информируют анализ тримминга о поведении отражения кода, [DynamicDependency] сохраняет только другие члены. Его можно использовать вместе с [UnconditionalSuppressMessage] для устранения некоторых предупреждений анализа.

Предупреждение

Используйте [DynamicDependency] атрибут только в качестве последнего способа, когда другие подходы недоступны. Предпочтительнее выразить поведение отражения с помощью [RequiresUnreferencedCode] или [DynamicallyAccessedMembers].

[DynamicDependency("Helper", "MyType", "MyAssembly")]
static void RunHelper()
{
    var helper = Assembly.Load("MyAssembly").GetType("MyType").GetMethod("Helper");
    helper.Invoke(null, null);
}

Без DynamicDependency обрезка может удалить Helper из MyAssembly или полностью удалить MyAssembly, если на него нет ссылок в другом месте, что приведет к предупреждению о возможной ошибке во время выполнения. Атрибут гарантирует, что Helper будет сохранен.

Атрибут указывает элементы, которые должны быть сохранены, с помощью string или DynamicallyAccessedMemberTypes. Тип и сборка либо являются неявными в контексте атрибута, либо явно указаны в атрибуте (с помощью Type или string для типа и имени сборки).

В строках типа и элемента используется разновидность формата строки идентификатора комментария документации C# без префикса элемента. Строка-член не должна содержать имя декларационного типа и может опустить параметры, чтобы сохранить все члены указанного имени. Некоторые примеры формата показаны в следующем коде:

[DynamicDependency("MyMethod()")]
[DynamicDependency("MyMethod(System,Boolean,System.String)")]
[DynamicDependency("MethodOnDifferentType()", typeof(ContainingType))]
[DynamicDependency("MemberName")]
[DynamicDependency("MemberOnUnreferencedAssembly", "ContainingType"
                                                 , "UnreferencedAssembly")]
[DynamicDependency("MemberName", "Namespace.ContainingType.NestedType", "Assembly")]
// generics
[DynamicDependency("GenericMethodName``1")]
[DynamicDependency("GenericMethod``2(``0,``1)")]
[DynamicDependency(
    "MethodWithGenericParameterTypes(System.Collections.Generic.List{System.String})")]
[DynamicDependency("MethodOnGenericType(`0)", "GenericType`1", "UnreferencedAssembly")]
[DynamicDependency("MethodOnGenericType(`0)", typeof(GenericType<>))]

Атрибут [DynamicDependency] предназначен для использования в случаях, когда метод содержит шаблоны отражения, которые не могут быть проанализированы даже с помощью метода DynamicallyAccessedMembersAttribute.