Обновить

Изучал коммиты, которые попали в релиз Go 1.27 и наткнулся на ряд улучшений, которые не отобразили в release notes.

Интересующие нас коммиты в этом посте: f4bfb1a9c6, 9be7615aa2, 5af294bac7, 84e0c4965a, 1bcfdf2df2.

Главное: теперь на heap перестанут утекать аргументы переданные в замыкания, если компилятор сможет статически определить, какая функция будет вызвана. Проблема была в том, что компилятор не знал, кто именно будет вызван, и консервативно считал, что все аргументы вызова утекают в heap.

Давайте посмотрим на пару примеров:

func Apply() int {
	var f func([]int) int
	f = func(p []int) int { return p[0] * 2 }
	arr := make([]int, 2)
	return f(arr)
}

Пример достаточно вычурный, но явно показывает разницу.

Бенчмарки: Go 1.26:

BenchmarkApply-10                	93288418	        11.95 ns/op	      16 B/op	       1 allocs/op

Go 1.27:

BenchmarkApply-10                	590856250	         2.028 ns/op	       0 B/op	       0 allocs/op

Компиляция с -gcflags=-m сразу укажет на причину.

Go 1.27:

./demo.go:26:6: func literal does not escape
./demo.go:27:13: make([]int, 2) does not escape

Go 1.26:

./demo.go:26:6: func literal does not escape
./demo.go:27:13: make([]int, 2) escapes to heap

Но не все так просто. У меня есть и контрпример, когда escape analysis промахивается:

var g func(*int) int = func(p *int) int { return *p * 3 }

func ApplyUnresolvable() int {
	var f func(*int) int
	f = func(p *int) int { return *p * 2 }
	f = g
	return f(new(int))
}

А в этом примере аргумент замыкания f убежит на хип, потому что в момент вызова f(new(int)) в f может лежать любое из присвоенных значений. Значение g компилятор доказать не может — это переменная уровня пакета, её содержимое escape analysis не отслеживает, то есть «аргумент может утечь куда угодно». Например, что если g будет запускать goroutine внутри. Поэтому тут компилятор выбирает консервативное поведение.

Теперь я покажу, как с переходом с 1.26 на 1.27 можно получить почти восьмикратное ускорение!

type Node struct {
	Val      int
	Children []*Node
}

func Sum(root *Node) int {
	var visit func(*Node) int
	visit = func(n *Node) int {
		t := n.Val
		for _, c := range n.Children {
			t += visit(c)
		}
		return t
	}
	return visit(root)
}

func BuildAndSum(v int) int {
	root := &Node{Val: v, Children: []*Node{{Val: 1}, {Val: 2}}}
	return Sum(root)
}

Здесь ни один экземпляр Node не утечет на хип! Смотрим на бенчмарки. Go 1.26

BenchmarkBuildAndSum-10          	17728094	        66.86 ns/op	     112 B/op	       4 allocs/op

Go 1.27

BenchmarkBuildAndSum-10          	148023318	         8.561 ns/op	       0 B/op	       0 allocs/op

Как и обещал ускорение примерно в 8 раз :)

P.S. Вообще удивительно как интересные вещи пропускают в release notes. Я нашел как минимум еще три изменения, которых нет в release notes, но прямо влияющих на перф. Одна из них так вообще прямо влияет на то как мы пишем бенчмарки! Но об этом в другой раз ;)

Если вам понравилась статья и вы хотите узнать про три других изменения, то заглядывайте в мой Telegram-канал.

Теги:
0
Комментарии0

Криптографы показали практическую подделку RSA‑подписей без факторизации ключа

Новое исследование представило метод, использующий классические вычислительные мощности для снижения текущего уровня безопасности RSA до неприемлемо низкого предела. Применение атаки к устаревшим 1024-битным ключам заняло всего несколько месяцев на академическом кластере процессоров.

Криптографы показали практическую подделку RSA‑подписей без факторизации ключа

Публикации