Ky postim është importuar nga https://metalblueberry.github.io/post/howto/2019-11-01_go-cpu-profiling/ Unë rekomandoj të shkoni atje për të pasur një përvojë të përsosur leximi.



Le t'i hedhim një sy mjeteve të profilizimit të Go CPU për të optimizuar llogaritjen e grupit Mandelbrot nga postimi i mëparshëm.

Ju duhet të lexoni "postimin e mëparshëm" për të kuptuar kontekstin e këtij postimi.

Pasi krahasova performancën e Llogaritjes së grupit Mandelbrot me shikuesin interaktiv në internet, kuptova se performanca është vërtet e dobët. Për këtë arsye, unë po krijoj këtë postim për të treguar se si të përdoren mjetet e profilizimit të CPU për të zbuluar bllokimin e kodit dhe për ta rregulluar atë.

Shkruani disa standarde

Ndjenja e mirë

Në pamje të parë shoh se metoda Diverges mund të optimizohet lehtësisht duke shmangur rrënjën katrore. Pra, le të shkruajmë disa standarde për të provuar se kam të drejtë.

Variabla e jashtme e vlerës është për të shmangur optimizimet gjatë kohës së përpilimit që mund të zvogëlojë kohën e standardit. "Dave Cheney" shpjegon të gjitha detajet në blogun e tij.

var value bool
func BenchmarkDiverges(b *testing.B) {
  for i := 0; i < b.N; i++ {
    point := NewPoint(-0.5, 0.5)
    value = point.Diverges()
  }
}
func (m *Point) Diverges() bool {
  return cmplx.Abs(m.z) > complex(2, 0)
}

Unë e kam lënë funksionin Diverges këtu vetëm si kujtesë. le të ekzekutojmë standardet për të parë rezultatin.

╰─>$ go test  -run='^$' -bench=BenchmarkDiverges
goos: linux
goarch: amd64
pkg: github.com/metalblueberry/mandelbrot/mandelbrot
BenchmarkDiverges-6     168255253                6.43 ns/op
PASS
ok      github.com/metalblueberry/mandelbrot/mandelbrot 1.464s

Pra, kemi 6.43 ns/op që është mjaft mirë. Le të modifikojmë funksionin Diverges dhe ta ekzekutojmë përsëri.

func (m *Point) Diverges() bool {
 return real(m.z)*real(m.z)+imag(m.z)*imag(m.z) > 4
}
╰─>$ go test  -run='^$' -bench=BenchmarkDiverges
goos: linux
goarch: amd64
pkg: github.com/metalblueberry/mandelbrot/mandelbrot
BenchmarkDiverges-6     227208410                5.10 ns/op
PASS
ok      github.com/metalblueberry/mandelbrot/mandelbrot 1.625s

Ka një rënie nga 6,43 ns/op në 5,10 ns/op dhe kjo është 20% më shpejt! jemi në rrugën e duhur.

Lajmi i keq është se kjo nuk është mënyra më e mirë për të optimizuar kodin. Fillimisht duhet ta analizojmë për të parë nëse ruajtja e 1 ns/op në këtë funksion është e rëndësishme apo jo për të gjithë punën llogaritëse.

Standard i plotë

Le të shkruajmë një funksion standard që simulon të gjithë procesin. Ideja është që të jetë sa më e ngjashme me kryesoren.

package mandelbrot_test
import (
  "testing"
  "github.com/metalblueberry/mandelbrot/mandelbrot"
)
func BenchmarkArea(b *testing.B) {
  set := mandelbrot.Area{
    HorizontalResolution: 200,
    VerticalResolution:   200,
    MaxIterations:        200,
    TopLeft:              complex(-2, 2),
    BottomRight:          complex(2, -2),
  }
  set.Init()
  // Start benchmarking here
  b.ResetTimer()
  for i := 0; i < b.N; i++ {
    p := set.Calculate()
    // loop until p is closed
    for range p {
    }
  }
}

Së pari le të krahasojmë daljen me funksionin e vjetër Diverge kundrejt daljes me funksionin e ri.

## Initial
╰─>$ go test  -run='^$' -bench=BenchmarkArea
goos: linux
goarch: amd64
pkg: github.com/metalblueberry/mandelbrot/mandelbrot
BenchmarkArea-6             2016            564276 ns/op
PASS
ok      github.com/metalblueberry/mandelbrot/mandelbrot 2.176s
## Optimized
╰─>$ go test  -run='^$' -bench=BenchmarkArea
goos: linux
goarch: amd64
pkg: github.com/metalblueberry/mandelbrot/mandelbrot
BenchmarkArea-6             3144            351339 ns/op
PASS
ok      github.com/metalblueberry/mandelbrot/mandelbrot 1.950s

Kodi është 40% më i shpejtë për këtë seksion të caktuar të grupit mandelbrot, dhe ne sapo kemi hequr një llogaritje të rrënjës katrore.

Profilizimi i CPU-së

Unë do të ekzekutoj përsëri standardin e plotë, por këtë herë me flamurin -cpuprofile dhe kjo do të gjenerojë një skedar profilizimi.

╰─>$ go test -run='^$' -bench=BenchmarkArea  -cpuprofile profile.out

Për të vizualizuar skedarin profile.out, na duhet mjeti go pprof. Është një mjet vërtet i fuqishëm, por tani për tani, le ta mbajmë të thjeshtë dhe thjesht të ekzekutojmë ndërfaqen e internetit me komandën e mëposhtme.

go tool pprof -http localhost:8080 profile.out

Faqja e internetit do të hapet në pamjen e grafikut. Por personalisht preferoj ta përfytyroj këtë si një grafik flakë. Këtë mund ta bëni duke klikuar në shikoni grafikun e flakës.

Dhe këtu qëndron problemi. Funksioni cmplx.Abs po merr 31,74% të kohës. Le të ekzekutojmë përsëri standardin, por me optimizimin.

E mrekullueshme. Funksioni Diverges tani merr vetëm 8,45% të kohës. cili është shkelësi tjetër kryesor i kohës së llogaritjes? Për t'iu përgjigjur kësaj pyetjeje, ne mund të vazhdojmë të shikojmë grafikun e flakës ose të shkojmë drejtpërdrejt në pamjen top ku mund të shohim kohën e kaluar në secilin funksion.

i sheshtë% shuma% sperma% 680ms 31.92% 31.92% 2060ms 96.71% github.com/metalblueberry/mandelbrot/mandelbrot.(*Sipërfaqja).Llogaritni.func1 330ms 15.49% 15.49 ms 6% 4 ms 12,21% 59,62% 820ms /mandelbrot/mandelbrot.(* Pika).Divergon 90ms 4.23% 82.16% 90ms 4.23% matematikë.Sincos 70ms 3.29% 85.45% 70ms 3.29% matematikë.xatan 40ms 1.88% 87.32% 81% 0.8 ms%H1. 40ms 1.88% matematikë.IsInf 40ms 1.88 % 91,08% 380ms 17,84% matematikë/cmplx.Pow

Surpriza e parë këtu është se "runtime.chansend" dhe "runtime.selectnbsend" po marrin 42% të kohës dhe është diçka që thjesht raporton progresin! Le ta heqim atë.

Optimizimet

Raportimi i progresit

Është e rëndësishme të vihet re se do ta bëjë funksionin Llogarit sinkron.

// Previous
func (a *Area) Calculate() (progress chan int) {
  progress = make(chan int)
  go func() {
    defer close(progress)
    for i, pixel := range a.Points {
      pixel.Calculate(a.MaxIterations)
      a.Points[i] = pixel
      select {
      case progress <- i:
      default:
      }
    }
  }()
  return
}
// New
func (a *Area) Calculate() {
  for i, pixel := range a.Points {
    pixel.Calculate(a.MaxIterations)
    a.Points[i] = pixel
  }
}

Duhet ta heqim edhe kanalin nga pikë referimi

// Previous
func BenchmarkArea(b *testing.B) {
  set := mandelbrot.Area{
    HorizontalResolution: 200,
    VerticalResolution:   200,
    MaxIterations:        200,
    TopLeft:              complex(-2, 2),
    BottomRight:          complex(2, -2),
  }
  set.Init()
  b.ResetTimer()
  for i := 0; i < b.N; i++ {
    progress := set.Calculate()
    for range progress {
    }
  }
}
  
// New
func BenchmarkArea(b *testing.B) {
  set := mandelbrot.Area{
    HorizontalResolution: 200,
    VerticalResolution:   200,
    MaxIterations:        200,
    TopLeft:              complex(-2, 2),
    BottomRight:          complex(2, -2),
  }
  set.Init()
  b.ResetTimer()
  for i := 0; i < b.N; i++ {
    set.Calculate()
  }
}

Dhe pikë referimi tani është…

╰─>$ go test -run='^$' -bench=BenchmarkArea  -cpuprofile profile.out
goos: linux
goarch: amd64
pkg: github.com/metalblueberry/mandelbrot/mandelbrot
BenchmarkArea-6             5301            206832 ns/op
PASS
ok      github.com/metalblueberry/mandelbrot/mandelbrot 2.007s

Nëse lëvizni lart për të kontrolluar rezultatin e mëparshëm, ne jemi përmirësuar nga 351339 ns/op në 206832 ns/op. Le të marrim grafikun e flakës dhe majën për të parë se si të vazhdojmë.

Flat Flat% shuma% sperma sperma% 0.93s 49.21% 49.21% 1.89s 100% github.com/metalblueberry/mandelbrot/mandelbrot.(area).calculate 0.45s 23.81% 73.02% 0.96S 50.79% github.com/MetalBlueBerry/ mandelbrot. 06 s 3.17% 91.01% 0.06s 3.17% matematikë.Hipot 0.05s 2.65% 93.65% 0.10s 5.29% matematikë.pow 0.02s 1.06% 94.71% 0.02s 0.06% 0.02s % 0,02s 1,06% matematikë. IsInf 0.02s 1.06% 96.83% 0.05s 2.65% matematikë.atan2 0.01s 0.53% 97.35% 0.01s 0.53% matematikë.frexp

96% e kohës shpenzohet brenda Zonës. Funksioni Llogarit dhe kjo do të thotë se nuk po humbim kohë duke bërë gjëra të tjera. Përveç kësaj, do të prisja të kaloja shumicën e kohës brenda Point. Calculate, pasi aty kryhen përsëritjet. Le të shkojmë te "skeda e burimit" për të parë kohën e shpenzuar për rresht.

Total:       930ms      1.89s (flat, cum)   100%
     33            .          .           func (a *Area) Calculate() {
     34        510ms      510ms             for i, pixel := range a.Points {
     35        140ms      1.10s               pixel.Calculate(a.MaxIterations)
     36        280ms      280ms               a.Points[i] = pixel
     37            .          .             }
     38            .          .           }

Pra, ne shpenzojmë 1⁄3 e kohës duke përsëritur mbi grupin Points dhe duke caktuar vlerën. Ndihem pak i humbur në këtë pikë, kështu që le të modifikojmë kodin për të parë nëse mund të përmirësohet. Gjëja e parë që dua të provoj është të zëvendësoj operacionin e diapazonit me një lak të thjeshtë për.

Kalimi i fetës

Ky është versioni i ri i funksionit Llogarit.

// Previous
func (a *Area) Calculate() {
  for i, pixel := range a.Points {
    pixel.Calculate(a.MaxIterations)
    a.Points[i] = pixel
  }
}
// New
func (a *Area) Calculate() {
  for i := 0; i < len(a.Points); i++ {
    a.Points[i].Calculate(a.MaxIterations)
  }
}
╰─>$ go test -run='^$' -bench=BenchmarkArea  -cpuprofile profile.out
goos: linux
goarch: amd64
pkg: github.com/metalblueberry/mandelbrot/mandelbrot
BenchmarkArea-6            10767            105798 ns/op
PASS
ok      github.com/metalblueberry/mandelbrot/mandelbrot 1.906s

Total:       390ms      1.71s (flat, cum)   100%
    33            .          .           func (a *Area) Calculate() {
     34        210ms      210ms             for i := 0; i < len(a.Points); i++ {
     35        180ms      1.50s               a.Points[i].Calculate(a.MaxIterations)
     36            .          .             }
     37            .          .           }

Ky ndryshim na jep 105798 ns/op që është gati dy herë më shpejt se versioni i mëparshëm! Kjo eshte fantastike. Nëse kontrolloni standardin fillestar, ishte 564276 ns/op, kështu që kjo do të thotë se ne jemi tashmë x5 herë më të shpejtë.

Përpara se të vazhdoj optimizimin, kam vendosur të krahasoj performancën me "shikuesin në internet". Vetëm për të pasur një referencë se sa shpejt mund të kryhet kjo detyrë. Lajmi i trishtuar është se për seksionin vijues, zbatimi ynë merr 100 sekonda dhe shikuesi në internet merr 3,7 sekonda.

Funksioni BenchmarkComplexArea është ai që merr 100 sekonda.

func BenchmarkArea(b *testing.B) {
  set := mandelbrot.Area{
    HorizontalResolution: 200,
    VerticalResolution:   200,
    MaxIterations:        200,
    TopLeft:              complex(-2, 2),
    BottomRight:          complex(2, -2),
  }
  benchmarkGivenArea(b, set)
}
func BenchmarkComplexArea(b *testing.B) {
  set := mandelbrot.Area{
    HorizontalResolution: 1060,
    VerticalResolution:   730,
    MaxIterations:        3534,
    TopLeft:              complex(-1.401854499759, -0.000743603637),
    BottomRight:          complex(-1.399689899172, 0.000743603637),
  }
  benchmarkGivenArea(b, set)
}
func benchmarkGivenArea(b *testing.B, set mandelbrot.Area) {
  set.Init()
  b.ResetTimer()
  for i := 0; i < b.N; i++ {
    set.Calculate()
  }
}

Kur ekzekutoj këtë pikë referimi, kam një profil interesant që tregon se 93,13% e kohës shpenzohet në funksionin matematikë/cmplx.Pow.

Kjo është vërtet e trishtueshme sepse ky funksion është vërtet i dobishëm dhe nuk shoh një mënyrë të qartë për ta optimizuar atë. Unë mendoj se pasi ky funksion mund të trajtojë çdo numër fuqie. Një optimizim mund të jetë vetëm shumëzimi i numrit në vetvete. Le te perpiqemi.

matematikë.Pow

Një tjetër ndryshim i lehtë. Këto janë versioni i vjetër dhe i ri i point.Calculate

// Previous
func (m *Point) Calculate(MaxIterations int) {
  for !m.Diverges() && m.iterations < MaxIterations {
    m.iterations++
    m.z = cmplx.Pow(m.z, 2) + m.Point
  }
}
// New
func (m *Point) Calculate(MaxIterations int) {
  for !m.Diverges() && m.iterations < MaxIterations {
    m.iterations++
    m.z = m.z*m.z + m.Point
  }
}

Le të zbatojmë standardet…

## Previous
╰─>$ go test -run='^$' -bench=Area  -cpuprofile profile.out                                                        12:31:28
goos: linux
goarch: amd64
pkg: github.com/metalblueberry/mandelbrot/mandelbrot
BenchmarkArea-6                    12500             96135 ns/op
BenchmarkComplexArea-6                 1        103429981309 ns/op
PASS
ok      github.com/metalblueberry/mandelbrot/mandelbrot 106.306s
## New
╰─>$ go test -run='^$' -bench=Area  -cpuprofile profile.out
goos: linux
goarch: amd64
pkg: github.com/metalblueberry/mandelbrot/mandelbrot
BenchmarkArea-6                    15406             73572 ns/op
BenchmarkComplexArea-6                 1        5013357483 ns/op
PASS
ok      github.com/metalblueberry/mandelbrot/mandelbrot 5.511s

Mendoj se kemi gjetur shkelësin kryesor të llogaritjes dhe e kemi përmirësuar lehtësisht atë. tani koha është 5013357483 ns/op për ComplexArea që është 5.013357483 s/op dhe kjo është vetëm 1.3 sekonda larg nga faqja e internetit!

Mos përdorni matematikë.Pow nëse nuk ju nevojitet vërtet.

Pas këtij ndryshimi, testet po dështojnë sepse çështja (0,1) nuk divergon më. Ne e kemi zgjidhur këtë problem numerik kur heqim math.Pow dhe testet duhet të përditësohen.

Dereferenca e treguesit

Brenda pikës së metodës. Llogaritni ka disa referenca për variablat brenda Point. Kjo e detyron programin të çreferencojë variablat çdo lak dhe kjo është një humbje kohe. Për ta rregulluar atë, unë do të krijoj një kopje të variablave në fillim të metodës. Kërkohet gjithashtu të futet metoda Diverges.

// Previous
func (m *Point) Calculate(MaxIterations int) {
  for !m.Diverges() && m.iterations < MaxIterations {
    m.iterations++
    m.z = m.z*m.z + m.Point
  }
}
func (m *Point) Diverges() bool {
  return real(m.z)*real(m.z)+imag(m.z)*imag(m.z) > 4
}
// New
func (m *Point) Calculate(MaxIterations int) {
  var z complex128
  point := m.Point
  iterations := m.iterations
  for real(z)*real(z)+imag(z)*imag(z) < 4 && iterations < MaxIterations {
    iterations++
    z = z*z + point
  }
  m.iterations = iterations
}

Dhe ekzekutimi i standardeve përsëri…

## Previous
╰─>$ go test -run='^$' -bench=.
goos: linux
goarch: amd64
pkg: github.com/metalblueberry/mandelbrot/mandelbrot
BenchmarkArea-6                    16176             76731 ns/op
BenchmarkComplexArea-6                 1        5011883400 ns/op
BenchmarkCalculate-6               94263             12727 ns/op
PASS
ok      github.com/metalblueberry/mandelbrot/mandelbrot 8.308s
## New
╰─>$ go test -run='^$' -bench=.
goos: linux
goarch: amd64
pkg: github.com/metalblueberry/mandelbrot/mandelbrot
BenchmarkArea-6                    13608             81561 ns/op
BenchmarkComplexArea-6                 1        3520533936 ns/op
BenchmarkCalculate-6              130862              8912 ns/op
PASS
ok      github.com/metalblueberry/mandelbrot/mandelbrot 6.284s

Është interesante se pikë referimi për zonën e parë është pak më e ngadaltë edhe pse dy të tjerat janë më të shpejta. Nuk mund të jap një shpjegim pse ndodh kjo. Gjithsesi, koha për kompleksin është… 3.5 sekonda!! më shpejt se faqja e internetit.

konkluzioni

Jam vërtet i kënaqur me rezultatin e optimizimit. Ne kemi përmirësuar kodin për të ekzekutuar 85% më shpejt se versioni fillestar dhe kjo është mjaft mbresëlënëse. Unë mendoj se hapi tjetër është të japim grupin me copa për të paralelizuar detyrën llogaritëse dhe për të përfituar nga bërthamat e shumta.

Mund të eksploroni depon për të parë të gjithë kodin dhe për të luajtur me të këtu.

Faleminderit që lexuat dhe mos ngurroni të lini një koment më poshtë, do të jem vërtet i lumtur të dëgjoj nga ju.

Referencat

  • "Si të shkruajmë go standard nga Dave Cheney"
  • "Vizualizues i flamegrafit në internet"
  • "Vizualizer online mandelbrot"
  • "Profilimi dhe optimizimi në lëvizje"