The Go Blog
Профилирование программ на Go
На Scala Days 2011 Роберт Хундт представил статью под названием Loop Recognition in C++/Java/Go/Scala. В статье был реализован конкретный алгоритм поиска циклов, как это может быть сделано в рамках прохода потока анализа компилятора, на C++, Go, Java, Scala, а затем использовались эти программы для вывода заключений о типичных проблемах производительности этих языков. Программа на Go, представленная в этой статье, выполняется довольно медленно, что делает её отличной возможностью продемонстрировать, как использовать инструменты профилирования Go, чтобы ускорить медленную программу.
Используя инструменты профилирования Go для выявления и устранения конкретных узких мест, мы можем сделать программу поиска циклов на Go в десять раз быстрее и использовать в 6 раз меньше памяти.
(Обновление: Благодаря недавним оптимизациям libstdc++ в gcc, сокращение использования памяти теперь составляет 3.7 раза.)
В статье Хундта не указаны конкретные версии инструментов C++, Go, Java и Scala,
которые он использовал.
В этом посте мы будем использовать последнюю еженедельную сборку компилятора 6g
языка Go и версию g++, поставляемую с дистрибутивом Ubuntu Natty.
(Мы не будем использовать Java или Scala, потому что мы не специализируемся на написании эффективных
программ на этих языках, поэтому сравнение было бы несправедливым.
Поскольку C++ оказался самым быстрым языком в статье, сравнения с ним здесь должны быть достаточными.)
(Обновление: В этом обновлённом посте мы будем использовать последнюю сборку компилятора Go
для архитектуры amd64 и последнюю версию g++ – 4.8.0, выпущенную в марте 2013 года.)
<code>$ go version go version devel +08d20469cc20 Tue Mar 26 08:27:18 2013 +0100 linux/amd64 $ g++ --version g++ (GCC) 4.8.0 Copyright (C) 2013 Free Software Foundation, Inc. ... $ </code>
Программы запускаются на компьютере с процессором Core i7-2600 частотой 3.4 ГГц и 16 ГБ оперативной памяти под управлением ядра Gentoo Linux 3.8.4-gentoo. Машина работает с отключенным управлением частотой процессора через
<code>$ sudo bash # for i in /sys/devices/system/cpu/cpu[0-7] do echo performance > $i/cpufreq/scaling_governor done # </code>
Мы взяли бенчмарки Хундта
на C++ и Go, объединили их в один исходный файл и удалили всё, кроме одной строки вывода.
Мы будем замерять время выполнения программы с помощью утилиты Linux time с форматом,
который показывает время пользователя, системное время, реальное время и максимальное использование памяти:
<code>$ cat xtime #!/bin/sh /usr/bin/time -f '%Uu %Ss %er %MkB %C' "$@" $ $ make havlak1cc g++ -O3 -o havlak1cc havlak1.cc $ ./xtime ./havlak1cc # of loops: 76002 (total 3800100) loop-0, nest: 0, depth: 0 17.70u 0.05s 17.80r 715472kB ./havlak1cc $ $ make havlak1 go build havlak1.go $ ./xtime ./havlak1 # of loops: 76000 (including 1 artificial root node) 25.05u 0.11s 25.20r 1334032kB ./havlak1 $ </code>
Программа на C++ выполняется за 17,80 секунды и использует 700 МБ памяти.
Программа на Go выполняется за 25,20 секунды и использует 1302 МБ памяти.
(Эти измерения трудно согласовать с результатами, приведёнными в статье, но цель этого поста — исследовать, как использовать go tool pprof, а не воспроизводить результаты из статьи.)
Чтобы начать настройку программы на Go, необходимо включить профилирование.
Если код использовал поддержку бенчмарков из пакета Go testing, мы могли бы использовать стандартные флаги -cpuprofile и -memprofile из gotest.
В автономной программе, такой как эта, необходимо импортировать runtime/pprof и добавить несколько строк кода:
<code>var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
func main() {
flag.Parse()
if *cpuprofile != "" {
f, err := os.Create(*cpuprofile)
if err != nil {
log.Fatal(err)
}
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
}
...
</code>
Новый код определяет флаг под названием cpuprofile, вызывает библиотеку флагов Go для разбора флагов командной строки, а затем, если флаг cpuprofile был задан в командной строке, запускает профилирование ЦП, перенаправленное в файл.
Профайлер требует завершающего вызова StopCPUProfile для сброса всех ожидающих записей в файл перед завершением работы программы; мы используем defer, чтобы убедиться, что это произойдёт при возвращении из main.
После добавления этого кода можно запустить программу с новым флагом -cpuprofile и затем выполнить go tool pprof для интерпретации профиля.
<code>$ make havlak1.prof ./havlak1 -cpuprofile=havlak1.prof # of loops: 76000 (including 1 artificial root node) $ go tool pprof havlak1 havlak1.prof Welcome to pprof! For help, type 'help'. (pprof) </code>
Программа go tool pprof представляет собой небольшое изменение C++ профайлера Google pprof.
Самая важная команда — topN, которая показывает топ N выборок в профиле:
<code>(pprof) top10 Total: 2525 samples 298 11.8% 11.8% 345 13.7% runtime.mapaccess1_fast64 268 10.6% 22.4% 2124 84.1% main.FindLoops 251 9.9% 32.4% 451 17.9% scanblock 178 7.0% 39.4% 351 13.9% hash_insert 131 5.2% 44.6% 158 6.3% sweepspan 119 4.7% 49.3% 350 13.9% main.DFS 96 3.8% 53.1% 98 3.9% flushptrbuf 95 3.8% 56.9% 95 3.8% runtime.aeshash64 95 3.8% 60.6% 101 4.0% runtime.settype_flush 88 3.5% 64.1% 988 39.1% runtime.mallocgc </code>
При включённом профилировании ЦП программа на Go останавливается примерно 100 раз в секунду и записывает выборку, состоящую из счётчиков программных адресов в стеке текущей выполняющейся горутины.
В профиле 2525 выборок, следовательно, программа работала чуть более 25 секунд.
В выводе go tool pprof для каждой функции, которая встречалась в выборке, есть строка.
Первые два столбца показывают количество выборок, в которых функция выполнялась (в отличие от ожидания завершения вызванной функции), как абсолютное значение и в процентах от общего числа выборок.
Функция runtime.mapaccess1_fast64 выполнялась в 298 выборках, или 11,8%.
Вывод команды top10 отсортирован по количеству выборок.
Третий столбец показывает накопленную сумму выборок во время вывода: первые три строки составляют 32,4% выборок.
Четвёртый и пятый столбцы показывают количество выборок, в которых функция встречалась (либо выполнялась, либо ожидала завершения вызванной функции).
Функция main.FindLoops выполнялась в 10,6% выборок, но находилась в стеке вызовов (либо сама, либо функции, которые она вызвала, выполнялись) в 84,1% выборок.
Чтобы отсортировать по четвёртому и пятому столбцам, используйте флаг -cum (для накопленных значений):
<code>(pprof) top5 -cum Total: 2525 samples 0 0.0% 0.0% 2144 84.9% gosched0 0 0.0% 0.0% 2144 84.9% main.main 0 0.0% 0.0% 2144 84.9% runtime.main 0 0.0% 0.0% 2124 84.1% main.FindHavlakLoops 268 10.6% 10.6% 2124 84.1% main.FindLoops (pprof) top5 -cum </code>
На самом деле, сумма для main.FindLoops и main.main должна была составлять 100%, но
каждый образец стека включает только последние 100 кадров стека; примерно в четверти
образцов рекурсивная функция main.DFS была глубже, чем main.main, на более чем 100 кадров,
поэтому полный трейс был обрезан.
Образцы трассировки стека содержат более интересные данные о взаимосвязях вызовов функций,
чем текстовые списки могут показать.
Команда web записывает граф данных профайлинга в формате SVG и открывает его в веб-браузере.
(Существует также команда gv, которая записывает PostScript и открывает его в Ghostview.
Для любой из этих команд необходимо установить graphviz.)
<code>(pprof) web </code>
Небольшая часть полного графа выглядит так:
Каждый прямоугольник на графике соответствует одной функции, и размеры прямоугольников
определяются количеством образцов, в которых функция выполнялась.
Ребро от прямоугольника X к прямоугольнику Y указывает, что X вызывает Y; число рядом с ребром
показывает, сколько раз этот вызов встречается в образце.
Если вызов встречается несколько раз в одном образце, например, при рекурсивных вызовах функций,
каждое вхождение учитывается в весе ребра.
Это объясняет значение 21342 на петле из main.DFS в себя.
Бросив взгляд на график, можно увидеть, что программа тратит много времени на операции с хэш-таблицами,
которые соответствуют использованию значений Go-map.
Можно указать команде web использовать только те образцы, которые включают определённую функцию,
например runtime.mapaccess1_fast64, что уменьшает шум на графике:
<code>(pprof) web mapaccess1 </code>
Если присмотреться, можно заметить, что вызовы runtime.mapaccess1_fast64 делаются функциями
main.FindLoops и main.DFS.
Теперь, когда у нас есть общее представление о картине, пришло время увеличить масштаб и сосредоточиться на конкретной функции.
Сначала рассмотрим main.DFS, просто потому что это более короткая функция:
<code>(pprof) list DFS
Total: 2525 samples
ROUTINE ====================== main.DFS in /home/rsc/g/benchgraffiti/havlak/havlak1.go
119 697 Total samples (flat / cumulative)
3 3 240: func DFS(currentNode *BasicBlock, nodes []*UnionFindNode, number map[*BasicBlock]int, last []int, current int) int {
1 1 241: nodes[current].Init(currentNode, current)
1 37 242: number[currentNode] = current
. . 243:
1 1 244: lastid := current
89 89 245: for _, target := range currentNode.OutEdges {
9 152 246: if number[target] == unvisited {
7 354 247: lastid = DFS(target, nodes, number, last, lastid+1)
. . 248: }
. . 249: }
7 59 250: last[number[currentNode]] = lastid
1 1 251: return lastid
(pprof)
</code>
Вывод показывает исходный код функции DFS (на самом деле, для каждой функции, соответствующей регулярному выражению DFS). Первые три столбца — это количество выборок, сделанных во время выполнения этой строки, количество выборок, сделанных во время выполнения этой строки или кода, вызванного из неё, и номер строки в файле.
Связанная команда disasm показывает дизассемблированный вид функции вместо исходного кода; когда выборок достаточно, это может помочь увидеть, какие инструкции являются ресурсоёмкими.
Команда weblist объединяет два режима: она показывает исходный код, в котором при щелчке по строке отображается дизассемблирование.
Поскольку мы уже знаем, что время тратится на поиск в картах, реализованных хэш-функциями среды выполнения, нас больше всего интересует второй столбец.
Большая часть времени уходит на рекурсивные вызовы функции DFS (строка 247), что соответствует ожидаемому поведению рекурсивного обхода.
Исключая рекурсию, кажется, что время тратится на доступ к карте number в строках 242, 246 и 250.
Для этой конкретной операции поиска карта — не самый эффективный выбор.
Как и в компиляторе, базовые блоки имеют уникальные номера, присвоенные им последовательно.
Вместо использования map[*BasicBlock]int мы можем использовать []int, срез, индексируемый по номеру блока.
Нет причин использовать карту, когда подойдёт массив или срез.
Изменение number с карты на срез требует редактирования семи строк в программе и сокращает время выполнения примерно в два раза:
<code>$ make havlak2 go build havlak2.go $ ./xtime ./havlak2 # of loops: 76000 (including 1 artificial root node) 16.55u 0.11s 16.69r 1321008kB ./havlak2 $ </code>
(См. разницу между havlak1 и havlak2)
Можно снова запустить профайлер, чтобы подтвердить, что main.DFS больше не составляет значительной части времени выполнения:
<code>$ make havlak2.prof ./havlak2 -cpuprofile=havlak2.prof # of loops: 76000 (including 1 artificial root node) $ go tool pprof havlak2 havlak2.prof Welcome to pprof! For help, type 'help'. (pprof) (pprof) top5 Total: 1652 samples 197 11.9% 11.9% 382 23.1% scanblock 189 11.4% 23.4% 1549 93.8% main.FindLoops 130 7.9% 31.2% 152 9.2% sweepspan 104 6.3% 37.5% 896 54.2% runtime.mallocgc 98 5.9% 43.5% 100 6.1% flushptrbuf (pprof) </code>
Запись main.DFS больше не появляется в профиле, а остальное время выполнения программы тоже уменьшилось.
Теперь программа тратит большую часть времени на выделение памяти и сборку мусора (runtime.mallocgc, которая как выделяет память, так и запускает периодическую сборку мусора, составляет 54.2% времени).
Чтобы выяснить, почему сборщик мусора запускается так часто, нужно выяснить, что именно выделяет память.
Один из способов — добавить профилирование памяти в программу.
Мы организуем так, чтобы если передан флаг -memprofile, программа останавливалась после одной итерации поиска циклов, записывала профиль памяти и завершалась:
<code>var memprofile = flag.String("memprofile", "", "записать профиль памяти в этот файл")
...
FindHavlakLoops(cfgraph, lsgraph)
if *memprofile != "" {
f, err := os.Create(*memprofile)
if err != nil {
log.Fatal(err)
}
pprof.WriteHeapProfile(f)
f.Close()
return
}
</code>
Вызываем программу с флагом -memprofile, чтобы записать профиль:
<code>$ make havlak3.mprof go build havlak3.go ./havlak3 -memprofile=havlak3.mprof $ </code>
(См. различия от havlak2)
Используем go tool pprof точно так же. Теперь образцы, которые мы анализируем, — это
выделения памяти, а не такты часов.
<code>$ go tool pprof havlak3 havlak3.mprof Adjusting heap profiles for 1-in-524288 sampling rate Welcome to pprof! For help, type 'help'. (pprof) top5 Total: 82.4 MB 56.3 68.4% 68.4% 56.3 68.4% main.FindLoops 17.6 21.3% 89.7% 17.6 21.3% main.(*CFG).CreateNode 8.0 9.7% 99.4% 25.6 31.0% main.NewBasicBlockEdge 0.5 0.6% 100.0% 0.5 0.6% itab 0.0 0.0% 100.0% 0.5 0.6% fmt.init (pprof) </code>
Команда go tool pprof сообщает, что FindLoops выделила около
56.3 из 82.4 МБ используемой памяти; CreateNode отвечает за ещё 17.6 МБ.
Чтобы уменьшить накладные расходы, профайлер памяти записывает информацию только для примерно
одного блока на полмегабайта выделенной памяти («1-in-524288 sampling rate»), поэтому эти
значения являются приближёнными к реальным.
Чтобы найти выделения памяти, можно вывести список тех функций.
<code>(pprof) list FindLoops
Total: 82.4 MB
ROUTINE ====================== main.FindLoops in /home/rsc/g/benchgraffiti/havlak/havlak3.go
56.3 56.3 Total MB (flat / cumulative)
...
1.9 1.9 268: nonBackPreds := make([]map[int]bool, size)
5.8 5.8 269: backPreds := make([][]int, size)
. . 270:
1.9 1.9 271: number := make([]int, size)
1.9 1.9 272: header := make([]int, size, size)
1.9 1.9 273: types := make([]int, size, size)
1.9 1.9 274: last := make([]int, size, size)
1.9 1.9 275: nodes := make([]*UnionFindNode, size, size)
. . 276:
. . 277: for i := 0; i < size; i++ {
9.5 9.5 278: nodes[i] = new(UnionFindNode)
. . 279: }
...
. . 286: for i, bb := range cfgraph.Blocks {
. . 287: number[bb.Name] = unvisited
29.5 29.5 288: nonBackPreds[i] = make(map[int]bool)
. . 289: }
...
</code>
Похоже, текущий узкий место такой же, как и раньше: использование карт там,
где достаточно более простых структур данных.
FindLoops выделяет около 29.5 МБ карт.
К слову, если запустить go tool pprof с флагом --inuse_objects, то будет
выводиться количество выделений вместо их размера:
<code>$ go tool pprof --inuse_objects havlak3 havlak3.mprof
Adjusting heap profiles for 1-in-524288 sampling rate
Welcome to pprof! For help, type 'help'.
(pprof) list FindLoops
Total: 1763108 objects
ROUTINE ====================== main.FindLoops in /home/rsc/g/benchgraffiti/havlak/havlak3.go
720903 720903 Total objects (flat / cumulative)
...
. . 277: for i := 0; i < size; i++ {
311296 311296 278: nodes[i] = new(UnionFindNode)
. . 279: }
. . 280:
. . 281: // Step a:
. . 282: // - initialize all nodes as unvisited.
. . 283: // - depth-first traversal and numbering.
. . 284: // - unreached BB's are marked as dead.
. . 285: //
. . 286: for i, bb := range cfgraph.Blocks {
. . 287: number[bb.Name] = unvisited
409600 409600 288: nonBackPreds[i] = make(map[int]bool)
. . 289: }
...
(pprof)
</code>
Поскольку ~200 000 карт составляют 29,5 МБ, кажется, что начальное выделение карты занимает около 150 байт. Это разумно, когда карта используется для хранения пар ключ-значение, но не тогда, когда карта используется как замена простого множества, как в данном случае.
Вместо использования карты, мы можем использовать простой срез для перечисления элементов.
Во всех случаях, кроме одного, где используются карты, алгоритму невозможно вставить дублирующийся элемент.
В оставшемся случае можно написать простую вариацию встроенной функции append:
<code>func appendUnique(a []int, x int) []int {
for _, y := range a {
if x == y {
return a
}
}
return append(a, x)
}
</code>
Помимо написания этой функции, изменение программы на Go для использования срезов вместо карт требует изменения лишь нескольких строк кода.
<code>$ make havlak4 go build havlak4.go $ ./xtime ./havlak4 # of loops: 76000 (including 1 artificial root node) 11.84u 0.08s 11.94r 810416kB ./havlak4 $ </code>
(См. различия от havlak3)
Теперь мы достигли скорости в 2,11 раза быстрее, чем в начале. Посмотрим еще раз на профиль процессора.
<code>$ make havlak4.prof ./havlak4 -cpuprofile=havlak4.prof # of loops: 76000 (including 1 artificial root node) $ go tool pprof havlak4 havlak4.prof Welcome to pprof! For help, type 'help'. (pprof) top10 Total: 1173 samples 205 17.5% 17.5% 1083 92.3% main.FindLoops 138 11.8% 29.2% 215 18.3% scanblock 88 7.5% 36.7% 96 8.2% sweepspan 76 6.5% 43.2% 597 50.9% runtime.mallocgc 75 6.4% 49.6% 78 6.6% runtime.settype_flush 74 6.3% 55.9% 75 6.4% flushptrbuf 64 5.5% 61.4% 64 5.5% runtime.memmove 63 5.4% 66.8% 524 44.7% runtime.growslice 51 4.3% 71.1% 51 4.3% main.DFS 50 4.3% 75.4% 146 12.4% runtime.MCache_Alloc (pprof) </code>
Теперь выделение памяти и последующая сборка мусора (runtime.mallocgc) составляют 50,9% нашего времени выполнения.
Другой способ понять, почему система собирает мусор — это посмотреть на выделения, вызывающие сборку, те, которые проводят большую часть времени в mallocgc:
<code>(pprof) web mallocgc </code>
Трудно понять, что происходит на этом графике, потому что множество узлов имеют небольшое количество выборок, заглушающих большие.
Можно указать go tool pprof игнорировать узлы, которые не составляют как минимум 10% выборок:
<code>$ go tool pprof --nodefraction=0.1 havlak4 havlak4.prof Welcome to pprof! For help, type 'help'. (pprof) web mallocgc </code>
Теперь легко следовать толстым стрелкам, чтобы увидеть, что FindLoops вызывает большую часть сборок мусора.
Если мы выведем FindLoops, можно увидеть, что значительная часть находится именно в начале:
<code>(pprof) list FindLoops
...
. . 270: func FindLoops(cfgraph *CFG, lsgraph *LSG) {
. . 271: if cfgraph.Start == nil {
. . 272: return
. . 273: }
. . 274:
. . 275: size := cfgraph.NumNodes()
. . 276:
. 145 277: nonBackPreds := make([][]int, size)
. 9 278: backPreds := make([][]int, size)
. . 279:
. . 280: number := make([]int, size)
. 17 281: header := make([]int, size, size)
. . 282: types := make([]int, size, size)
. . 283: last := make([]int, size, size)
. . 284: nodes := make([]*UnionFindNode, size, size)
. . 285:
. . 286: for i := 0; i < size; i++ {
2 79 287: nodes[i] = new(UnionFindNode)
. . 288: }
...
(pprof)
</code>
Каждый раз, когда вызывается FindLoops, выделяются некоторые значительные структуры для учёта данных.
Поскольку бенчмарки вызывают FindLoops 50 раз, эти выделения накапливаются в значительное количество мусора, что приводит к большому объёму работы для сборщика мусора.
Наличие языка с автоматической сборкой мусора не означает, что можно игнорировать вопросы выделения памяти.
В данном случае простое решение — это введение кэша, чтобы каждый вызов FindLoops повторно использовал память предыдущего вызова, когда это возможно.
(На самом деле, в статье Хандта он объясняет, что для получения приемлемой производительности программе на Java требовалось именно это изменение, но он не внес аналогичного изменения в другие реализации с автоматической сборкой мусора.)
Добавим глобальную структуру cache:
<code>var cache struct {
size int
nonBackPreds [][]int
backPreds [][]int
number []int
header []int
types []int
last []int
nodes []*UnionFindNode
}
</code>
а затем заставим FindLoops использовать её вместо выделения памяти:
<code>if cache.size < size {
cache.size = size
cache.nonBackPreds = make([][]int, size)
cache.backPreds = make([][]int, size)
cache.number = make([]int, size)
cache.header = make([]int, size)
cache.types = make([]int, size)
cache.last = make([]int, size)
cache.nodes = make([]*UnionFindNode, size)
for i := range cache.nodes {
cache.nodes[i] = new(UnionFindNode)
}
}
nonBackPreds := cache.nonBackPreds[:size]
for i := range nonBackPreds {
nonBackPreds[i] = nonBackPreds[i][:0]
}
backPreds := cache.backPreds[:size]
for i := range nonBackPreds {
backPreds[i] = backPreds[i][:0]
}
number := cache.number[:size]
header := cache.header[:size]
types := cache.types[:size]
last := cache.last[:size]
nodes := cache.nodes[:size]
</code>
Использование глобальной переменной, конечно же, является плохой инженерной практикой: это означает, что одновременные вызовы FindLoops теперь становятся небезопасными.
На данный момент мы вносим минимально необходимые изменения, чтобы понять, какие аспекты важны для производительности нашей программы; это изменение простое и отражает код из реализации на Java.
Финальная версия программы на Go будет использовать отдельный экземпляр LoopFinder для отслеживания этой памяти, восстанавливая возможность параллельного использования.
<code>$ make havlak5 go build havlak5.go $ ./xtime ./havlak5 # of loops: 76000 (including 1 artificial root node) 8.03u 0.06s 8.11r 770352kB ./havlak5 $ </code>
(См. отличия от havlak4)
Есть ещё несколько способов улучшить программу и сделать её более эффективной, но ни один из них не требует применения методов профилирования, которые мы уже рассмотрели. Список задач, используемый в внутреннем цикле, может быть переиспользован между итерациями и между вызовами функции FindLoops, а также может быть объединён с отдельным «пулом узлов», создаваемым в ходе выполнения этой функции. Аналогично, хранение графа циклов может быть переиспользовано на каждой итерации вместо того, чтобы выделяться заново. В дополнение к этим улучшениям производительности, финальная версия написана с использованием идиоматического стиля Go, с применением структур данных и методов. Стилистические изменения оказывают лишь незначительное влияние на время выполнения: алгоритм и ограничения остались без изменений.
Финальная версия выполняется за 2.29 секунды и использует 351 МБ памяти:
<code>$ make havlak6 go build havlak6.go $ ./xtime ./havlak6 # of loops: 76000 (including 1 artificial root node) 2.26u 0.02s 2.29r 360224kB ./havlak6 $ </code>
Это в 11 раз быстрее, чем исходная программа. Даже если отключить переиспользование сгенерированного графа циклов, так что кэшируется только необходимая информация для поиска циклов, программа всё равно работает в 6.7 раза быстрее оригинала и использует в 1.5 раза меньше памяти.
<code>$ ./xtime ./havlak6 -reuseloopgraph=false # of loops: 76000 (including 1 artificial root node) 3.69u 0.06s 3.76r 797120kB ./havlak6 -reuseloopgraph=false $ </code>
Конечно, теперь уже несправедливо сравнивать эту программу на Go с оригинальной программой на C++, которая использовала неэффективные структуры данных, такие как set, где были бы более уместны vector. В качестве проверки мы перевели финальную программу на Go в эквивалентный C++ код. Время его выполнения примерно совпадает с временем выполнения программы на Go:
<code>$ make havlak6cc g++ -O3 -o havlak6cc havlak6.cc $ ./xtime ./havlak6cc # of loops: 76000 (including 1 artificial root node) 1.99u 0.19s 2.19r 387936kB ./havlak6cc </code>
Программа на Go работает почти так же быстро, как и программа на C++. Поскольку программа на C++ использует автоматическое удаление и выделение памяти вместо явного кэширования, программа на C++ немного короче и проще в написании, хотя и не значительно:
<code>$ wc havlak6.cc; wc havlak6.go 401 1220 9040 havlak6.cc 461 1441 9467 havlak6.go $ </code>
(См. havlak6.cc и havlak6.go)
Бенчмарки хороши только в той мере, в какой хороши программы, которые они измеряют. Мы использовали go tool pprof для анализа неэффективной программы на Go, а затем улучшили её производительность в разы и сократили использование памяти в 3,7 раза. Сравнение с эквивалентно оптимизированной программой на C++ показывает, что Go может конкурировать с C++, если программисты внимательны к количеству мусора, генерируемого внутренними циклами.
Исходные тексты программы, двоичные файлы для Linux x86-64 и профили, использованные для написания этой статьи, доступны в проекте benchgraffiti на GitHub.
Как упоминалось выше, go test уже включает эти флаги профилирования: определите функцию бенчмарка, и всё будет готово. Также существует стандартный HTTP-интерфейс для данных профилирования. В HTTP-сервере добавление
<code>import _ "net/http/pprof" </code>
установит обработчики для нескольких URL-адресов по пути /debug/pprof/. Затем вы можете запустить go tool pprof, передав ему один аргумент — URL к данным профилирования вашего сервера, и он скачает и проанализирует живой профиль.
<code>go tool pprof http://localhost:6060/debug/pprof/profile # 30-секундный профиль CPU go tool pprof http://localhost:6060/debug/pprof/heap # профиль кучи go tool pprof http://localhost:6060/debug/pprof/block # профиль блокировок горутин </code>
Профиль блокировок горутин будет объяснён в будущем посте. Следите за обновлениями.
Следующая статья: Функции первого класса в Go
Предыдущая статья: Обзор внешних библиотек на Go
Индекс блога