Интересни имплементационни детайли в Go
10.01.2017
Въпроси за мъфини
Въпрос за мъфин #1
Как използваме C в Go?
- import-ваме несъществуващия пакет "C"
- в коментари преди това пишем C код и препроцесорни директиви
- от Go кода извикваме функциите с
C.nesho_si()
- стискаме палци и зъби
Въпрос за мъфин #2
Как може да видим errno
на C функция, която сме извикали?
- чрез втория резултат на функцията
- той ще бъде различен от
nil
ако errno
е бил вдигнат
Въпрос за мъфин #3
За какво се използва unsafe
пакета?
- ако не внимаваме, за застрелване в поне единия крак
- ако много внимаваме, за аритметика с указатели и за заобикаляне на типовата система на Go
Въпрос за мъфин #4
Как може да променим броя процесорни ядра, на които се изпълнява нашата Go програма?
- чрез environment променливата
GOMAXPROCS
- чрез функцията
GOMAXPROCS()
от runtime
пакета
Но преди това
- Първа незадължителна защита на проектите - 17.01 (следващия път)
- Вижте в новината на сайта какво точно очакваме
- 18.02.2017 (събота) от 10:00ч. до 15:00ч. в зала 306 ще е реалната защита
- 19.02.2017 (неделя) от 14:00ч. до 17:00ч. в зала 326 ще дадем втория тест и ще пишем оценките
Излъгахме че сме показали целият език
- Последната част, която остана да преподадем
- Супер важно, пет нови ключови думи!!
notwithstanding
, thetruthofthematter
, despiteallobjections
, whereas
, insofaras
- Абсолютно валидни и запазени ключови думи... които не правят нищо
package main
import "fmt"
func main() {
notwithstanding var a int = 10
whereas var b int = 20
if despiteallobjections a < b && insofaras a != 3 {
fmt.Printf("hello, world\n")
}
}
go build
-x го кара да принтира какво всъщност прави:
-> go build -x github.com/Vladimiroff/vec2d
WORK=/tmp/go-build495023012
mkdir -p $WORK/github.com/Vladimiroff/vec2d/_obj/
mkdir -p $WORK/github.com/Vladimiroff/
cd /home/mstoykov/workspace/go/src/github.com/Vladimiroff/vec2d
/home/mstoykov/workspace/goroot/go/pkg/tool/linux_amd64/compile
-o $WORK/github.com/Vladimiroff/vec2d.a -trimpath $WORK
-p github.com/Vladimiroff/vec2d -complete
-buildid 577672012d89431ef32536b66f5254f5f6f49189
-D _/home/mstoykov/workspace/go/src/github.com/Vladimiroff/vec2d -I $WORK
-pack ./utils.go ./vector.go
Стойности
- Обикновена стойност
- Заема точно 4 байта
- В Python са 24
- В Java са 4, докато не решите да го сложите в List или Map
- В Go винаги можем да определим големината на нещо
CPU Cache
type Location struct {
// float64 -> 8 bytes
X, Y, Z float64
// 24 bytes in total
}
var Locations [1000]Location
// 24 * 1000 bytes stored *sequentially*
- Структурите са компактни
- Без излишна индирекция
- При полета с различна големина, нещата стават по сложни
- Ползвайте tool-ове
github.com/opennota/check
golang-sizeof.tips
=> По-добро използване на кеша
Извикване на функция
- Създава се нов стек фрейм
- Записан е return адресът на този, който е извикал функцията
- Всеки регистър, в който се пише от тази функция, трябва да бъде запазен предварително
- Смята се адресът на функцията и се изпълнява
Бе върши се доста работа по темата.
Пример
package util
func Max(a, b int) int {
if a > b {
return a
}
return b
}
---
package main
func double(a, b int) int {
return 2 * util.Max(a, b)
}
Inlining
package main
func double(a, b int) int {
max := b
if a > b {
max = a
}
return 2 * max
}
Dead code elimination
- Тривиална оптимизация, която прави компилаторът
- Недостъпни парчета изобщо не биват компилирани
func LargeAndComplicatedAction() {
if false {
[...]
}
[...]
}
- Да, де, ама колко често правим такива неща?
Debug checks
func DebugChecks() bool {
return debugBuildTagIsOn
}
func LargeAndComplicatedAction() {
if DebugChecks() {
[...]
}
[...]
}
- Това изглежда адекватно, ама сега компилаторът не може да е сигурен, че това е "мъртъв" код
- Inlining :)
Escape analysis
Кое къде отива в С, примерно?
- дефинирана променлива в скоупа на функция отива на стека
malloc
отива на хийпа
Толкова е просто. Хайде сега на Go
Stupid Sum
func SumFirst100() int {
numbers := make([]int, 100)
for i := range numbers {
numbers[i] = i + 1
}
var sum int
for _, i := range numbers {
sum += i
}
return sum
}
Cursor
type Cursor struct {
X, Y int
}
const width, height = 640, 480
func Center(c *Cursor) {
c.X += width / 2
c.Y += height / 2
}
func CenterCursor() (int, int) {
c := new(Cursor)
Center(c)
return c.X, c.Y
}
Компилаторът на Go е комуникативно същество
-> go build -gcflags=-m esc.go
# command-line-arguments
./esc.go:9: can inline Center
./esc.go:14: can inline CenterCursor
./esc.go:16: inlining call to Center
./esc.go:9: Center c does not escape
./esc.go:15: CenterCursor new(Cursor) does not escape
./esc.go:22: SumFirst100 make([]int, 100) does not escape
Да си поговорим за горутини
- Цял семестър ви обясняваме колко са малки, бързи и евтини за създаване.
- Нека ги разгледаме отвътре
Cooperatively scheduled
- chan send/receive
- go ...()
- syscall
- gc
Cooperatively scheduled
Да си поговорим за C
Сценарий: Изпълняваме thread на C.
- Стандартната библиотека ще алокира памет за него
- Ще съобщи на kernel-а за нея
- От тук приема, че вече kernel-а знае какво да прави с тази памет
- Всичко е песен
Unless...
int ack(int m, int n)
{
if (m == 0) {
return n + 1;
} else if (m > 0 && n == 0) {
return ack(m - 1, 1);
} else {
return ack(m - 1, ack(m, n - 1));
}
}
ack(4, 5);
// segmentation fault
Рекурсия. Свърши ни паметта на стека.
Как можем да го решим това?
- Да заделяме по-голям стек за всяка функция
Да, това ще понася по-дълбока рекурсия, но пък ще заделяме повече памет за
АБСОЛЮТНО всяка функция, която тя може да не използва.
- Да вземаме решение колко голям да бъде стекът
Ще знаем точно колко голям стек да заделяме за всяка функция, но този анализ ще отнема време, което води до бавна компилация с много налучкване или бавно изпълнение на всяка функция
Адресно пространство
Guard page
Thread stacks and guard pages
Segmented Stacks
Това е начинът, по който Go до версия 1.3 управляваше стековете.
- При създаване на горутина се заделя 8kb и тя работи с тях
- Става интересно, когато те свършат
- Няма Guard page-ове
- При всяко влизане във функция се прави проверка, дали стекът е запълнен
- Ако това е така и все още има свободна памет, извиква
morestack
morestack
заделя нова памет за стек
- На дъното на новия стек заделя една структура с информация за стария стек (включително адресът му)
- Рестартира последната функция, която е запълнила стека
Stack Split
+---------------+
| |
| unused |
| stack |
| space |
+---------------+
| Foobar |
| |
+---------------+ +---------------+
| | +-->| Foobar |
| lessstack | | | |
+---------------+ | +---------------+
| Stack info |---+ | rest of stack |
| | | |
+---------------+ +---------------+
lessstsack
- Функция, която стои заложена, в края на стека
- Когато стигнем до нея, чете информацията за предния стек
- Намества стек пойнтъра към предишния сегмент
- След това спокойно можем да деалокираме този нов стек
Супер яко, нали?
- Нямаме максимален размер на стека
- Което ни дава възможността всяка горутина да е с максимално малък стек
- Това е едно от нещата, което прави стартирането на нови горутини евтино
- Горутините, които порастнат твърде много се грижат да почистят след себе си
Но има и проблеми
- Деалокиране на стек е скъпа операция
Stack copying
- Почти като сегментираните стекове, но не прави нов с информация за стария
- Вместо това прави нов двойно по-голям стек и копира първия в него
- Старият стек вече може да бъде затрит
- Ако новият случайно намалее, няма нужда да правим нищо
- Ако той пак порастне, отново няма нужда да правим нищо
- От 1.4 правим точно така за почти всичко
- От 1.5 за всичко
- Възможно е да бъдат намалени по време на GC
Канали
Отдолу имплементацията на кратко (и значително опрoстено):
- цикличен буфер за стойностите на канала
- една опашка с писари
- една опашка с четци
- един lock
- един bool дали е затворен
- малко допълнителни неща за по-бърза (грозна) имплементация
Бързи операции
Били са открити някои неща, които се случват МНОГО често. Те са били по оптимизирани.
- проверка дали даден канал има стойност (select-ите) за четене
- проверка дали даден канал може да получи стойност (select-ите)
- четене от затворен канал
- chan struct{}-овете които може да ползвате като семафори
- еднонишково четене/писане в канал
- предложение
- пък и малко код
Паралелен GC
- В последните версии GC на Go е намалил паузите до страхотно ниски времена
- По идея на Edsger W.Dijkstra (разбира се!), Leslie Lamport, A.J.Martin, C.S.Scholten и E.F.M.Steffens
- Обяснения в математика
- И в картинки
Яки коментари
Поради нужди на имплементацията на Go има едно известно количесто ... 'яки' коментари.
Те са такива защото правят неща които няма как да се направят с другите части на езика и същевремнно лъхат на развалено кисело зеле.
- //go:linkname reflect_makechan reflect.makechan е черна магия която прави извикването на (празната) makechan фунцкия в пакета reflect да вика (не празната) фунцкия reflect_makechan в текущия.
- //line path/to/file:number казва че следващия ред е от файла 'path/to/file' и е номер 'number', следващите редове инкрементират 'number'.
- ...
Яки коментари (2)
- //go:nosplit казва при извикването на следващата функция да не се проверява дали няма да излезе извън стека. Това се използва от функции чиито изпълнение не трябва да бъде прекъсвано от смяна на изпълнимата горутина.
- //go:noescape казва че параметрите на следващата (празна, иплементирана в C) функция могат да са на стека(не избягват)