Найти значительное узкое место в производительности стандартной библиотеки или зрелого приложения — это редкость.
Я был удивлён, когда в top10 списке CPU-профиля hugo при сборке digitalgov.gov на первой позиции находился метод reflect.Type.MethodByName().
flat flat% sum% cum cum% 8.84s 6.28% 6.28% 57.85s 41.10% reflect.(*rtype).MethodByName 7.93s 5.63% 11.92% 8.50s 6.04% reflect.name.readVarint 7.56s 5.37% 17.29% 111.79s 79.43% reflect.Value.call 7.53s 5.35% 22.64% 23.33s 16.58% runtime.mallocgc 7.29s 5.18% 27.82% 16.10s 11.44% reflect.name.name
В этой статье я расскажу вам о том, как так вышло и что с этим можно было бы сделать.
Почему MethodByName такой медленный
Давайте откроем исходники MethodByName:
func (t *rtype) MethodByName(name string) (m Method, ok bool) { // <лишний код вырезан> // TODO(mdempsky): Binary search. for i, p := range ut.exportedMethods() { if t.nameOff(p.name).name() == name { return t.Method(i), true } } return Method{}, false }
MethodByName использует линейный поиск. Если экспортируемых методов у типа много, то работать это может очень долго, особенно если искомый метод будет находиться где-то в конце слайса.
Как можно ускорить MethodByName
Вариант первый: не использовать MethodByName. Или кешировать результат. Но будем считать, что мы не можем этого сделать.
func (t *rtype) MethodByName(name string) (m Method, ok bool) { // <лишний код вырезан> if len(methods) > 8 { i := sort.Search(len(methods), func(i int) bool { return t.nameOff(methods[i].name).name() >= name }) if i < len(methods) && t.nameOff(methods[i].name).name() == name { return t.Method(i), true } } else { for i, p := range methods { if t.nameOff(p.name).name() == name { return t.Method(i), true } } } return Method{}, false }
Мы оставили линейный поиск для малых слайсов, чтобы избежать замедления. На более крупных размерах бинарный поиск выигрывает почти в любой ситуации, кроме разве что случаев, когда искомый метод находится в самом начале слайса.
Если мы напишем бенчмарки, то получим примерно такие цифры:
name old time/op new time/op delta MethodByName/4_first-8 426ns ± 2% 423ns ± 1% ~ MethodByName/4_last-8 482ns ± 1% 476ns ± 1% -1.33% MethodByName/4_nonexisting-8 57.8ns ± 0% 57.3ns ± 1% -0.78% MethodByName/16_first-8 427ns ± 1% 538ns ± 0% +25.97% MethodByName/16_last-8 643ns ± 1% 519ns ± 1% -19.24% MethodByName/16_nonexisting-8 194ns ± 0% 105ns ± 0% -46.03% MethodByName/32_first-8 429ns ± 1% 552ns ± 1% +28.78% MethodByName/32_last-8 831ns ± 0% 540ns ± 2% -34.97% MethodByName/32_nonexisting-8 396ns ± 1% 125ns ± 1% -68.56% MethodByName/64_first-8 431ns ± 1% 580ns ± 2% +34.64% MethodByName/64_last-8 1.36µs ± 1% 0.56µs ± 1% -58.95% MethodByName/64_nonexisting-8 759ns ± 0% 146ns ± 1% -80.82% [Geo mean] 430ns 303ns -29.58%
Как и ожидалось, новый метод работает хуже для случаев, когда искомый метод находится в начале слайса. Если учесть, что это вырожденный случаи и в среднем метод будет находиться где-то в середине, то выигрыш довольно очевиден.
Проверяем влияние на hugo
Изначально, указанный выше сайт собирался за 20.4 секунды.
Соберём hugo с обновлённым пакетом reflect и запустим сборку сайта ещё раз.
Невероятно, сборка сайта теперь занимает 16.5 секунд!
А что же там с профилем?
(pprof) top 20 Showing nodes accounting for 50.85s, 44.61% of 114s total Dropped 1174 nodes (cum <= 0.57s) Showing top 20 nodes out of 194 flat flat% sum% cum cum% 8.64s 7.58% 7.58% 25.01s 21.94% runtime.mallocgc 7.69s 6.75% 14.32% 87.02s 76.33% reflect.Value.call 4.17s 3.66% 17.98% 5.39s 4.73% runtime.heapBitsSetType 2.88s 2.53% 20.51% 4s 3.51% runtime.findObject 2.83s 2.48% 22.99% 2.83s 2.48% runtime.nextFreeFast (inline) 2.79s 2.45% 25.44% 9.46s 8.30% runtime.scanobject 2.74s 2.40% 27.84% 2.74s 2.40% runtime.duffcopy 1.82s 1.60% 29.44% 1.88s 1.65% runtime.pageIndexOf 1.76s 1.54% 30.98% 1.76s 1.54% runtime.memclrNoHeapPointers 1.69s 1.48% 32.46% 3.24s 2.84% runtime.mapaccess2 1.66s 1.46% 33.92% 1.83s 1.61% syscall.Syscall 1.49s 1.31% 35.23% 1.61s 1.41% reflect.name.readVarint 1.43s 1.25% 36.48% 3.07s 2.69% reflect.name.name 1.42s 1.25% 37.73% 10.54s 9.25% reflect.FuncOf 1.37s 1.20% 38.93% 1.75s 1.54% github.com/gohugoio/hugo/... 1.34s 1.18% 40.11% 4.99s 4.38% sync.(*Map).Load 1.33s 1.17% 41.27% 1.33s 1.17% reflect.(*rtype).Kind 1.29s 1.13% 42.40% 1.29s 1.13% cmpbody 1.28s 1.12% 43.53% 1.28s 1.12% runtime.resolveNameOff 1.23s 1.08% 44.61% 1.30s 1.14% runtime.heapBitsForAddr
MethodByName, который был на 1-ой позиции, теперь не входит даже в top20.
Но нам стоит копнуть глубже, чтобы делать какие-либо выводы. Давайте посчитаем сколько там было вызовов MethodByName и каково распределение размеров слайсов exportedMethods и позиций, на которых метод был найден.
Общее количество вызовов за сборку сайта: 17042238.
15920637 times | 120 methods 493669 times | 29 methods 227736 times | 16 methods 180098 times | 8 methods 143846 times | 39 methods 31371 times | 6 methods 14636 times | 1 methods 10933 times | 31 methods 10094 times | 9 methods 9026 times | 4 methods 102 times | 113 methods 85 times | 7 methods 4 times | 114 methods 1 times | 0 methods
Большая часть вызовов была над типом, у которого 120 экспортируемых методов.
12998952 times | 98 index pos 1203864 times | 70 index pos 515978 times | 102 index pos 354191 times | 110 index pos 262965 times | 6 index pos 200819 times | 11 index pos 178457 times | 19 index pos 170806 times | 97 index pos 147510 times | 74 index pos 136927 times | 3 index pos 123071 times | 20 index pos 106250 times | 9 index pos 87190 times | 7 index pos 62767 times | 31 index pos 62192 times | 5 index pos 50170 times | 51 index pos 49120 times | 69 index pos 45325 times | 72 index pos 42236 times | 115 index pos 38646 times | 0 index pos 37106 times | 28 index pos 22744 times | 108 index pos 21521 times | 14 index pos 18134 times | 92 index pos 17341 times | 4 index pos 11535 times | 103 index pos 10791 times | 63 index pos 10303 times | 2 index pos 9170 times | 10 index pos 7278 times | 68 index pos 5936 times | 15 index pos 5466 times | 65 index pos 5385 times | 16 index pos 5172 times | 47 index pos 4478 times | 43 index pos 3032 times | 40 index pos 2340 times | 17 index pos 2265 times | 25 index pos 1516 times | 44 index pos 1068 times | 88 index pos 843 times | 1 index pos 709 times | 71 index pos 358 times | 109 index pos 100 times | 67 index pos 50 times | 64 index pos 40 times | 42 index pos 33 times | 34 index pos 17 times | 86 index pos 15 times | 83 index pos 15 times | 41 index pos 15 times | 33 index pos 12 times | 118 index pos 5 times | 8 index pos 5 times | 27 index pos 4 times | 66 index pos
Распределение индексов искомых методов тоже подсказывает, что линейный поиск в данном случае — не лучший выбор. Большая часть методов находилась на позиции 98.
Подводные камни
В коде выше я использовал sort.Search, но он не доступен для пакета reflect.
Дело в том, что пакет sort импортирует reflect, поэтому мы получаем ошибку циклических импортов.
Чтобы этого избежать, можно продублировать реализацию sort.Search в пакете reflect или создать новый internal пакет для Go, где будет некоторое подмножество пакета sort, не зависящее от reflect.
Этот фактор немного затрудняет внедрение предлагаемого патча, ведь самый красивый вариант реализации нам недоступен.
Как воспроизвести результаты, собрать профили
$ git clone https://github.com/GSA/digitalgov.gov.git $ cd digitalgov.gov $ hugo --profile-cpu cpu.out
Далее используем go tool pprof для анализа профиля cpu.out.
Я использовал hugo с коммита c8b5ab75b743914f89b51046eee8e3daa2eb1eec. Но думаю примерно такие же результаты можно получить и с других версий.
В digitalgov.gov надо будет поправить пару файлов, убрав использования getenv или добавив их в исключения.
modified: themes/digital.gov/layouts/partials/core/head.html modified: themes/digital.gov/layouts/partials/core/set-env.html
Версия Go особо значения не имеет, потому что линейный поиск в MethodByName был всегда.
Выводы
Я выслал CL с изменениями в Go, но не знаю, примут его или нет (spoiler: скорее всего нет).
В любом случае, было забавно увидеть как такая безобидная на первый взгляд операция замедляла сборку статического сайта на какое-то значительное время.
Немного другого я ожидал от hugo с заголовком «The world’s fastest framework for building websites.»
Если вдруг вы хотите поставить +1 к высланному патчу, сделать это можно, оставив реакцию к golang/issue50617.
ссылка на оригинал статьи https://habr.com/ru/post/645727/
Добавить комментарий