ℹ️ В статье речь идёт о Go 1.13.
Циклы в программировании — понятие мощное и довольно простое. Тем не менее возникает необходимость преобразовывать их в основные команды, которые может понять компьютер. Способ компиляции может повлиять и на другие компоненты стандартной библиотеки. Начнём с анализа цикла диапазона.
Ассемблерный код цикла
Цикл диапазона выполняет перебор массива, среза или канала. Вот пример функции, которая считает сумму чисел, используя цикличное выполнение на срезе:
func main() {
l := []int{9, 45, 23, 67, 78}
t := 0
for _, v := range l {
t += v
}
println(t)
}
Команда go tool compile -S main.go
выводит сгенерированный код на ассемблере, и вот какой дамп цикла диапазона мы имеем:
0x0041 00065 (main.go:4) XORL AX, AX
0x0043 00067 (main.go:4) XORL CX, CX
0x0045 00069 (main.go:7) JMP 82
0x0047 00071 (main.go:7) MOVQ ""..autotmp_5+16(SP)(AX*8), DX
0x004c 00076 (main.go:7) INCQ AX
0x004f 00079 (main.go:8) ADDQ DX, CX
0x0052 00082 (main.go:7) CMPQ AX, $5
0x0056 00086 (main.go:7) JLT 710x0058 00088 (main.go:11) MOVQ CX, "".t+8(SP)
Я разделил инструкции на две части: инициализация и сам цикл. Первые две инструкции инициализируют два регистра значением 0:
0x0041 00065 (main.go:4) XORL AX, AX
0x0043 00067 (main.go:4) XORL CX, CX
Регистр AX содержит текущую позицию в цикле, а в CX находится значение переменной t
. Вот визуальное представление с инструкциями и регистрами общего назначения:
Цикл начинается с инструкции JMP 82
, которая расшифровывается как “Jump to instruction 82” (то есть «перейти к инструкции 82»). Номер инструкции указан во втором столбце:
Следующая инструкция CMPQ AX, $5
расшифровывается как “Compare register AX and the value 5” («сравнить регистр AX и значение 5»). Она вычитает значения регистра DX из AX и сохраняет результат в другом регистре. Это значение можно использовать в следующей инструкции JLT 71
, расшифровывающейся как “Jump to instruction 71 if less than 0” («перейти к инструкции 71, если меньше 0»). Табличка теперь выглядит вот так:
Если условие не выполняется, перехода не происходит и программа продолжается на следующей инструкции после цикла.
Теперь у нас есть скелет цикла. А вот и цикл, преобразованный обратно в Go:
goto end
start:
?
end:
if i < 5 {
goto start
}
println(t)
Тело цикла отсутствует. Зато есть инструкции:
0x0047 00071 (main.go:7) MOVQ ""..autotmp_5+16(SP)(AX*8), DX
0x004c 00076 (main.go:7) INCQ AX
0x004f 00079 (main.go:8) ADDQ DX, CX
Первая инструкция MOVQ ""..autotmp_5+16(SP)(AX*8), DX
расшифровывается как “Move memory from source to destination” («переместить память от источника к месту назначения»). Она состоит из:
- среза
""..autotmp_5+16(SP)
, гдеSP
— это указатель стека (наш текущий фрейм памяти), аautotmp_*
— это автоматически генерируемое имя переменной; - смещения 8 (8 байтов в 64-разрядной архитектуре), помноженного на значение регистра AX и показывающего текущую позицию в цикле;
- назначения (здесь это регистр DX, в котором содержится текущее значение цикла).
Вторая инструкция INCQ
расшифровывается как “Increment” («инкремент») и добавляет текущую позицию цикла:
Последняя инструкция тела цикла ADDQ DX, CX
расшифровывается как “Add DX into CX” («добавить DX в CX»). Напомним, DX содержит текущее значение цикла, а CX — значение переменной t
:
Цикл остановится, когда счётчик цикла дойдёт до пяти. Затем инструкция, сразу после того как цикл укажет на регистр CX, передаст его значение в t
:
0x0058 00088 (main.go:11) MOVQ CX, "".t+8(SP)
Вот как выглядит конечное состояние цикла:
А так выглядит в конечной форме преобразование цикла в Go:
func main() {
l := []int{9, 45, 23, 67, 78}
t := 0
i := 0
var tmp int
goto end
start:
tmp = l[i]
i++
t += tmp
end:
if i < 5 {
goto start
}
println(t)
}
Генерирование ассемблерного кода для этой новой программы даст тот же результат.
Улучшения
То, как происходит преобразование цикла, может повлиять на другие характеристики Go, например на планировщик. В Go до версии 1.10 цикл компилировался так же, как этот код:
func main() {
l := []int{9, 45, 23, 67, 78}
t := 0
i := 0
var tmp int
p := uintptr(unsafe.Pointer(&l[0]))
if i >= 5 {
goto end
}
body:
tmp = *(*int)(unsafe.Pointer(p))
p += unsafe.Sizeof(l[0])
i++
t += tmp
if i < 5 {
goto body
}
end:
println(t)
}
Проблема с этой реализацией заключается в том, что указатель p проходит конец распределения, когда i доходит до пяти. Из-за этого цикл не так легко прерывается, так как его тело не защищено. Оптимизация компиляции цикла призвана обеспечить то, чтобы не создавалось указателя на элемент, следующий за последним элементом. Это улучшение сделано на случай автономного или пассивного вытеснения в планировщике Go.
Читайте также:
- Как работает функция Defer в Golang
- Полиморфизм с интерфейсами в Golang
- Удалённые вызовы процедур в Golang
Перевод статьи Vincent Blanchon: Go: How Are Loops Translated to Assembly?