На стр. 96 документа Pro .NET Performance — Optimize Your C# Applications говорится о сборе корневого каталога GC:

Для каждой локальной переменной JIT встраивает в таблицу адреса самых ранних и последних указателей инструкций, где переменная все еще актуальна как корень. Затем GC использует эти таблицы при обходе стека.

Затем он предоставляет этот пример:

    static void Main(string[] args)
    {
        Widget a = new Widget();
        a.Use();
        //...additional code
        Widget b = new Widget();
        b.Use();
        //...additional code
        Foo(); //static method
    }

Затем он говорит:

Приведенное выше обсуждение подразумевает, что разбиение вашего кода на более мелкие методы и использование меньшего количества локальных переменных — это не просто хорошая конструктивная мера или метод разработки программного обеспечения. .NET GC также может обеспечить выигрыш в производительности, поскольку у вас меньше локальных корней! Это означает меньше работы для JIT при компиляции метода, меньше места, занимаемого корневыми IP-таблицами, и меньше работы для GC при обходе стека.

Я не понимаю, как может помочь разбиение кода на более мелкие методы. Я разбил код на это:

    static void Main(string[] args)
    {
        UseWidgetA();
        //...additional code
        UseWidgetB();
        //...additional code
        Foo(); //static method
    }

    static void UseWidgetA()
    {
        Widget a = new Widget();
        a.Use();
    }

    static void UseWidgetB()
    {
        Widget b = new Widget();
        b.Use();
    }
}

Меньше местных корней:

Почему местных корней меньше? Количество локальных корней осталось прежним, по одному в каждом методе.

Меньше работы для JIT при компиляции метода:

Конечно, это ухудшит ситуацию, потому что для двух дополнительных методов потребуются 2 дополнительные таблицы. JIT также по-прежнему должен будет записывать самые ранние и последние указатели инструкций, где переменная все еще актуальна в каждом методе, но у него просто будет больше методов для этого.

Меньше работы для сборщика мусора при обходе стека:

Как наличие более мелких методов означает меньшую работу сборщика мусора во время обхода стека?

4
David Klempfner 17 Дек 2019 в 13:29
Это довольно эзотерическая оптимизация. Как правило, небольшие методы — это неплохо, однако причина, по которой вы можете разделить этот примерный код таким образом, заключается в том, что вы не используете переменные на следующих шагах, поэтому вы можете ограничить их область действия более мелкими методами. Вы могли бы добиться аналогичного эффекта, используя фигурные скобки в одном методе, но, как правило, JIT достаточно умен, чтобы распознавать неиспользуемые переменные и исключать их из корней GC. См. эти вчерашние вопросы и ответы, в которых обсуждается именно это. Так что только для производительности GC вам не нужно разбивать метод.
 – 
Holger
17 Дек 2019 в 17:43

1 ответ

Я не в мыслях Саши, но позвольте мне добавить к этому свои пять копеек.

Во-первых, я воспринимаю это как обобщенное правило — когда вы разбиваете метод на более мелкие, есть шанс, что какие-то части не нужно будет JIT-ить, потому что некоторые подпрограммы выполняются условно.

Во-вторых, JITting действительно создает так называемую информацию GC о корнях живого стека. Чем больше метод, тем больше информация GC. Теоретически должны быть большие затраты на их интерпретацию во время GC, однако это преодолевается путем разделения информации GC на фрагменты. Однако информация о живучести корней стека сохраняется только для так называемых безопасных точек. Существует два типа методов:

  • частично прерываемый — единственные безопасные точки — во время вызовов других методов. Это делает метод менее «приостанавливаемым», поскольку среде выполнения необходимо дождаться такой безопасной точки, чтобы приостановить метод, но потребляет меньше памяти для информации GC.
  • полностью прерываемый - каждая инструкция метода рассматривается как безопасная точка, что, очевидно, делает метод очень "приостанавливаемым", но требует значительного объема памяти (количества, аналогичного самому коду)

Как сказано в Book Of The Runtime: «JIT выбирает, выдавать ли полностью или частично прерываемый код, основанный на эвристике, чтобы найти наилучший компромисс между качеством кода, размер информации GC и задержка приостановки GC».

На мой взгляд, меньшие методы помогают JIT принимать лучшие решения (на основе его эвристики), чтобы сделать методы частично или полностью прерываемыми.

4
Konrad Kokosa 17 Дек 2019 в 14:42