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"