Когда я недавно исследовал оптимизацию производительности, я увидел документ в пакете времени выполнения golang.HACKING.md
Мне он показался довольно интересным, после прочтения я почувствовал, что стал лучше понимать рантайм, поэтому задумался о его переводе.
Содержание этой главы будет иметь определенную глубину и потребует от читателей определенной подготовки, поскольку из-за ограничений по объему здесь невозможно полностью раскрыть каждую деталь.
Это документ, предназначенный для целевой аудитории разработчиков, поэтому у нас есть много контента для общего использования.
Этот документ будет часто редактироваться, и со временем текущее содержание может устареть. Этот документ предназначен для иллюстрации кода среды выполнения и написания кода, который обычно работает по-разному, поэтому вместо того, чтобы сосредоточиться на некоторых деталях реализации некоторых общих концепций.
структура планировщика
Планировщик управляет тремя типами, важными во время выполнения:G
,M
иP
. Даже если вы не пишете код, связанный с планировщиком, вы должны понимать эти концепции.
Г, М и П
ОдинG
Просто горутин, через тип во время выполненияg
Представлять. Когда горутина завершает работу,g
Объект будет размещен в свободномg
Пул объектов для последующего использования goroutine (Примечание переводчика: уменьшите накладные расходы на выделение памяти).
ОдинM
Это системный поток, который может выполнять пользовательский код запуска, код времени выполнения, системный вызов или ожидание бездействия. типы проходов во время выполненияm
Представлять. В то же время может быть любое количествоM
, так как любое количествоM
Может заблокировать системный вызов. (Примечание переводчика: когдаM
При выполнении блокирующего системного вызоваM
иP
отвязать и создать новыйM
выполнитьP
другой наG
. )
последнийP
Представляет ресурсы, необходимые для выполнения пользовательского кода перехода, такие как состояние планировщика, состояние распределителя памяти и т. д. типы проходов во время выполненияp
Представлять.P
Количество в точности (в точности) равноGOMAXPROCS
. ОдинP
Его можно понимать как ЦП в планировщике операционной системы,p
Тип можно понять как состояние каждого ЦП. Здесь вы можете поставить некоторых, которые необходимо эффективно поделиться, но не для каждогоP
(заP
) или каждыйM
(заM
) состояние (Примечание переводчика: это означает, что вы можете поставить некоторыеP
общие данные уровня).
Задача планировщика состоит в том, чтобы поставитьG
(код для выполнения), aM
(где выполняется код) иP
(разрешения и ресурсы, необходимые для выполнения кода) объединены. когдаM
При прекращении выполнения пользовательского кода (например, при вводе блокирующего системного вызова) необходимо поместить егоP
вернуться на свободуP
В пуле; чтобы продолжить выполнение пользовательского кода перехода (например, при выходе из блокирующего системного вызова), ему необходимо начать с незанятогоP
получить один из бассейнаP
.
всеg
,m
иp
Объекты размещаются в куче и никогда не освобождаются, поэтому их использование памяти стабильно. Благодаря этому среда выполнения может избежать барьеров записи в реализации планировщика.
Пользовательский стек и системный стек
каждый не мертвыйG
Там будет связанный пользовательский стек, и код пользователя выполняется в этом стеке пользователя. Стеки пользователя начинаются маленькие (скажем, 2К) и растут или усаживаются динамически.
КаждыйM
иметь связанный системный стек (также известный какg0
стек, потому что этот стек также проходит черезg
реализован); если на платформе Unix, есть такжеsignal
стек (также известный какgsignal
куча). системный стек иsignal
Стек не может расти, но он достаточно велик для запуска любой среды выполнения и кода cgo (8 КБ в чистом двоичном коде go, выделенных системой в случае cgo).
код времени выполнения часто вызываетсяsystemstack
,mcall
илиasmcgocall
Временно переключитесь на системный стек для выполнения некоторых специальных задач, таких как те, которые не могут быть вытеснены, те, которые не должны расширять пользовательский стек, и те, которые переключают пользовательские горутины. Код, работающий в системном стеке, неявно не может быть вытеснен, и сборщик мусора не сканирует системный стек. когдаM
При работе в системном стеке текущий пользовательский стек не выполняется.
getg()
иgetg().m.curg
Если вы хотите получить текущий пользовательg
, Необходимо использоватьgetg().m.curg
.
getg()
Хотя он вернет текущийg
, но когда системный стек илиsignal
При выполнении в стеке он вернет текущийM
изg0
илиgsignal
, скорее всего, не то, что вы хотите.
Если вы хотите определить, выполняется ли он в данный момент в системном стеке или в пользовательском стеке, вы можете использоватьgetg() == getg().m.curg
.
Обработка ошибок и отчетность
В пользовательском коде есть несколько разумно исправимых ошибок, которые можно использовать как обычно.panic
, но в некоторых случаяхpanic
Это может привести к немедленным фатальным ошибкам, таким как вызов или выполнение в системном стеке.mallocgc
Время.
Большинство ошибок времени выполнения неисправимы, и для этих неисправимых ошибок вы должны использоватьthrow
,throw
распечатаетtraceback
и немедленно завершить процесс.throw
Должна быть передана строковая константа, чтобы в этом случае также не нужно было выделять память для строки. По договоренности дополнительная информация должна бытьthrow
использовался раньшеprint
илиprintln
распечатаны и должны начинаться сruntime.
начало.
Чтобы выполнить отладку ошибок во время выполнения, очень практичным методом является установкаGOTRACEBACK=system
илиGOTRACEBACK=crash
.
Синхронизировать
В среде выполнения существуют различные механизмы синхронизации, которые отличаются не только семантически, но и взаимодействием между планировщиком go и планировщиком операционной системы.
Самый простойmutex
,можно использоватьlock
иunlock
работать. Этот метод в основном используется для защиты некоторых общих данных в краткосрочной перспективе (и низкой производительности в долгосрочной перспективе). существуетmutex
Блокировка вверх будет напрямую блокировать весьM
, без взаимодействия с планировщиком go. Поэтому на самом низком уровне во время выполнения используйтеmutex
безопасен, потому что он также блокирует связанныеG
иP
перенесено(M
заблокированы и не могут выполнять планирование).rwmutex
Тоже похоже.
Если вы хотите сделать однократное уведомление, вы можете использоватьnote
.note
при условииnotesleep
иnotewakeup
. в отличие от традиционного UNIXsleep/wakeup
,note
не зависит от расы, поэтому, еслиnotewakeup
случилось, тоnotesleep
вернется немедленно.note
можно использовать послеnoteclear
для сброса, но будьте осторожныnoteclear
иnotesleep
,notewakeup
Соревнование не может состояться. похожийmutex
, заблокирован вnote
заблокирует весьM
. Тем не мение,note
обеспечивает разные способы позвонитьsleep
:notesleep
предотвратит связанныеG
иP
быть перенесенным;notetsleepg
ведет себя как блокирующий системный вызов, позволяяP
повторно используется для запуска другогоG
. Тем не менее, это все еще больше, чем прямая обструкцияG
быть неэффективным, потому что это потребляетM
.
Если вам нужно напрямую взаимодействовать с планировщиком go, вы можете использоватьgopark
иgoready
.gopark
Приостановить текущую горутину - превратить ее вwaiting
состояние и удаляется из очереди выполнения планировщика, а затем назначает другую горутину на текущуюM
илиP
.goready
Возобновить приостановленную горутину, чтобыrunnable
состояние и поставить его в очередь выполнения.
Он сведен в следующую таблицу:
Blocks | |||
---|---|---|---|
Interface | G | M | P |
(rw)mutex | Y | Y | Y |
note | Y | Y | Y/N |
park | Y | N | N |
атомарность
использование во время выполненияruntime/internal/atomic
Некоторые атомарные операции в себе есть. Это иsync/atomic
соответствуют, за исключением того, что имена методов несколько отличаются по историческим причинам, и есть некоторые дополнительные методы, требуемые средой выполнения.
В общем, мы очень осторожно относимся к использованию atomic во время выполнения и максимально избегаем ненужных атомарных операций. Если доступ к переменной уже защищен другим механизмом синхронизации, защищенный доступ обычно не обязательно должен быть атомарным. Делается это в основном по следующим причинам:
- Разумное использование неатомарных и атомарных операций делает код более удобочитаемым и удобочитаемым.Атомарная операция над одной переменной означает, что могут быть параллельные операции над этой переменной в другом месте.
- Неатомарные операции позволяют автоматически определять гонки. Сама среда выполнения в настоящее время не имеет детектора гонок, но может появиться в будущем. Атомарные операции заставят детектор гонки игнорировать проверку, но неатомарные операции могут пройти детектор гонки, чтобы проверить вашу гипотезу (не произойдет ли гонка).
- Неатомарные операции могут повысить производительность.
Конечно, все неатомарные операции над общей переменной должны документировать, как операция защищена.
Некоторые из наиболее распространенных сценариев смешивания атомарных и неатомарных операций:
- Большинство операций — это чтение, а запись — это переменные, защищенные блокировкой. В рамках защиты от блокировок операции чтения не обязательно являются атомарными, но операции записи должны быть атомарными. Операции чтения должны быть атомарными за пределами области, защищенной блокировкой.
- Операции чтения выполняются только во время STW, записи во время STW не производятся. Тогда в это время операция чтения не обязательно должна быть атомарной.
Было сказано, что,Go Memory Model
Совет, данный по-прежнему в силеDon't be [too] clever
. Производительность среды выполнения важна, но надежность еще важнее.
Неуправляемая память
При нормальных обстоятельствах времени выполнение пытается использовать общий подход к памяти приложения (память кучи, управление GC), но должен применить некоторую иностранную память кучи (неуправляемая память) не управляется GC в некоторых случаях выполнения выполнения. Это необходимо, потому что можно снять память, - это сам менеджер памяти, или вызывающий абонент не имеетP
(Примечание переводчика: например, до инициализации планировщика он не существуетP
из).
Есть три способа запросить память вне кучи:
-
sysAlloc
Чтобы получить память непосредственно из операционной системы, запрашиваемая память должна быть целым числом, кратным длине системной таблицы страниц. в состоянии пройтиsysFree
освободить. -
persistentalloc
Объединение нескольких небольших запросов памяти в один большойsysAlloc
Чтобы избежать фрагментации памяти (фрагментации). Однако, как следует из названия, кpersistentalloc
Приложение памяти не может быть освобождено. -
fixalloc
ЯвляетсяSLAB
Распределитель памяти стиля, который выделяет память фиксированного размера. пройти черезfixalloc
Выделенные объекты могут быть освобождены, но память может использоваться только тем же самымfixalloc
Бассейн используется повторно. такfixalloc
Подходит для объектов одного типа.
Обычно он используется для выделения типа памяти с использованием трех вышеуказанных методов и должен быть помечен как//go:notinheap
(увидеть ниже).
Объекты, выделенные в памяти вне кучине следуетСодержит объекты-указатели в куче, если не соблюдаются следующие правила:
- Все указатели на кучу из памяти вне кучи должны быть корнями сборки мусора. То есть все указатели должны быть доступны через глобальную переменную или явно с помощью
runtime.markroot
помечать. - Если память используется повторно, указатели в куче должны быть инициализированы нулями (см. ниже), прежде чем они будут помечены как корни GC и станут видимыми для GC. В противном случае сборщик мусора может обнаружить устаревшие указатели кучи. увидеть ниже
Zero-initialization versus zeroing
.
Zero-initialization versus zeroing
Существует два типа нулевых инициализированных при выполнении во время выполнения, в зависимости от того, была ли память инициализирована на безопасное состояние типа.
Если память не находится в типобезопасном состоянии, это означает, что она может содержать некоторые мусорные значения, потому что она только что была выделена и инициализирована в первый раз (Изучающие язык C должны быть в состоянии понять, что это значит), тогда этот кусок памяти должен быть использованmemclrNoHeapPointers
провестиzero-initialized
Или пишите без указателей. Это не вызывает барьер записи (Примечание переводчика: барьер записи — это концепция в GC).
память может бытьtypedmemclr
илиmemclrHasPointers
для записи нулевого значения, установленного в безопасное для типов состояние. Это вызывает барьер записи.
Директивы компилятора только во время выполнения (директивы компилятора)
Кромеgo doc compile
Отметил внутри//go:
В дополнение к директивам компиляции компилятор поддерживает некоторые дополнительные директивы в пакете среды выполнения.
go:systemstack
go:systemstack
Указывает, что функция должна выполняться в системном стеке, что динамически проверяется с помощью специального пролога функции.
go:nowritebarrier
go:nowritebarrier
Скажите компилятору вызвать ошибку, если следующая функция содержит барьер записи (это не предотвращает создание барьера записи, это просто гипотеза).
В общем, вы должны использоватьgo:nowritebarrierrec
.go:nowritebarrier
Используйте тогда и только тогда, когда "лучше не" писать барьеры, но это не обязательно для корректности.
Перейти: NOWRITEBARRIERREC и Перейти: YESWRITEBARRIERREC
go:nowritebarrierrec
сообщает компилятору, если следующая функция и функции, которые она вызывает (рекурсивно), пока неgo:yeswritebarrierrec
До сих пор, если включен барьер записи, срабатывает ошибка.
Логически компилятор будет генерировать граф вызовов из каждогоgo:nowritebarrierrec
функция запускается до тех пор, пока не встретитgo:yeswritebarrierrec
функция (или конец). Если одна из обнаруженных функций содержит барьер записи, генерируется ошибка.
go:nowritebarrierrec
В основном используется для реализации самого барьера записи, чтобы избежать бесконечного цикла.
Обе эти прагмы используются в планировщике. Барьеры записи требуют активногоP
(getg().m.p != nil
), однако код, связанный с планировщиком, может не иметь активногоP
в случае операции. при этих обстоятельствах,go:nowritebarrierrec
будет использоваться в некоторых выпускахP
или нетP
запустить функцию,go:yeswritebarrierrec
Будет повторно приобретенP
на коде. Поскольку это комментарии функционального уровня, отпуститеP
и получитьP
Код должен быть разделен на две функции.
go:notinheap
go:notinheap
Применяется к объявлениям типов, указывая, что тип не должен выделяться в куче сборщика мусора. В частности, указатели на этот тип всегда должны бытьruntime.inheap
Потерпел неудачу в суде. Этот тип может использоваться для глобальных переменных, переменных в стеке или объектов в памяти вне кучи (например, черезsysAlloc
,persistentalloc
,fixalloc
или другие управляемые вручнуюspan
进行分配)。 специальный:
-
new(T)
,make([]T)
,append([]T, ...)
и неявное дляT
Выделения в куче не разрешены (хотя неявные выделения никогда не разрешены во время выполнения). - указатель на обычный тип (кроме
unsafe.Pointer
) Не может быть преобразован в указывающийgo:notinheap
указатели типов, даже если они имеют один и тот же базовый тип. - любой содержит
go:notinheap
Тип самого типа такжеgo:notinheap
из. Если структуры и массивы содержатgo:notinheap
элементы, то они самиgo:notinheap
тип. карта и канал не разрешеныgo:notinheap
тип. Сделать вещи более понятными, любой неявнымgo:notinheap
типы должны быть явно отмеченыgo:notinheap
. - направление
go:notinheap
Барьеры записи для указателей типа можно игнорировать.
Последняя точкаgo:notinheap
Типа реальная выгода. Среда выполнения использует это в базовой структуре, чтобы избежать барьеров памяти для планировщика и распределителя памяти, чтобы избежать незаконных проверок или просто повысить производительность. Этот подход достаточно безопасен и не снижает удобочитаемости во время выполнения.
Автор: Чистый белый
Ссылка на эту статью:Woohoo.чистый белый.IO/2020/10/14/…
Уведомление об авторских правах. Все статьи в этом блоге распространяются по лицензии BY-NC-SA, если не указано иное. Пожалуйста, укажите источник!