Inline и throw

Picture from mobillegends.net about inline functions

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: 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) того, что это положительно скажется на работе приложения.

P.S.: Начал писать в телегу про производительность. Заглядывайте, если интересно.

© Habrahabr.ru