Inline и throw

Inline и throw

C# heppard
Picture from mobillegends.net about inline functions

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

Вторая проблема - inline. Дело в том, что JIT по-умолчанию старается не инлайнить методы с throw, чтобы избежать сложной обвязки вокруг выброса исключения. Анализируя метод, JIT собирает некие метрики, из которого получается число. Если число превышает некий порог, то метод заинлайнен не будет. В случае с throw, это число сразу превышает порог.

Более глубоко о том, что такое inline методов, можно, например, посмотреть у девушки на кровати. Узнать, чем руководствуется JIT при inline'e методов можно, например, вот тут. Мы же сразу перейдём к benchmark'у.

Проверяем inline

Ситуация простая: у нас есть класс, в котором несколько абсолютно одинаковых методов. Ну, почти.

public sealed class InliningService {    
    private readonly int _min;
    ...
    
    [MethodImpl(MethodImplOptions.AggressiveInlining)]    
    public int AggressiveInline(int a, int b) {        
        if (a < _min || b < _min)
            throw new InvalidOperationException();        

        return a + b;    
    }

    public int AutoInline(int a, int b) {
        if (_min < 0 || b < _min) Errors.InvalidOperation();           
        return a + b;    
    }

    public int WithoutInline(int a, int b) {
        if (a < _min || b < _min) 
            throw new InvalidOperationException();
        return a + b;    
    }
}

В одном случае мы делаем throw прямо в методе (Without Inline), в другом случае мы просим всё-таки заинлайнить такой метод (Aggressive Inline), а в третьем случае мы полагаемся на JIT и вызов статического метода, в котором выбрасывается ошибка (Auto Inline).

Benchmark: inline method in C#

Глядя на этот benchmark, можно сделать несколько наблюдений.

Во-первых, скорость работы во всех трёх популярных версиях .NET примерно одинаковая. Странное увеличение времени работы метода без inline'a на .NET Core 3.1 я предлагаю списать на погрешность. Тем более, что размер IL-кода (Code Size) во всех трёх framework'ах одинаковый.

Во-вторых, скорость работы метода, который не был заинлайнен, предсказуемо ниже, чем версия, где JIT принял решение сделать inline. Причём почти в два раза. Это позволяет нам говорить о том, что нужно прятать throw в статический класс там, где выброс Exception будет дорогим и мы надеемся на inline.

В-третьих, колонка Code Size достаточно чётко намекает нам на то, что aggressive inline метода с throw в этом случае позволяет JIT сделать inline, но путём увеличения размера кода. По сравнению с AutoInline - разница драматичная. Подобный inline плох, поскольку повлиял бы на работу и возможность inline'a других методов, сделал бы невозможным inline тех методов, где это действительно важно.

Выводы

  1. Создайте статический класс а-ля Errors для выброса Exception'ов. Это стандартизирует выброс ошибок и сделает код чище.
  2. Методы класса Errors могут возвращать объектное представление сформированного Exception, но лучше, чтобы throw происходил прямо в методе этого класса. Введя подобную практику при написании кода, можно расширить возможности JIT'a по инлайну.
  3. Выброс Exception в критичном месте кода, где мы надеемся на inline - плохая идея, которая мешает JIT'у заинлайнить метод.
  4. Не надо баловаться с MethodImplAttribute, если вы не понимаете, как это работает и на что может повлиять. Используйте aggressive inline только тогда, когда вы имеете подтверждение (benchmark) того, что это положительно скажется на работе приложения.

Report Page