Contenu

Traduction profilage et optimisation en Go

Je vous propose une traduction de cette page où il est question de profilage et d’optimisation du code Go.

Présentation faite par Brad Fitzpatrick
YAPC::Asia 2015
Tokyo Big Sight, 2015-08-22
Voir le diaporama d’introduction
Regarder la vidéo de cette présentation

Les exigences

Si vous le faites de chez vous, vous aurez besoin des éléments suivants:

Programme de départ

Débuggons et optimisons un simple serveur http

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
  "fmt"
  "log"
  "net/http"
  "regexp"
)

var visitors int

func handleHi(w http.ResponseWriter, r *http.Request) {
  if match, _ := regexp.MatchString(`^\w*$`, r.FormValue("color")); !match {
    http.Error(w, "Optional color is invalid", http.StatusBadRequest)
    return
  }
  visitors++
  w.Header().Set("Content-Type", "text/html; charset=utf-8")
  w.Write([]byte("<h1 style='color: " + r.FormValue("color") +
       "'>Welcome!</h1>You are visitor number " + fmt.Sprint(visitors) +   "!"))
}

func main() {
  log.Printf("Starting on port 8080")
  http.HandleFunc("/hi", handleHi)
  log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}

Exécutons le:

1
2
3
4
5
6
$ cd $GOPATH/src/github.com/bradfitz/talk-yapc-asia-2015/demo
$ go run demo.go
ou
$ go build && ./demo
ou
$ go install && demo

Testons:

1
2
$ go test
?       yapc/demo       [no test files]

Oula. Pas de test. Écrivons-en un peu.
Dans le fichier demo_test.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package demo

import (
  "bufio"
  "net/http"
  "net/http/httptest"
  "strings"
  "testing"
)

func TestHandleRoot_Recorder(t *testing.T) {
  rw := httptest.NewRecorder()
  handleHi(rw, req(t, "GET / HTTP/1.0\r\n\r\n"))
  if !strings.Contains(rw.Body.String(), "visitor number") {
    t.Errorf("Unexpected output: %s", rw.Body)
  }
}

func req(t *testing.T, v string) *http.Request {
  req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(v)))
  if err != nil {
    t.Fatal(err)
  }
  return req
}

Maintenant:

1
2
3
4
5
$ go test -v
=== RUN   TestHandleHi_Recorder
--- PASS: TestHandleHi_Recorder (0.00s)
PASS
ok      yapc/demo       0.053s

Ceci teste le gestionnaire HTTP avec une simple implémentation en mémoire de l’interface ResponseWriter.

Une autre façon d’écrire un test HTTP consiste à utiliser le client et le serveur HTTP actuel mais avec des adresses localhost automatiquement crées, en utilisant le paquet httptest:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func TestHandleHi_TestServer(t *testing.T) {
  ts := httptest.NewServer(http.HandlerFunc(handleHi))
  defer ts.Close()
  res, err := http.Get(ts.URL)
  if err != nil {
    t.Error(err)
    return
  }
  if g, w := res.Header.Get("Content-Type"), "text/html; charset=utf-8"; g != w {
    t.Errorf("Content-Type = %q; want %q", g, w)
  }
  slurp, err := ioutil.ReadAll(res.Body)
  defer res.Body.Close()
  if err != nil {
    t.Error(err)
    return
  }
  t.Logf("Got: %s", slurp)
}

Race detector

La simultanéité est intégrée au langage Go et il parallélise automatiquement le code selon les besoins sur les CPU disponibles. Contrairement à Rust, en Go, vous pouvez écrire un code avec une race condition si vous n’êtes pas prudent. Une race condition arrive quand de multiple goroutines accèdent à une donnée partagée sans synchronisation et quand une des gouroutines fait de l’écriture.

Avant d’optimiser notre code, assurons-nous de ne pas avoir de race condition.

Lançons juste les tests avec le drapeau -race:

1
2
3
$ go test -race
PASS
ok      yapc/demo       1.047s

Tout est bon, non ?

Hé bien non !

Le détecteur de race conditions de Go utilise l’analyse d’exécution. Il n’a pas de faux positifs mais il a des faux négatifs. S’il ne voit pas vraiment de race condition, il ne peut pas les signaler.

Changeons notre test pour faire deux choses à la fois:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func TestHandleHi_TestServer_Parallel(t *testing.T) {
  ts := httptest.NewServer(http.HandlerFunc(handleHi))
  defer ts.Close()
  var wg sync.WaitGroup
  for i := 0; i < 2; i++ {
    wg.Add(1)
    go func() {
      defer wg.Done()
      res, err := http.Get(ts.URL)
      if err != nil {
        t.Error(err)
        return
      }
      if g, w := res.Header.Get("Content-Type"), "text/html; charset=utf-8"; g != w {
        t.Errorf("Content-Type = %q; want %q", g, w)
      }
      slurp, err := ioutil.ReadAll(res.Body)
      defer res.Body.Close()
      if err != nil {
        t.Error(err)
        return
      }
      t.Logf("Got: %s", slurp)
      }()
    }
  wg.Wait()
}

Maintenant nous pouvons relancer le test et regarder ce qui se passe:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
$ go test -v -race
=== RUN   TestHandleHi_Recorder
--- PASS: TestHandleHi_Recorder (0.00s)
=== RUN   TestHandleHi_TestServer
--- PASS: TestHandleHi_TestServer (0.00s)
        demo_test.go:46: Got: <h1 style='color: '>Welcome!</h1>You are visitor number 2!
=== RUN   TestHandleHi_TestServer_Parallel
==================
WARNING: DATA RACE
Read by goroutine 21:
  yapc/demo.handleHi()
      /Users/bradfitz/src/yapc/demo/demo.go:17 +0xf5
  net/http.HandlerFunc.ServeHTTP()
      /Users/bradfitz/go/src/net/http/server.go:1422 +0x47
  net/http/httptest.(*waitGroupHandler).ServeHTTP()
      /Users/bradfitz/go/src/net/http/httptest/server.go:200 +0xfe
  net/http.serverHandler.ServeHTTP()
      /Users/bradfitz/go/src/net/http/server.go:1862 +0x206
  net/http.(*conn).serve()
      /Users/bradfitz/go/src/net/http/server.go:1361 +0x117c

Previous write by goroutine 23:
  yapc/demo.handleHi()
      /Users/bradfitz/src/yapc/demo/demo.go:17 +0x111
  net/http.HandlerFunc.ServeHTTP()
      /Users/bradfitz/go/src/net/http/server.go:1422 +0x47
  net/http/httptest.(*waitGroupHandler).ServeHTTP()
      /Users/bradfitz/go/src/net/http/httptest/server.go:200 +0xfe
  net/http.serverHandler.ServeHTTP()
      /Users/bradfitz/go/src/net/http/server.go:1862 +0x206
  net/http.(*conn).serve()
      /Users/bradfitz/go/src/net/http/server.go:1361 +0x117c

Goroutine 21 (running) created at:
  net/http.(*Server).Serve()
      /Users/bradfitz/go/src/net/http/server.go:1912 +0x464

Goroutine 23 (running) created at:
  net/http.(*Server).Serve()
      /Users/bradfitz/go/src/net/http/server.go:1912 +0x464
==================
--- PASS: TestHandleHi_TestServer_Parallel (0.00s)
        demo_test.go:68: Got: <h1 style='color: '>Welcome!</h1>You are visitor number 3!
        demo_test.go:68: Got: <h1 style='color: '>Welcome!</h1>You are visitor number 4!
PASS
Found 1 data race(s)
exit status 66
FAIL    yapc/demo       1.056s

Maintenant, nous pouvons voir que l’écriture sur la ligne 17 (la variable visitors) est en conflit avec la lecture de la ligne 17 (sur la même variable). Pour le rendre plus évident, modifiez le code comme suit:

1
2
now := visitors + 1
visitors = now

… et il affichera des numéros de lignes différents pour chaque erreur.

Corrigeons la race condition

Si votre code comporte des races conditions, tous les paries sont perdus et attendez vous à un plantage de l’application.

Il y a plusieurs solutions pour résoudre le problème:

  • Utiliser les canaux (“Ne pas communiquer en partageant la mémoire; à la place, partagez la mémoire en communiquant”)
  • Utiliser un Mutex
  • Utiliser un atomic

Les mutex

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var visitors struct {
  sync.Mutex
  n int
}
...
func foo() {
  ...
  visitors.Lock()
  visitors.n++
  yourVisitorNumber := visitors.n
  visitors.Unlock()
}

atomic

1
2
3
4
5
6
var visitors int64 // must be accessed atomically
...
func foo() {
  ...
  visitNum := atomic.AddInt64(&visitors, 1)
}

À quelle vitesse peut-il aller ? Profilage CPU !

Pour utiliser le profileur de CPU Go, il est préférable d’écrire une fonction de référence qui est très similaire à une fonction de test:

1
2
3
4
5
6
7
8
func BenchmarkHi(b *testing.B) {
  b.ReportAllocs()
  r := req(b, "GET / HTTP/1.0\r\n\r\n")
  for i := 0; i < b.N; i++ {
    rw := httptest.NewRecorder()
    handleHi(rw, r)
  }
}

(et changez func req en une interface testing.TB à la place. Elle prendra un argument *testing.T ou *testing.B)

Nous pouvons maintenant lancer le test de performances:

1
2
3
4
$ go test -v -run=^$ -bench=.
PASS
BenchmarkHi-4     100000             12843 ns/op
ok      yapc/demo       1.472s

Jouez avec les drapeaux comme -benchtime.

Est-ce que c’est rapide ? lent ?

Mais regardons où va le CPU maintenant …

Profilage du CPU

1
$ go test -v -run=^$ -bench=^BenchmarkHi$ -benchtime=2s -cpuprofile=prof.cpu

(Laissons le binaire de demo.test de coté)

Maintetant, utilisons le visualiseur de profil Go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
$ go tool pprof demo.test prof.cpu
Entering interactive mode (type "help" for commands)

(pprof) top
3070ms of 3850ms total (79.74%)
Dropped 62 nodes (cum <= 19.25ms)
Showing top 10 nodes out of 92 (cum >= 290ms)
      flat  flat%   sum%        cum   cum%
    1710ms 44.42% 44.42%     1710ms 44.42%  runtime.mach_semaphore_signal
     290ms  7.53% 51.95%     1970ms 51.17%  runtime.growslice
     230ms  5.97% 57.92%      230ms  5.97%  runtime.mach_semaphore_wait
     200ms  5.19% 63.12%     2270ms 58.96%  runtime.mallocgc
     160ms  4.16% 67.27%      160ms  4.16%  runtime.heapBitsSetType
     110ms  2.86% 70.13%      210ms  5.45%  runtime.mapassign1
     110ms  2.86% 72.99%      110ms  2.86%  runtime.memclr
     100ms  2.60% 75.58%      640ms 16.62%  regexp.makeOnePass.func2
     100ms  2.60% 78.18%      100ms  2.60%  runtime.memmove
      60ms  1.56% 79.74%      290ms  7.53%  runtime.makeslice

(pprof) top --cum
0.26s of 3.85s total ( 6.75%)
Dropped 62 nodes (cum <= 0.02s)
Showing top 10 nodes out of 92 (cum >= 2.22s)
      flat  flat%   sum%        cum   cum%
         0     0%     0%      3.55s 92.21%  runtime.goexit
         0     0%     0%      3.48s 90.39%  testing.(*B).launch
         0     0%     0%      3.48s 90.39%  testing.(*B).runN
     0.01s  0.26%  0.26%      3.47s 90.13%  yapc/demo.BenchmarkHi
     0.01s  0.26%  0.52%      3.44s 89.35%  yapc/demo.handleHi
         0     0%  0.52%      3.30s 85.71%  regexp.MatchString
     0.01s  0.26%  0.78%         3s 77.92%  regexp.Compile
         0     0%  0.78%      2.99s 77.66%  regexp.compile
     0.20s  5.19%  5.97%      2.27s 58.96%  runtime.mallocgc
     0.03s  0.78%  6.75%      2.22s 57.66%  regexp.compileOnePass

(pprof) list handleHi
Total: 3.85s
ROUTINE ======================== yapc/demo.handleHi in /Users/bradfitz/src/yapc/demo/demo.go
      10ms      3.44s (flat, cum) 89.35% of Total
         .          .      8:)
         .          .      9:
         .          .     10:var visitors int
         .          .     11:
         .          .     12:func handleHi(w http.ResponseWriter, r *http.Request) {
         .      3.30s     13:   if match, _ := regexp.MatchString(\w*$r.FormValue("color")); !match {
         .          .     14:           http.Error(w, "Optional color is invalid", http.StatusBadRequest)
         .          .     15:           return
         .          .     16:   }
      10ms       10ms     17:   visitors++
         .       50ms     18:   w.Header().Set("Content-Type", "text/html; charset=utf-8")
         .       80ms     19:   w.Write([]byte("<h1 style='color: " + r.FormValue("color") + "'>Welcome!</h1>You are visitor number " + fmt.Sprint(visitors) + "!"))
         .          .     20:}
         .          .     21:
         .          .     22:func main() {
         .          .     23:   log.Printf("Starting on port 8080")
         .          .     24:   http.HandleFunc("/hi", handleHi)

(pprof) web

/images/go/cpu0.webp

3 secondes dans regexp.MatchString ? \

Le profilage de la CPU affiche 85,71% du temps cumulatif dans MatchString, 77,92% dans la compilation regexp et 58,96% dans le garbage collector (qui est déclenchée via les allocations).

Moins d’allocation => moins d’utilisation du garbage collector (GC)

Pourquoi allouons-nous ?

Profilage de mémoire

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
$ go tool pprof --alloc_space demo.test prof.mem
(pprof) top
1159.72MB of 1485.25MB total (78.08%)
Dropped 12 nodes (cum <= 7.43MB)
Showing top 10 nodes out of 33 (cum >= 1484.75MB)
      flat  flat%   sum%        cum   cum%
  294.10MB 19.80% 19.80%   294.10MB 19.80%  regexp.onePassCopy
  174.53MB 11.75% 31.55%   174.53MB 11.75%  regexp.progMachine
  119.03MB  8.01% 39.57%   170.54MB 11.48%  regexp/syntax.(*compiler).compile
  106.53MB  7.17% 46.74%   106.53MB  7.17%  net/textproto.MIMEHeader.Set
  100.51MB  6.77% 53.51%   308.51MB 20.77%  regexp.makeOnePass
      99MB  6.67% 60.17%   208.01MB 14.00%  regexp.makeOnePass.func2
   84.50MB  5.69% 65.86%    84.50MB  5.69%  regexp.mergeRuneSets.func2
   69.51MB  4.68% 70.54%    80.01MB  5.39%  regexp/syntax.(*parser).op
   58.51MB  3.94% 74.48%   242.02MB 16.30%  regexp/syntax.Parse
   53.50MB  3.60% 78.08%  1484.75MB   100%  yapc/demo.BenchmarkHi

(pprof) top --cum
249.02MB of 1485.25MB total (16.77%)
Dropped 12 nodes (cum <= 7.43MB)
Showing top 10 nodes out of 33 (cum >= 308.51MB)
      flat  flat%   sum%        cum   cum%
         0     0%     0%  1484.75MB   100%  runtime.goexit
         0     0%     0%  1484.75MB   100%  testing.(*B).launch
         0     0%     0%  1484.75MB   100%  testing.(*B).runN
   53.50MB  3.60%  3.60%  1484.75MB   100%  yapc/demo.BenchmarkHi
   52.50MB  3.53%  7.14%  1431.25MB 96.36%  yapc/demo.handleHi
         0     0%  7.14%  1265.21MB 85.18%  regexp.MatchString
         0     0%  7.14%  1087.18MB 73.20%  regexp.Compile
   42.51MB  2.86% 10.00%  1087.18MB 73.20%  regexp.compile
         0     0% 10.00%   602.61MB 40.57%  regexp.compileOnePass
  100.51MB  6.77% 16.77%   308.51MB 20.77%  regexp.makeOnePass

(pprof) list BenchmarkHi
Total: 1.45GB
ROUTINE ======================== yapc/demo.BenchmarkHi in /Users/bradfitz/src/yapc/demo/demo_test.go
   53.50MB     1.45GB (flat, cum)   100% of Total
         .          .     72:}
         .          .     73:
         .          .     74:func BenchmarkHi(b *testing.B) {
         .          .     75:   r := req(b, "GET / HTTP/1.0\r\n\r\n")
         .          .     76:   for i := 0; i < b.N; i++ {
   53.50MB    53.50MB     77:           rw := httptest.NewRecorder()
         .     1.40GB     78:           handleHi(rw, r)
         .          .     79:   }
         .          .     80:}

(pprof) list handleHi
Total: 1.45GB
ROUTINE ======================== yapc/demo.handleHi in /Users/bradfitz/src/yapc/demo/demo.go
   52.50MB     1.40GB (flat, cum) 96.36% of Total
         .          .      8:)
         .          .      9:
         .          .     10:var visitors int
         .          .     11:
         .          .     12:func handleHi(w http.ResponseWriter, r *http.Request) {
         .     1.24GB     13:   if match, _ := regexp.MatchString(\w*$r.FormValue("color")); !match {
         .          .     14:           http.Error(w, "Optional color is invalid", http.StatusBadRequest)
         .          .     15:           return
         .          .     16:   }
         .          .     17:   visitors++
         .   106.53MB     18:   w.Header().Set("Content-Type", "text/html; charset=utf-8")
   52.50MB    59.50MB     19:   w.Write([]byte("<h1 style='color: " + r.FormValue("color") + "'>Welcome!</h1>You are visitor number " + fmt.Sprint(visitors) + "!"))
         .          .     20:}
         .          .     21:
         .          .     22:func main() {
         .          .     23:   log.Printf("Starting on port 8080")
         .          .     24:   http.HandleFunc("/hi", handleHi)

Compilons cette regexp juste une fois

1
2
3
var colorRx = regexp.MustCompile(`\w*$`)
...
  if !colorRx.MatchString(r.FormValue("color")) {

Et maintenant:

1
2
3
4
$ go test -bench=.
PASS
BenchmarkHi-4    1000000              1451 ns/op
ok      yapc/demo       1.517s

10x plus vite !

Comparons avec le CPU maintenant:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
bradfitz@laptop demo$ go test -v -run=^$ -bench=. -benchtime=3s -memprofile=prof.mem -cpuprofile=prof.cpu
PASS
BenchmarkHi-4    3000000              1420 ns/op
ok      yapc/demo       5.768s
bradfitz@laptop demo$ profcpu
Entering interactive mode (type "help" for commands)
(pprof) top --cum 30
2.78s of 6.24s total (44.55%)
Dropped 75 nodes (cum <= 0.03s)
Showing top 30 nodes out of 114 (cum >= 0.67s)
      flat  flat%   sum%        cum   cum%
         0     0%     0%      4.84s 77.56%  runtime.goexit
         0     0%     0%      3.72s 59.62%  testing.(*B).launch
         0     0%     0%      3.72s 59.62%  testing.(*B).runN
     0.02s  0.32%  0.32%      3.71s 59.46%  yapc/demo.BenchmarkHi
         0     0%  0.32%      3.04s 48.72%  yapc/demo.handleHi
     0.37s  5.93%  6.25%      2.55s 40.87%  runtime.mallocgc
     2.16s 34.62% 40.87%      2.16s 34.62%  runtime.mach_semaphore_signal
         0     0% 40.87%      2.16s 34.62%  runtime.mach_semrelease
         0     0% 40.87%      2.16s 34.62%  runtime.notewakeup
         0     0% 40.87%      2.16s 34.62%  runtime.semawakeup
         0     0% 40.87%      2.03s 32.53%  runtime.startm
         0     0% 40.87%      2.02s 32.37%  runtime.wakep
         0     0% 40.87%      1.83s 29.33%  runtime.systemstack
         0     0% 40.87%      1.54s 24.68%  runtime.ready
         0     0% 40.87%      1.54s 24.68%  runtime.startGC
         0     0% 40.87%      1.32s 21.15%  runtime.schedule
     0.02s  0.32% 41.19%      1.21s 19.39%  runtime.mcall
     0.01s  0.16% 41.35%      1.06s 16.99%  runtime.semasleep.func1
         0     0% 41.35%      1.05s 16.83%  runtime.semasleep1
         0     0% 41.35%      1.01s 16.19%  runtime.concatstring5
     0.11s  1.76% 43.11%      1.01s 16.19%  runtime.concatstrings
     0.01s  0.16% 43.27%      0.94s 15.06%  runtime.rawstringtmp
     0.03s  0.48% 43.75%      0.93s 14.90%  runtime.rawstring
     0.01s  0.16% 43.91%      0.75s 12.02%  runtime.stringtoslicebyte
     0.02s  0.32% 44.23%      0.74s 11.86%  runtime.rawbyteslice
         0     0% 44.23%      0.73s 11.70%  runtime.stopm
     0.02s  0.32% 44.55%      0.70s 11.22%  runtime.newobject
         0     0% 44.55%      0.69s 11.06%  runtime.semasleep
         0     0% 44.55%      0.67s 10.74%  runtime.findrunnable
         0     0% 44.55%      0.67s 10.74%  runtime.goschedImpl

(pprof) bradfitz@laptop demo$ profmem
Entering interactive mode (type "help" for commands)
(pprof) top --cum
2739.53MB of 2740.53MB total (  100%)
Dropped 9 nodes (cum <= 13.70MB)
      flat  flat%   sum%        cum   cum%
         0     0%     0%  2740.03MB   100%  runtime.goexit
         0     0%     0%  2740.03MB   100%  testing.(*B).launch
         0     0%     0%  2740.03MB   100%  testing.(*B).runN
  728.06MB 26.57% 26.57%  2740.03MB   100%  yapc/demo.BenchmarkHi
  561.03MB 20.47% 47.04%  2011.98MB 73.42%  yapc/demo.handleHi
         0     0% 47.04%  1382.94MB 50.46%  net/http.Header.Set
 1382.94MB 50.46% 97.50%  1382.94MB 50.46%  net/textproto.MIMEHeader.Set
   67.50MB  2.46%   100%       68MB  2.48%  fmt.Sprint
(pprof)

(pprof) list handleHi
Total: 2.68GB
ROUTINE ======================== yapc/demo.handleHi in /Users/bradfitz/src/yapc/demo/demo.go
  561.03MB     1.96GB (flat, cum) 73.42% of Total
         .          .     15:   if !colorRx.MatchString(r.FormValue("color")) {
         .          .     16:           http.Error(w, "Optional color is invalid", http.StatusBadRequest)
         .          .     17:           return
         .          .     18:   }
         .          .     19:   visitors++
         .     1.35GB     20:   w.Header().Set("Content-Type", "text/html; charset=utf-8")
  561.03MB   629.03MB     21:   w.Write([]byte("<h1 style='color: " + r.FormValue("color") + "'>Welcome!</h1>You are visitor number " + fmt.Sprint(visitors) + "!"))
         .          .     22:}
         .          .     23:
         .          .     24:func main() {
         .          .     25:   log.Printf("Starting on port 8080")
         .          .     26:   http.HandleFunc("/hi", handleHi)

Optimisation de la mémoire

  • Supprimer la ligne d’en tête Content-Type (le serveur net/http le fera pour nous)
  • Utiliser fmt.Fprintf(w, … à la place de la concaténation

Benchcmp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ go test -bench=. -memprofile=prof.mem | tee mem.0
... (fix)
$ go test -bench=. -memprofile=prof.mem | tee mem.1
... (fix)
$ go test -bench=. -memprofile=prof.mem | tee mem.2

$ benchcmp mem.0 mem.2
benchmark         old ns/op     new ns/op     delta
BenchmarkHi-4     1180          964           -18.31%

benchmark         old allocs     new allocs     delta
BenchmarkHi-4     9              5              -44.44%

benchmark         old bytes     new bytes     delta
BenchmarkHi-4     720           224           -68.89%

Une nette amélioration. Maintenant d’où vient cette mémoire ?

profmem, voir et fixer ResponseRecorder:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func BenchmarkHi(b *testing.B) {
  b.ReportAllocs()
  r := req(b, "GET / HTTP/1.0\r\n\r\n")
  rw := httptest.NewRecorder()
  for i := 0; i < b.N; i++ {
          handleHi(rw, r)
          reset(rw)
  }
}

func reset(rw *httptest.ResponseRecorder) {
  m := rw.HeaderMap
  for k := range m {
          delete(m, k)
  }
  body := rw.Body
  body.Reset()
  *rw = httptest.ResponseRecorder{
          Body:      body,
          HeaderMap: m,
  }
}

Maintenant:

1
2
3
$ go test -bench=. -memprofile=prof.mem | tee mem.3
PASS
BenchmarkHi-4    2000000               649 ns/op              32 B/op          2 allocs/op

Où est-ce ?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
(pprof) top --cum 10
88MB of 88MB total (  100%)
      flat  flat%   sum%        cum   cum%
         0     0%     0%    87.50MB 99.43%  runtime.goexit
         0     0%     0%    87.50MB 99.43%  testing.(*B).launch
         0     0%     0%    87.50MB 99.43%  testing.(*B).runN
         0     0%     0%    87.50MB 99.43%  yapc/demo.BenchmarkHi
   87.50MB 99.43% 99.43%    87.50MB 99.43%  yapc/demo.handleHi
    0.50MB  0.57%   100%     0.50MB  0.57%  runtime.malg
         0     0%   100%     0.50MB  0.57%  runtime.mcommoninit
         0     0%   100%     0.50MB  0.57%  runtime.mpreinit
         0     0%   100%     0.50MB  0.57%  runtime.rt0_go
         0     0%   100%     0.50MB  0.57%  runtime.schedinit

(pprof) list handleHi
Total: 88MB
ROUTINE ======================== yapc/demo.handleHi in /Users/bradfitz/src/yapc/demo/demo.go
   87.50MB    87.50MB (flat, cum) 99.43% of Total
         .          .     24:   visitors.n++
         .          .     25:   num := visitors.n
         .          .     26:   visitors.Unlock()
         .          .     27:   //      w.Header().Set("Content-Type", "text/html; charset=utf-8")
         .          .     28:
   87.50MB    87.50MB     29:   fmt.Fprintf(w, "<h1 style='color: %s'>Welcome!</h1>You are visitor number %d!", r.FormValue("color"), num)
         .          .     30:}
         .          .     31:
         .          .     32:func main() {
         .          .     33:   log.Printf("Starting on port 8080")
         .          .     34:   http.HandleFunc("/hi", handleHi)

(pprof) disasm handleHi
...
         .          .      831f7: LEAQ 0x70(SP), BX
         .          .      831fc: MOVQ BX, 0x8(SP)
         .          .      83201: MOVQ $0x0, 0x10(SP)
      43MB       43MB      8320a: CALL runtime.convT2E(SB)
         .          .      8320f: MOVQ 0x18(SP), CX
         .          .      83214: MOVQ 0x20(SP), AX
...

Représentation en temps réel des structures de données Go

Regardez ce diaporama: accéder aux structures de données intégrées

  • Une interface Go représente 2 mots en mémoire: (type, pointer)
  • Une chaîne en Go représente 2 mots en mémoire: (pointeur de base, longueur)
  • Une liste en Go représente 3 mots en mémoire: (pointeur de base, taille, capacité)

En sachant cela, regardons ces 32 octets / op.

La ligne en Go est:

1
2
fmt.Fprintf(w, "<h1 style='color: %s'>Welcome!</h1>You are visitor number %d!",
               r.FormValue("color"), num)

La signature de la fonction fmt.Fprintf est:

1
2
3
4
5
$ go doc fmt.Fprintf
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)

    Fprintf formats according to a format specifier and writes to w. It returns
    the number of bytes written and any write error encountered.

Ces valeurs d’interface sont de 16 octets chacune. Elles sont passées par valeur, mais le mot de données d’une interface doit être un pointeur et parce que la chaîne est une valeur composée de 2 mots (plus grand que le 1 mot de données) et int n’est pas un pointeur, la conversion d’un type vers une interface vide (runtime.convT2E) alloue 16 octets pour chacune (la plus petite taille d’allocation sur 64 bits) et met un pointeur sur l’allocation dans le mot de données de la valeur de l’interface.

Allez à cette partie dans les diapositives maintenant:

accéder aux structures de données intégrées

Supprimer toutes les allocations

Vous ne voulez probablement pas écrire de code comme celui-ci, mais quand cela arrive, vous pouvez faire quelque chose comme:

1
2
3
4
5
var bufPool = sync.Pool{
  New: func() interface{} {
    return new(bytes.Buffer)
  },
}

… pour créer un pool de mémoire tampon par processeur à portée globale, puis dans le gestionnaire:

1
2
3
4
5
6
7
8
9
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)
buf.Reset()
buf.WriteString("<h1 style='color: ")
buf.WriteString(r.FormValue("color"))
buf.WriteString(">Welcome!</h1>You are visitor number ")
b := strconv.AppendInt(buf.Bytes(), int64(num), 10)
b = append(b, '!')
w.Write(b)

Profile de contestation

Premièrement, écrivons un benchmark parallère:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func BenchmarkHiParallel(b *testing.B) {
  r := req(b, "GET / HTTP/1.0\r\n\r\n")
  b.RunParallel(func(pb *testing.PB) {
    rw := httptest.NewRecorder()
    for pb.Next() {
        handleHi(rw, r)
        reset(rw)
    }
  })
}

et faisons des mesures:

1
$ go test -bench=Parallel -blockprofile=prof.block

“Fixons” ça:

1
2
3
4
5
6
7
8
9
var colorRxPool = sync.Pool{
  New: func() interface{} { return regexp.MustCompile(`\w*$`) },
}
...
func handleHi(w http.ResponseWriter, r *http.Request) {
  if !colorRxPool.Get().(*regexp.Regexp).MatchString(r.FormValue("color")) {
    http.Error(w, "Optional color is invalid", http.StatusBadRequest)
    return
  }

Qu’en est-il de ce mutex de visiteurs ?

Sortez-le dans une fonction:

1
2
3
4
5
6
7
8
  num := nextVisitorNum()
...
func nextVisitorNum() int {
  visitors.Lock()
  defer visitors.Unlock()
  visitors.n++
  return visitors.n
}

puis, écrivons quelques benchmark:

1
2
3
4
5
6
7
func BenchmarkVisitCount(b *testing.B) {
  b.RunParallel(func(pb *testing.PB) {
    for pb.Next() {
      incrementVisitorNum()
    }
  })
}

Essayez:

  • sans defer
  • avec int(atomic.AddInt64(&atomicVisitors, 1))

Couverture

1
2
3
4
5
6
$ go test -cover -coverprofile=cover
PASS
coverage: 54.8% of statements
ok      yapc/demo       0.066s
$ go tool cover -html=cover
(opens web browser)