Dev48
ЯЗЫК
  • О нас
  • Услуги
  • Индустрии
  • Технологии
  • Статьи
  • Контакты
Забронировать звонок
    Главная/Статьи/Vydelenie pamyati spetsializirovannoe po razmeru
Dev48

© 2026 · All rights reserved.

Выделение памяти, специализированное по размеру

Источник: Go

Выделение памяти, специализированное по размеру

Источник: Go

Go 1.27 повышает производительность мелких выделений памяти с помощью специализированных по размеру функций выделения.

25 сентября 2026 г.

Go 1.27 включает более быстрое выделение памяти для блоков размером 80 байт или меньше. Выделение памяти может быть быстрее на 20-30%, что ускоряет программы с интенсивным выделением памяти до 1%. Среда выполнения Go повышает производительность такого выделения памяти за счет добавления специализированных функций, которые используются для выделения определенных размеров. Эти специализированные функции могут делать определенные предположения, которые делают их более быстродействующими и простыми для оптимизации. В этой статье блога будет объяснено, как это работает и как это ускоряет ваши программы.

Выделения в куче создаются функцией mallocgc среды выполнения, которой требуется размер выделяемого блока и информация о том, содержит ли он указатели. Когда компилятор определяет, что объект попадает в кучу или иным образом требует динамического выделения, он вставляет вызов newobject — простой функции-обертки, которая извлекает размер объекта и информацию о наличии указателей, а затем передает их в mallocgc.

Эти два фрагмента информации определяют большую часть работы, которую должен выполнить аллокатор. Размер важен, потому что аллокатор определяет диапазоны размеров, называемые «классами размеров». Для большинства выделений памяти, которые не слишком велики и не слишком малы, он возвращает блок памяти из списка свободных объектов, размер которых равен максимальному размеру класса размера. Так, например, класс размера 3 охватывает 17-24 байта. Поэтому, выделяете ли вы 17 байт или 24 байта, аллокатор выдаст вам следующий доступный свободный 24-байтный объект из своего списка 24-байтных объектов. Ниже приведена таблица диапазонов размеров для каждого из классов размеров до 80 байт. Существуют отдельные наборы списков свободных объектов, которые мы называем диапазонами (spans), в зависимости от того, содержит ли выделяемый блок указатели, из-за учета, который нам необходимо вести для сборщика мусора.

Поскольку класс размера и наличие указателей в выделяемом блоке определяют, из какого диапазона мы выделяем память, аллокатор имеет тип, называемый классом диапазона (span class), значение которого кодирует и то, и другое: оно определяется как sizeClass<<1 | noPointers. При реализации специализированного по размеру malloc стало очевидно, что имеет наибольший смысл специализировать именно по этим классам диапазонов, поскольку большая часть поведения аллокатора определяется классом диапазона.

Мы генерируем специализированный вариант mallocgc для каждого класса диапазона. Например, функция, которая выделяет объекты без указателей в классе размера 3, называется mallocgcSmallNoScanSC3. Small означает не крошечный и не большой, NoScan означает отсутствие указателей, а SC3 означает класс размера 3 (17-24 байта). Специализированные функции разработаны так, чтобы быть как можно более простыми, и не могут обрабатывать каждый краевой случай во время выделения. Когда они обнаруживают такой случай, например, когда активен сборщик мусора, они возвращаются к более общей процедуре выделения.

В итоге мы получаем новую специализированную функцию mallocgc для крошечного случая (всегда без указателей) и по одной для каждого класса некрошечного диапазона: диапазоны без указателей для класса размера 2 и выше, и диапазоны с указателями для класса размера 1 и выше. Сами эти специализированные варианты mallocgc работают быстрее, чем вызов mallocgc, но преимущества специализации уменьшаются по мере увеличения размеров: в выделении памяти часто начинает преобладать необходимость очистки памяти, и в какой-то момент остальная работа по выделению становится незначительной. И недостаточно просто быть быстрее, чем mallocgc! При выделении памяти, специализированном по размеру, когда компилятор знает, для какого класса диапазона ему нужно выделить память, он может напрямую вставить вызов специализированной функции вместо вызова newobject. Но компилятор часто не знает, какой класс диапазона выделяется при генерации кода: подумайте о срезах динамической длины. Поскольку компилятор не может определить размер выделения, он сохраняет вызов mallocgc, и сам mallocgc должен определить, доступна ли специализированная функция, какая именно, а затем вызвать ее. Поэтому производительность специализированной функции должна быть достаточно высокой, чтобы даже с накладными расходами на динамический вызов она все равно работала быстрее.

Проблема с накладными расходами — не единственная сложность. Если бы дело было только в ней, мы могли бы создать более крупные специализированные функции и зарезервировать их для вставки компилятором, когда размер известен во время компиляции. И если бы мы не вызывали их динамически, нам не пришлось бы платить за динамические накладные расходами. Но каждая добавленная специализированная функция увеличивает размер исполняемых файлов, создаваемых компилятором, и, что более важно, занимает драгоценное пространство кэша инструкций. Единственная функция mallocgc часто находится в кэше инструкций из-за того, как часто происходят выделения памяти. Если у нас слишком много специализированных функций и они недоступны в кэше, накладные расходы на извлечение специализированного кода в кэш могут свести на нет все преимущества. И чем больше специализированного кода выделения находится в кэше инструкций, тем сильнее он вытесняет пользовательский код, замедляя его загрузку и выполнение. Проведя серию бенчмарков, останавливаясь на разных классах размеров, мы определили, что остановка на 80 байтах является оптимальным решением.

Конечно, добавление новых специализированных функций означает больший объем кода для поддержки. Если бы каждая функция была написана вручную, код в специализированных функциях мог бы легко разойтись и рассинхронизироваться. Чтобы помочь с этим, мы выделили общие части специализированных функций и написали инлайнер, используя пакет стандартной библиотеки go/ast для синтаксического анализа и форматирования и пакет golang.org/x/tools/go/ast/astutil для манипулирования AST (абстрактными синтаксическими деревьями). Общие части функций написаны на стандартном коде Go, который собирается и проверяется на типы вместе с остальной частью среды выполнения, чтобы наши инструменты могли обнаруживать проблемы, но в основном они представляют собой просто заглушки для инлайнера.

Таким образом, нам удалось замерить улучшения в специализированных по размеру функциях выделения памяти, но почему они действительно работают быстрее? Наиболее очевидная оптимизация заключается в очистке памяти. Память, возвращаемая mallocgc, не всегда должна быть обнулена, но часто это необходимо, и это может занимать большую часть времени выделения. Функция очистки памяти memclrNoHeapPointers написана на высокооптимизированном ассемблере, но мы могли бы сделать лучше для очистки блоков меньшего размера. В специализированной функции, если размер очистки является константой, компилятор может заменить вызовы memclrNoHeapPointers кодом для непосредственной генерации инструкций по очистке памяти. В этом случае выделение памяти может пропустить вызов функции и некоторые ветвления. Это имеет значение для действительно мелких выделений памяти, но по мере их увеличения накладные расходы на вызов функции становятся незначительными.

Хотя более быстрое очищение памяти обеспечивает наибольшее улучшение, в специализированных функциях возможны и другие оптимизации: поскольку они специализированы для каждого класса диапазонов (span-class), функции не нужно вычислять класс диапазона при получении самого диапазона. А поскольку размер выделения является константой, компилятор может выполнять определенные оптимизации для ускорения учета, необходимого для выделения памяти. Одним из таких примеров является разметка расположения указателей в выделенной памяти. Специализированные функции также могут вручную встраивать (inline) несколько своих вспомогательных функций. Компилятор Go может встраивать код, но он избегает встраивания функций, которые считает слишком большими. Мы можем переопределить это поведение в сгенерированном коде, вставив тела функций прямо в вызывающий код. С помощью генератора мы можем создавать копии каждого из тел без необходимости беспокоиться о том, что эти копии разойдутся со временем. И мы могли бы перенести код, обрабатывающий менее распространенные сценарии (например, флаги отладки во время выполнения), в функции медленного пути (slow path), чтобы сделать специализированные функции меньше.

Хотя мы надеемся, что это объяснение выделения памяти с учетом конкретных размеров показалось вам интересным, вам как программисту на Go не нужно думать ни о чем из этого при написании своего кода. Выделение памяти станет просто немного быстрее, причем наибольшую выгоду получат программы с наиболее распространенными размерами выделения, особенно размерами в 16 и 24 байта. Эти размеры являются одними из самых частых, поскольку они состоят из двух или трех 64-битных значений и включают в себя выделение памяти для таких вещей, как значения интерфейсов и строки, состоящие из двух значений, или срезы, состоящие из трех. Мы потратили много времени на настройку поведения специализированного под размер аллокатора и обеспечение минимального влияния эффектов кэша инструкций: изначально мы планировали выпустить это нововведение в Go 1.26, но решили подождать еще один релиз, чтобы провести дополнительную настройку и максимально сократить размер дополнительного кода.

Все, что вам нужно сделать для получения повышенной производительности от выделения памяти, специализированного по размеру, — это собрать свои программы с помощью Go 1.27. Если вас интересуют более конкретные шаги, которые вы можете предпринять для улучшения производительности выделения памяти и сборки мусора, пожалуйста, ознакомьтесь с Руководством по оптимизации сборщика мусора в Go.

Хотя мы уверены, что выделение памяти, специализированное по размеру, не должно приводить к регрессиям в вашем коде, при необходимости вы можете собрать свои программы, используя GOEXPERIMENT=nosizespecializedmalloc, чтобы отключить эту функцию. Если вам действительно пришлось сделать это для решения проблемы, связанной с новым аллокатором, пожалуйста, сообщите о ней на go.dev/issue/new, чтобы мы могли ее исследовать.

← Все статьи