├── .github
└── FUNDING.yml
├── README-es.md
├── README.md
├── TODO
├── performance-es.md
├── performance-ptbr.md
├── performance-zh.md
└── performance.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: https://buymeacoffee.com/dgryski
2 |
--------------------------------------------------------------------------------
/README-es.md:
--------------------------------------------------------------------------------
1 | # go-perfbook
2 |
3 | [](https://www.buymeacoffee.com/dgryski)
4 |
5 | Este documento describe las mejores prácticas para escribir código de alto rendimiento en Go.
6 |
7 | Las primeras secciones cubren cómo escribir código optimizado en cualquier lenguaje.
8 | Las secciones posteriores cubren técnicas específicas de Go.
9 |
10 | ### Versiones en varios idiomas
11 |
12 | * [English](README.md)
13 | * [中文](performance-zh.md)
14 | * [Español](README-es.md)
15 |
16 | ### Tabla de contenidos
17 |
18 | 1. [Escribiendo y optimizando código en Go](performance-es.md#escribir-y-optimizar-codigo-en-go)
19 | 1. [Cuándo y dónde optimizar](performance-es.md#cuándo-y-dónde-optimizar)
20 | 1. [Modificar los datos](performance-es.md#modificar-los-datos)
21 | 1. [Modificar los algoritmos](performance-es.md#modificar-los-algoritmos)
22 |
23 | ### Cómo contribuir
24 |
25 | Este es un libro en desarrollo sobre rendimiento y optimización en Go.
26 |
27 | Hay diferentes maneras de contribuir:
28 |
29 | 1) agregar o resumir los recursos en el fichero [TODO](TODO)
30 | 2) agregar puntos o nuevos temas a cubrir
31 | 3) completar las secciones en el libro escribiendo el contenido
32 |
33 | Eventualmente se necesitarán ejercicios y ejemplos de programas para optimizar (tal vez).
34 |
35 | La coordinación se realizará en el canal #performance del slack de Gophers.
36 |
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # go-perfbook
2 |
3 | [](https://www.buymeacoffee.com/dgryski)
4 |
5 | This document outlines best practices for writing high-performance Go code.
6 |
7 | The first sections cover writing optimized code in any language.
8 | The later sections cover Go-specific techniques.
9 |
10 | ### Multiple Language Versions
11 |
12 | * [English](performance.md)
13 | * [中文](performance-zh.md)
14 | * [Español](README-es.md)
15 | * [Português Brasileiro](performance-ptbr.md)
16 |
17 | ### Table of Contents
18 |
19 | 1. [Writing and Optimizing Go code](performance.md#writing-and-optimizing-go-code)
20 | 1. [How to Optimize](performance.md#how-to-optimize)
21 | 1. [Optimization Workflow](performance.md#optimization-workflow)
22 | 1. [Concrete Optimization Tips](performance.md#concrete-optimization-tips)
23 | 1. [Data Changes](performance.md#data-changes)
24 | 1. [Algorithmic Changes](performance.md#algorithmic-changes)
25 | 1. [Benchmark Inputs](performance.md#benchmark-inputs)
26 | 1. [Program Tuning](performance.md#program-tuning)
27 | 1. [Optimization Workflow Summary](performance.md#optimization-workflow-summary)
28 | 1. [Garbage Collection](performance.md#garbage-collection)
29 | 1. [Runtime and Compiler](performance.md#runtime-and-compiler)
30 | 1. [Unsafe](performance.md#unsafe)
31 | 1. [Common gotchas with the standard library](performance.md#common-gotchas-with-the-standard-library)
32 | 1. [Alternate Implementations](performance.md#alternate-implementations)
33 | 1. [CGO](performance.md#cgo)
34 | 1. [Advanced Techniques](performance.md#advanced-techniques)
35 | 1. [Assembly](performance.md#assembly)
36 | 1. [Optimizing an Entire Service](performance.md#optimizing-an-entire-service)
37 | 1. [Tooling](performance.md#tooling)
38 | 1. [Profiling](performance.md#introductory-profiling)
39 | 1. [Tracer](performance.md#tracer)
40 | 1. Appendix
41 | 1. [Implementing Research Papers](performance.md#appendix-implementing-research-papers)
42 |
43 | ### Contributing
44 |
45 | This is a work-in-progress book in Go performance.
46 |
47 | There are different ways to contribute:
48 |
49 | 1) add to or summarizes the resources in TODO
50 | 2) add bullet points or new topics to be covered
51 | 3) write prose and flesh out the sections in the book
52 |
53 | Eventually sample programs to optimize and exercises will be needed (maybe).
54 |
55 | Coordination will be done in the #performance channel on the Gophers slack.
56 |
57 |
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 |
2 | * blog posts
3 | - http://jmoiron.net/blog/go-performance-tales/
4 | - use integer map keys if possible
5 | - hard to compete with Go's map implementation; esp. if your data structure has lots of pointer chasing
6 | - aes-ni instructions make string hashing much faster
7 | - prefer structs to maps if you know the map keys (esp. coming from perl, etc)
8 | - channels are useful, but slow; raw atomics can help with performance
9 | - cgo has overhead
10 | - profile before optimizing
11 | - http://slideshare.net/cloudflare/go-profiling-john-graham-cumming ( https://www.youtu.be/_41bkNr7eik )
12 | - don't waste programmer cycles saving the wrong CPU cycles (or memory allocations)
13 | - bash$ time; time.Now()/time.Since(); pprof.StartCPUProfile/pprof.StopCPUProfile; go tool pprof http://.../profile
14 | - bash$ ps; runtime.ReadMemStats(); runtime.WriteHeapProfile(); go tool pprof http://.../heap
15 | - slice operations are sometimes O(n)
16 | - https://golang.org/pkg/runtime/debug/
17 | - sync.Pool (basically)
18 | - https://methane.github.io/2015/02/reduce-allocation-in-go-code
19 | - 1. correctness is important
20 | - 2. BenchmarkXXX with b.ReportAllocs() (or -benchmem when running)
21 | - 3. allocfreetrace=1 produces stack trace on every allocation
22 | - strategies:
23 | - avoid string concat; use []byte+append() (+strconv.AppendInt(), ...)
24 | - benchcmp
25 | - avoid time.Format
26 | - avoid range when iterating strings ([]rune conversion + utf8 decoding)
27 | - can append string to []byte
28 | - write two versions, one for string, one for []byte (avoids conversion+copy (sometimes...))
29 | - reuse existing buffers instead of creating new ones
30 | - http://bravenewgeek.com/so-you-wanna-go-fast/
31 | - performance fast vs. delivery fast; make the right decision
32 | - lock-free ring buffer vs. channels: faster except with GOMAXPROCS=1
33 | - defer has a cost (allocation+cpu)
34 | BenchmarkMutexDeferUnlock-8 20000000 96.6 ns/op
35 | BenchmarkMutexUnlock-8 100000000 19.5 ns/op
36 | - reflection+json
37 | - ffjson avoids reflection
38 | - msgp avoids json
39 | - interfaces have dynamic dispatch which can't be inlined
40 | - => use concrete types (+ code duplication)
41 | - heap vs. stack; escape analysis
42 | - lots of short-lived objects is expensive for the gc
43 | - sync.Pool reuses objects *between* gc runs
44 | - you need your own free list to hold onto things between gc runs
45 | (but now you're subverting the purpose of a garbage collector)
46 | - false sharing
47 | - custom lock-free data structures: fast but *hard*
48 | - "Speed comes at the cost of simplicity, at the cost of development time, and at the cost of continued maintenance. Choose wisely."
49 | - https://software.intel.com/en-us/blogs/2014/05/10/debugging-performance-issues-in-go-programs
50 | - http://blog.golang.org/profiling-go-programs
51 | - https://medium.com/%40hackintoshrao/daily-code-optimization-using-benchmarks-and-profiling-in-golang-gophercon-india-2016-talk-874c8b4dc3c5
52 | - If you're writing benchmarks, read http://dave.cheney.net/2013/06/30/how-to-write-benchmarks-in-go
53 | - cache line explanation: http://mechanitis.blogspot.com/2011/07/dissecting-disruptor-why-its-so-fast_22.html
54 | - avoiding false sharing: http://www.drdobbs.com/parallel/eliminate-false-sharing/217500206
55 | - how does this translate to go? http://www.catb.org/esr/structure-packing/
56 | - https://en.wikipedia.org/wiki/Amdahl%27s_law
57 | - https://github.com/ardanlabs/gotraining/tree/master/topics/profiling
58 | - https://github.com/ardanlabs/gotraining/tree/master/topics/benchmarking
59 | - http://dave.cheney.net/2015/11/29/a-whirlwind-tour-of-gos-runtime-environment-variables
60 | - https://github.com/davecheney/high-performance-go-workshop
61 | - Mutex profile: https://rakyll.org/mutexprofile
62 | - https://segment.com/blog/allocation-efficiency-in-high-performance-go-services/
63 | - http://brendanjryan.com/2018/01/15/go-benchmarks.html
64 | - https://lemire.me/blog/2018/01/16/microbenchmarking-calls-for-idealized-conditions/
65 | - https://signalfx.com/blog/a-pattern-for-optimizing-go-2/
66 | - https://medium.com/@hackintoshrao/daily-code-optimization-using-benchmarks-and-profiling-in-golang-gophercon-india-2016-talk-874c8b4dc3c5
67 | - https://artem.krylysov.com/blog/2017/03/13/profiling-and-optimizing-go-web-applications/
68 | - https://segment.com/blog/allocation-efficiency-in-high-performance-go-services/
69 | - https://www.cockroachlabs.com/blog/how-to-optimize-garbage-collection-in-go/
70 | - https://hashrocket.com/blog/posts/go-performance-observations
71 | - https://lists.freebsd.org/pipermail/freebsd-current/2010-August/019310.html
72 | - https://marcellanz.com/post/file-read-challenge/
73 | - https://boyter.org/posts/sloc-cloc-code/
74 |
75 | cgo:
76 | cgo has overhead
77 | (which has only gotten more expensive over time) -- ~200 ns/call
78 | (reduced in 1.8 to <100ns; still not free)
79 | ssa backend means less difference in codegen
80 | really think if you want cgo: http://dave.cheney.net/2016/01/18/cgo-is-not-go
81 | https://www.youtube.com/watch?v=lhMhApWQp2E : cgo gophercon
82 | cgo performance tracking bug: https://github.com/golang/go/issues/9704
83 |
84 | videos:
85 | https://gophervids.appspot.com/#tags=optimization
86 | -- figure out which of these are specifically worth listing
87 |
88 | "Profiling and Optimizng Go" (Uber)
89 | https://www.youtube.com/watch?v=N3PWzBeLX2M
90 |
91 | https://go-talks.appspot.com/github.com/davecheney/presentations/writing-high-performance-go.slide
92 | https://www.youtube.com/watch?v=zWp0N9unJFc
93 |
94 | Björn Rabenstein
95 | https://docs.google.com/presentation/d/1Zu0BdbhMRar7ycEwDi8jepGokTXTDXlKFf7C13tusuI/edit
96 | https://www.youtube.com/watch?v=ZuQcbqYK0BY
97 |
98 | https://go-talks.appspot.com/github.com/mkevac/golangmoscow2016/gomeetup.slide
99 |
100 | CppCon 2014: Chandler Carruth "Efficiency with Algorithms, Performance with Data Structures"
101 | https://www.youtube.com/watch?v=fHNmRkzxHWs
102 |
103 | Performance Engineering of Software Systems
104 | https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-172-performance-engineering-of-software-systems-fall-2018/
105 |
106 | https://talks.golang.org/2013/highperf.slide#1
107 |
108 | Machine Architecture: Things Your Programming Language Never Told You
109 | https://www.youtube.com/watch?v=L7zSU9HI-6I
110 |
111 | 7 Ways to Profile Go Applications
112 | https://www.youtube.com/watch?v=2h_NFBFrciI
113 |
114 | dotGo 2016 - Damian Gryski - Slices: Performance through cache-friendliness
115 | https://www.youtube.com/watch?v=jEG4Qyo_4Bc
116 |
117 | Performance Bugs
118 | https://www.youtube.com/watch?v=89qiHoDjeDg
119 |
120 | The Hurricane's Butterfly: Debugging Pathologically Performing Systems
121 | https://www.youtube.com/watch?v=7AO4wz6gI3Q
122 |
123 | "So You Wanna Go Fast?" by Tyler Treat
124 | https://www.youtube.com/watch?v=DJ4d_PZ6Gns
125 |
126 | GopherCon 2017: Peter Bourgon - Evolutionary Optimization with Go
127 | https://www.youtube.com/watch?v=ha8gdZ27wMo
128 |
129 | CppCon 2015: Bryce Adelstein-Lelbach “Benchmarking C++ Code"
130 | https://www.youtube.com/watch?v=zWxSZcpeS8Q
131 |
132 | CppCon 2018: Fedor Pikus “Design for Performance”
133 | https://www.youtube.com/watch?v=m25p3EtBua4
134 |
135 | asm:
136 | https://golang.org/doc/asm
137 | https://goroutines.com/asm
138 | http://www.doxsey.net/blog/go-and-assembly
139 | https://www.youtube.com/watch?v=9jpnFmJr2PE
140 | https://blog.gopheracademy.com/advent-2016/peachpy/
141 | https://blog.sgmansfield.com/2017/04/a-foray-into-go-assembly-programming/
142 | http://lemire.me/blog/2016/12/21/performance-overhead-when-calling-assembly-from-go/
143 | http://davidwong.fr/goasm/
144 | minio posts + tooling
145 | https://github.com/teh-cmc/go-internals/blob/master/chapter1_assembly_primer/README.md
146 | https://blog.hackercat.ninja/post/quick_intro_to_go_assembly/
147 | https://quasilyte.dev/blog/post/go-asm-complementary-reference/
148 |
149 | posts:
150 | http://www.eecs.berkeley.edu/~rcs/research/interactive_latency.html
151 | https://arxiv.org/abs/1509.05053 (array layouts for comparison-based searching)
152 | http://grokbase.com/t/gg/golang-nuts/155ea0t5hf/go-nuts-after-set-gomaxprocs-different-machines-have-different-bahaviors-some-speed-up-some-slow-down
153 | http://grokbase.com/t/gg/golang-nuts/14138jw64s/go-nuts-concurrent-read-write-of-different-parts-of-a-slice
154 |
155 | Escape Analysis Flaws
156 | https://docs.google.com/document/d/1CxgUBPlx9iJzkz9JWkb6tIpTe5q32QDmz8l0BouG0Cw/preview
157 |
158 | https://hackernoon.com/optimizing-optimizing-some-insights-that-led-to-a-400-speedup-of-powerdns-5e1a44b58f1c
159 | http://leto.net/docs/C-optimization.php
160 |
161 | http://www.stochasticlifestyle.com/algorithm-efficiency-comes-problem-information/
162 |
163 | tools:
164 | https://godoc.org/github.com/aclements/go-perf
165 | https://godoc.org/x/perf/cmd/benchstat
166 | https://github.com/rakyll/gom
167 | https://github.com/tam7t/sigprof
168 | https://github.com/aybabtme/dpprof
169 | https://github.com/wblakecaldwell/profiler
170 | https://github.com/MiniProfiler/go
171 | https://perf.wiki.kernel.org/index.php/Main_Page
172 | https://github.com/dominikh/go-structlayout
173 | http://www.brendangregg.com/perf.html
174 | https://github.com/davecheney/gcvis
175 | https://github.com/pavel-paulau/gcterm
176 | https://github.com/jonlawlor/benchls
177 |
178 | pprof:
179 | https://rakyll.org/pprof-ui/
180 | https://rakyll.org/profiler-labels/
181 | https://rakyll.org/custom-profiles/
182 |
183 | trace:
184 | https://making.pusher.com/go-tool-trace/
185 | https://www.youtube.com/watch?v=mmqDlbWk_XA
186 | https://www.youtube.com/watch?v=nsM_m4hZ-bA
187 | https://blog.gopheracademy.com/advent-2017/go-execution-tracer/
188 |
189 | papers:
190 | https://www.akkadia.org/drepper/cpumemory.pdf
191 | https://software.intel.com/sites/default/files/article/392271/aos-to-soa-optimizations-using-iterative-closest-point-mini-app.pdf
192 |
193 | optimization guides:
194 | http://developer.amd.com/resources/developer-guides-manuals/
195 | http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.uan0015b/index.html
196 | https://www-ssl.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-optimization-manual.html
197 | https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines.html#S-performance
198 | https://github.com/fenbf/AwesomePerfCpp
199 | https://www.kernel.org/pub/linux/kernel/people/paulmck/perfbook/perfbook.2017.11.22a.pdf
200 |
201 | stackoverflow:
202 | https://stackoverflow.com/questions/19397699/why-struct-with-padding-fields-works-faster/19397791#19397791
203 | https://stackoverflow.com/questions/10017026/no-speedup-in-multithread-program/10017482#10017482
204 |
205 | practice:
206 | https://twitter.com/dgryski/status/584682584942194689
207 |
208 | distributed system design: (out of scope for this book)
209 | http://highscalability.com/blog/2010/12/20/netflix-use-less-chatty-protocols-in-the-cloud-plus-26-fixes.html
210 |
211 | books:
212 | Writing Efficient Programs
213 | Algorithm Engineering: https://www.springer.com/gp/book/9783642148651
214 | http://www.cs.tufts.edu/~nr/cs257/archive/don-knuth/empirical-fortran.pdf
215 |
216 | Usborne: Programming Tricks and Skills
217 | https://drive.google.com/file/d/0Bxv0SsvibDMTdElPMHF5NVpmU0U/view
218 |
219 | Quotes: (Bumper Sticker Computer Science)
220 | [The First Rule of Program Optimization] Don't do it.
221 | [The Second Rule of Program Optimization---For experts only] Don't do it yet.
222 | Michael Jackson
223 | Michael Jackson Systems Ltd.
224 |
225 | The key to performance is elegance, not battalions of special cases.
226 | — Jon Bentley and Doug McIlroy
227 |
228 | You’re bound to be unhappy if you optimize everything.
229 | — Donald Knuth
230 |
231 | You'll never know how bad things are until you look.
232 | - Howard Chu
233 |
--------------------------------------------------------------------------------
/performance-es.md:
--------------------------------------------------------------------------------
1 | # Escribiendo y optimizando código en Go
2 |
3 | Este documento describe las mejores prácticas para escribir código de alto rendimiento en Go.
4 |
5 | Si bien se discutirán maneras de optimizar servicios individuales (almacenamiento en caché, etc.), el diseño de sistemas distribuidos de alto rendimiento está fuera del alcance de este trabajo. Ya existen textos detallados sobre monitorización y diseño de sistemas distribuidos. Dicho tema abarca un conjunto completamente diferente de investigación y concesiones en el diseño.
6 |
7 | Todo el contenido está sujeto a licencia bajo CC-BY-SA.
8 |
9 | Este libro está dividido en diferentes secciones:
10 |
11 | 1. Consejos básicos para escribir software que no sea lento.
12 | * Temas básicos de Ciencias de la Computación
13 | 2. Consejos para escribir software eficiente.
14 | * Secciones específicas de Go sobre cómo obtener lo mejor del lenguaje
15 | 3. Consejos avanzados para escribir *software realmente* eficiente
16 | * Para cuando tu código optimizado no sea lo suficientemente eficiente.
17 |
18 | Podemos resumir estas tres secciones como:
19 |
20 | 1. "Sé razonable"
21 | 2. "Sé deliberado"
22 | 3. "Sé peligroso"
23 |
24 | ## Cuándo y dónde optimizar
25 |
26 | Escribo esta sección primero porque es el paso más importante. Deberías estar haciendo esto?
27 |
28 | Toda optimización tiene un coste. Generalmente, este coste se expresa en términos de complejidad de código o carga cognitiva -- un código optimizado es rara vez más simple que una versión sin optimizar.
29 |
30 | Pero hay otro aspecto que llamaré la economía de la optimización. Como programador, tu tiempo es valioso. Está el coste de oportunidad de otras cosas que podrías estar haciendo en tu proyecto, `bug` que podrías arreglar, mejoras que podrías agregar. Optimizar cosas es divertido, pero no siempre es la tarea correcta a hacer. El rendimiento de un programa es una característica, pero también lo es terminarlo y cuan correcto está hecho.
31 |
32 | Escoge lo más importante en lo que debas trabajar. A veces esto no es una optimización del uso de CPU, sino una de experiencia de usuario. Algo tan simple como agregar una barra de progreso, o modificar una página para que sea más veloz ejecutando cálculos en segundo plano después de mostrarla.
33 |
34 | Algunas veces esto será obvio: un informe que se genera cada hora y tarda 3 horas en completar es menos útil que uno que termina en menos de una hora.
35 |
36 | Sólo porque algo sea fácil de optimizar no significa que valga la pena hacerlo. Ignorar lo simple es una estrategia de desarrollo válida.
37 |
38 | Considera esto como optimizar *tu* tiempo.
39 |
40 | Tú decides qué optimizar y cuándo optimizar. Puedes mover la manilla entre "Software Veloz" y "Desarrollo Rápido".
41 |
42 | Las personas escuchan y repiten sin pensar que "la optimización prematura es la raíz de todo mal", pero ellos ignoran el contexto completo de la frase.
43 |
44 | "Los Programadores gastan una enorme cantidad de tiempo pensando, o preocupandose, por la velocidad de las partes no críticas de sus programas, y estos intentos por ser más eficientes en realidad causan un gran impacto negativo cuando el mantenimiento o la depuración son considerados. Debemos olvidarnos de las pequeñas eficiencias, digamos el 97% del tiempo: la optimización prematura es la raíz de todo mal. Sin embargo, no deberíamos dejar pasar la oportunidad de optimizar en ese otro 3% crítico." -- Knuth
45 |
46 | https://www.youtube.com/watch?time_continue=429&v=3WBaY61c9sE
47 |
48 | - No ignores las optimizaciones fáciles
49 | - Más conocimiento de algoritmos y estructuras de datos hacen que la optización sea más "fácil" u "obvia"
50 |
51 | "Deberías optimizar tu código? Si, pero sólo si el problema es importante, el programa es genuinamente lento, y si hay alguna expectativa de que se puede mejorar mientras se mantenga la exactitud, robustez, y claridad" -- The Practice of Programming, Kernighan and Pike
52 |
53 | La optimización prematura también puede afectarte al atarte a ciertas decisiones. El código final puede ser más difícil de modificar si los requerimientos cambian y más difícil de desechar (falacia de coste) si es necesario.
54 |
55 | [La estimación de desempeño BitFunnel](http://bitfunnel.org/strangeloop) muestra datos que hacen este equilibrio más explícito. Imagina una plataforma de búsqueda hipotética que necesite 30.000 servidores en varios centros de datos. Estos servidores tienen un coste aproximado de $1.000 USD por año. Si duplicaras la velocidad del software, este cambio puede ahorrarle a la compañia $15M USD por año. Incluso un solo desarrollador trabajando un año completo para mejorar el rendimiento por solo 1% valdría la pena.
56 |
57 | En la gran mayoría de casos, el tamaño y velocidad del programa no es el problema. La optimización más fácil es no hacerla. La segunda alternativa más fácil es simplemente comprar mejor hardware.
58 |
59 | Cuando hayas decidido cambiar tu programa, sigue leyendo.
60 |
61 | ## Cómo optimizar
62 |
63 | ### Flujo de trabajo de optimización
64 |
65 | Antes de entrar en detalle, hablemos del proceso general de optimización.
66 |
67 | La optimización es una forma de refactoring. Pero cada paso, en vez de mejorar algún aspecto del código (código duplicado, claridad, etc...), mejora algún aspecto relacionado con el rendimiento: menor uso de CPU, de memoria, latencia, etc... Estas mejoras generalmente se hacen a costa de la legibilidad. Esto supone que además de un completo conjunto de pruebas unitarias (para garantizar que tus cambios no han roto nada), necesitarás un buen conjunto de benchmarks para garantizar que tus cambios tienen el efecto deseado sobre el rendimiento. Tienes que ser capaz de verificar que tu cambio realmente *está* reduciendo el uso de CPU. A veces, un cambio que pensabas iba a mejorar el rendimiento realmente no tiene impacto o tiene un impacto negativo. Asegúrate de deshacer tus cambios en estos casos.
68 |
69 | [What is the best comment in source code you have ever encountered? - Stack Overflow](https://stackoverflow.com/questions/184618/what-is-the-best-comment-in-source-code-you-have-ever-encountered):
70 |
71 | //
72 | // Dear maintainer:
73 | //
74 | // Once you are done trying to 'optimize' this routine,
75 | // and have realized what a terrible mistake that was,
76 | // please increment the following counter as a warning
77 | // to the next guy:
78 | //
79 | // total_hours_wasted_here = 42
80 | //
81 |
82 |
83 | Los benchmarks que estés usando deben ser correctos y generar medidas reproducibles con cargas de trabajo representativas. Si las ejecuciones individuales tienen variaciones muy altas, hará muy difícil identificar pequeñas mejoras. Necesitarás usar [benchstat](https://golang.org/x/perf/benchstat) o similares tests estadísticos y no podrás valorar los cambios a ojo.
84 |
85 | (Ten en cuenta que el uso de tests estadísticos es una buena idea en cualquier caso). Los pasos para ejecutar los benchmarks deben estar documentandos, y cualquier script y herramienta personalizada debe ser añadida al repositorio con instrucciones para ejecutarlos. Se cuidadoso con los grandes conjuntos de benchmarks que tardan mucho en ejecutarse: harán el proceso de desarollo más lento.
86 |
87 | Ten también en cuenta que cualquier cosa que puede ser medida puede ser optimizada. Asegúrate de estar midiendo lo correcto.
88 |
89 | El siguiente paso es decidir qué vas a optimimizar. Si el objetivo es mejorar el uso de CPU, ¿cuál es una velocidad aceptable? ¿Quieres mejorar el rendimiento actual por 2? ¿Por 10? ¿Puedes describirlo como "un problema de tamaño N en un tiempo menor a T"? ¿Estás intentando reducir el uso de memoria? ¿Por cuánto? ¿Cuánto más lento es aceptable a cambio de reducir el uso de memoria? ¿Qué estás dispuesto a perder a cambio de reducir los requerimientos de espacio?
90 |
91 | Optimizar la latencia de un servicio es más complejo. Se han escrito libros enteros explicando como probar el rendimiento de servidores web. El principal problema es que para una única función, el rendimiento es bastante consistente para un problema de un tamaño dado. Para servicios web no tienes un único número. Un conjunto de benchmarks adecuado para un servicio web proporcionará una distribución de latencia para un determinado nivel de peticiones/segundo. Esta charla da una buena visión general de algunos de estos temas:
92 | ["How NOT to Measure Latency" by Gil Tene](https://youtu.be/lJ8ydIuPFeU)
93 |
94 | TODO: See the later section on optimizing web services
95 |
96 | Los objetivos de rendimientos deben ser específicos. (Casi) siempre serás capaz de hacer que algo sea más rapido. Optimizar es frecuentemente un juego de rendimientos decrecientes. Debes saber cuando parar. ¿Cuánto esfuerzo vas a dedicar para conseguir ese pequeño paso final? ¿Cuánto estás dispuesto a ensuciar y a dificultar el mantenimiento del código?
97 |
98 | La charla de Dan Luu que se menciona más arriba sobre [estimación del rendimiento de BitFunnel](http://bitfunnel.org/strangeloop) muestra un ejemplo del uso de cálculos aproximados para determinar si tus objetivos de rendimiento son razonables.
99 |
100 | TODO: Programming Pearls has "Fermi Problems". Knowing Jeff Dean's slide helps.
101 |
102 | Cuando se empieza el desarrollo, no debes dejar el benchmarking y los objetivos de rendimiento para el final. Es fácil decir "arreglaremos esto más adelante", pero si el rendimiento es realmente importante, será una consideración de diseño desde el principio. Cualquier cambio significativo en la arquitectura requerido para arreglar problemas de rendimiento será demasiado arriesgado cerca de la fecha límite. Ten en cuenta que *durante* el desarrollo, el foco debe estar en diseños, algoritmos y estructura de datos razonables. Optimizar los niveles más bajos del stack debe esperar hasta más adelante en el ciclo de desarrollo cuando se tenga una visión más completa del rendimiento del sistema. Cualquier perfilado global del sistema que se haga cuando el sistema esté incompleto dará una vista sesgada de donde estarán los cuellos de botella una vez que el sistema se finalize.
103 |
104 | TODO: How to avoid/detect "Death by 1000 cuts" from poorly written software.
105 | Solution: "Premature pessimization is the root of all evil". This matches with
106 | my Rule 1: Be deliberate. You don't need to write every line of code
107 | to be fast, but neither should by default do wasteful things.
108 |
109 | "Pesimismo prematuro es cuando escribes código que es más lento de lo que debería ser, generalmente porque se realiza trabajo adicional innecesario, cuando un código con un nivel de complejidad equivalente será más rápido y fluye de namera natural de tus dedos." -- Herb Sutter
110 |
111 | Incluir benchmarking como una parte de CI es difícil debido a los vecinos ruidosos e incluso en el caso de no tener vecinos a diferencias en los entornos de CI. Es difícil controlar las métricas de rendimiento. Un buen término medio es tener benchmarks que son ejecutadas por el desarrollador (en hardware adecuado) e incluidas en el mensaje de commit para commits que tratan especificamente con temas de rendimiento. Para aquellos commits más generales, intenta detectar problemas de rendimiento "a ojo" en la revisión del código.
112 |
113 | TODO: how to track performance over time?
114 |
115 | Escribe código para el que puedas aplicar benchmark. El perfilado se puede hacer en sistemas de mayor tamaño. Mediante benchmarking quieres probar partes aisladas. Debes poder extraer y preparar un contexto suficente como para que los benchmarks prueben lo necesario y sean representativos.
116 |
117 | La diferencia entre tu redimiento objetivo y tu rendimiento actual te dará una idea de por donde empezar. Si sólo necesitas una mejora de un 10-20%, probablemente podrás conseguirla con algunos ajustes en la implementación y pequeños arreglos. Si necesitas mejorar por un factor de 10x o más, entonces no lo vas a conseguir reemplazando una multiplicación con un left-shift. En este caso probablemente tengas que hacer cambios de arriba a abajo en tu stack, posiblemente rediseñando grandes partes del sistema teniendo los objetivos de rendimiento en mente.
118 |
119 | Un buen trabajo que afecte al rendimiento requiere conocimiento en muchos niveles diferentes, desde el diseño de sistemas, redes, hardware (CPU, caches, almacenamiento), algoritmos, tuning y debugging. Con tiempo y recursos limitados, considera en que nivel tendrás mayores mejoras: no siempre será un cambio de algoritmo o tuning del programa.
120 |
121 | En general, las optimizaciones deben hacerse de arriba a abajo. Optimizaciones a nivel de sistema tendrán más impacto que optimizaciones a nivel de expresión. Asegúrate de estar resolviendo el problema en el nivel apropiado.
122 |
123 | Este libro va a hablar sobre todo de como reducir el uso de CPU, el uso de memoria y la latencia. Es importante indicar que raramente podrás conseguir las tres cosas. Quizás el tiempo de CPU sea más rapido, pero ahora tu programa usa más memoria. Quizás necesites reducir el espacio en memoria, pero ahora el programa tarda más.
124 |
125 | [La ley de Amdahl](https://en.wikipedia.org/wiki/Amdahl%27s_law) dice que pongamos el foco en los cuellos de botella. Si doblas la velocidad de una rutina que solo ocupa el 5% del tiempo de ejecución, solo resulta en una mejora total del 2.5%. Por otro lado, acelerar una rutima que ocupa el 80% del tiempo en tan solo un 10%, mejorará el tiempo de ejecución en casi un 8%. El perfilado te ayudará a identificar donde realmente se ocupa el tiempo.
126 |
127 | Al optimizar, quieres reducir la cantidad de trabajo que la CPU tiene que hacer. Quick sort es más rapido que Bubble sort porque resuelve el mismo problema (ordenar) en menos pasos. Es un algoritmo más eficiente. Has reducido el trabajo que la CPU tiene que hacer para conseguir el mismo resultado.
128 |
129 | El tuning de un programa, como las optimizaciones del compilador, generalmente solo resultará en una pequeña reducción del tiempo de ejecución total. Las grandes ganancias vendrán casi siempre de un cambio de algoritmo o de un cambio en la estructura de datos, un cambio fundamental en cómo está organizado tu programa. [La ley de Proebsting](http://proebsting.cs.arizona.edu/law.html) dice que los compiladores doblan su rendimientos cada 18 *años*, un contraste evidente con la (ligeramente malinterpretada) Ley de Moore que dice que el rendimiento del procesador se dobla cada 18 *meses*. Las mejoras en los algoritmos funcionan en magnitudes mayores. Los algoritmos de mixed integer programming, [mejoraron en un factor de 30.000 entre 1991 y 2008](https://agtb.wordpress.com/2010/12/23/progress-in-algorithms-beats-moore%E2%80%99s-law). Para un ejemplo más concreto, considera [este análisis](https://medium.com/@buckhx/unwinding-uber-s-most-efficient-service-406413c5871d) sobre reemplazar un algoritmo geoespacial de fuerza bruta descrito en un blog post de Uber con una especialización más adecuada para la tarea presentada. No hay un cambio en el compilador que resulte en un incremento equivalente del rendimiento.
130 |
131 | TODO: Optimizing floating point FFT and MMM algorithm differences in gttse07.pdf
132 |
133 | Un profiler quizás te muestre que se pasa mucho tiempo en una rutina en particular. Puede ser que sea una rutina pesada o puede ser una rutina ligera que se llama muchas, muchas veces. En lugar de inmediatamente intentar acelerar esta rutina, mira si puedes reducir el número de veces que se llama o eliminarla completamente. Discutiremos estrategias de optimización más concretas en la siguiente sección.
134 |
135 | Las Tres Preguntas de Optimización:
136 |
137 | * ¿Hace falta hacer esto? El código más rapido es aquel que nunca se ejecuta.
138 | * Si sí, ¿es este el mejor algoritmo?.
139 | * Si sí, ¿es esta la mejor *implementación* de este algoritmo?.
140 |
141 | ## Consejos concretos para optimizar
142 |
143 | La obra de 1982 de Jon Bentley "Writing Efficient Programs", abordó la optimización de programas como un problema de ingeniería: Mide. Analiza. Mejora. Verifica. Itera. Unos cuantos de sus consejos son aplicados automaticamente por los compiladores. El trabajo de un programador es aplicar las transformaciones que los compiladores *no pueden* hacer.
144 |
145 | Hay resumenes del libro:
146 |
147 | *
148 | *
149 |
150 | y de las reglas de tuning de programas:
151 |
152 | *
153 |
154 | Cuando pienses en los cambios que puedes hacer en tu programa, hay dos opciones básicas: puedes modificar los datos o puedes modificar tu código.
155 |
156 | ### Modificar los datos
157 |
158 | Modificar los datos significa agregar o alterar la representación de los datos
159 | que estas procesando. Desde el punto de vista del rendimiento, algunos de
160 | estos acabaran cambiando la complejidad O() asociada a diferentes aspectos de
161 | la estructura de datos.
162 |
163 | Ideas para mejorar tu estructura de datos:
164 |
165 | * Campos adicionales
166 |
167 | El clásico ejemplo de esto es almacenar la longitud de una lista enlazada en un
168 | campo en el nodo raíz. Mantenerla actualizada conlleva un poco más de trabajo,
169 | pero consultar la longitud se convierte en un simple acceso a un campo en vez de una
170 | búsqueda transversal con complejidad O(n). Tu estructura de datos puede presentar una
171 | mejora similar: un poco de mantenimiento en algunas operaciones a cambio de
172 | mejorar el rendimiento en un caso de uso común.
173 |
174 | De manera similar, almacenar punteros a nodos frecuentemente utilizados en vez
175 | de realizar búsquedas adicionales. Esto cubre cosas como el link "hacia atrás"
176 | en una lista doblemente enlazada para hacer que la eliminación de nodos tenga una complejidad O(1).
177 | Algunas listas de salto guardan un "puntero de búsqueda", donde almacenas un
178 | puntero a donde recientemente estuviste en tu estructura de datos, bajo la
179 | suposición de que es un buen punto de partida para la siguiente operación.
180 |
181 | * Índices de búsqueda adicionales
182 |
183 | La mayoría de las estructuras de datos están diseñadas para un único tipo de
184 | consulta. Si necesitas dos tipos de consultas, disponer de una "vista"
185 | adicional a tus datos puede ser una gran mejora. Por ejemplo, un set de
186 | structs puede tener un ID primario (integer) que usas para buscar en un
187 | slice, pero a veces necesitas buscar por un ID secundario (string). En lugar
188 | de iterar sobre el slice, puedes mejorar tu estructura de datos con un mapa de
189 | string a ID o directamente al struct en cuestión.
190 |
191 | * Información adicional sobre los elementos
192 |
193 | Por ejemplo, mantener un filtro de Bloom de todos los elementos que has
194 | insertado puede permitirte retornar rápidamente "sin coincidencias" a las
195 | búsquedas. Estos necesitan ser pequeños y rápidos para no abrumar el resto de
196 | la estructura de datos. (Si una búsqueda en tu estructura de datos principal
197 | es barata, el costo del filtro de Bloom superará cualquier ahorro.)
198 |
199 | * Si las búsquedas son costosas, agrega una cache
200 |
201 | A mayor escala, una cache interna o externa (como memcache) puede ayudar.
202 | Puede ser excesivo para una única estructura de datos. Hablaremos más sobre
203 | caches más adelante.
204 |
205 | Este tipo de cambios son útiles cuando los datos que necesitan son baratos de
206 | almacenar y fáciles de mantener actualizados.
207 |
208 | Estos son todos ejemplos claros de "realizar menos trabajo" a nivel de
209 | la estructura de datos. Todos cuestan espacio. La mayoría de las veces, si estás
210 | optimizando para CPU, tu programa usará más memoria. Se trata del clásico [space-time trade-off](https://en.wikipedia.org/wiki/Space%E2%80%93time_tradeoff).
211 |
212 | Si tu programa utiliza demasiada memoria, también es posible ir por el otro
213 | camino. Reduce el uso de espacio a cambio de una mayor carga computacional. En
214 | lugar de almacenar cosas, calcúlalas cada vez. También puedes comprimir los datos
215 | en memoria y descomprimirlos cuando los necesites.
216 |
217 | [Small Memory Software](http://smallmemory.com/book.html) es un libro disponible
218 | online que cubre técnicas para reducir el espacio utilizado por tus programas.
219 | Aunque fue originalmente escrito dirigído a desarrolladores de sistemas
220 | embebidos, sus ideas son aplicables para programas en hardware moderno que
221 | manejen gran cantidad de datos.
222 |
223 | * Reorganiza tus datos
224 |
225 | Elimina el padding. Remueve campos extra. Utiliza tipos de datos más pequeños.
226 |
227 | * Cambia a una estructura de datos más lenta
228 |
229 | Estructuras de datos más simples frecuentemente presentan menores
230 | requerimientos de memoria. Por ejemplo, cambiar una estructura tipo arbol con
231 | uso extensivo de punteros a un slice y búsqueda lineal.
232 |
233 | * Compresión a medida para tus datos
234 |
235 | Los algoritmos de compresión dependen fuertemente de qué esté siendo comprimido. Lo mejor es
236 | elegir uno que se ajuste a tus datos. Si tienes un []byte, entonces algo como snappy, gzip, lz4,
237 | funciona bien. Para datos de punto flotante existe go-tsz para series temporales y fpc para datos
238 | científicos. Se ha realizado mucha investigación sobre la compresión de integers, generalmente
239 | para la obtención de datos en motores de búsqueda. Algunos ejemplos son delta encoding y variantes
240 | o esquemas más complejos que involucran diferencias xor codificadas con el algoritmo de Huffman.
241 | También puedes usar tu propio algoritmo optimizado exactamente para tus datos.
242 |
243 | ¿Necesitas inspeccionar los datos o pueden permanecer comprimidos? ¿Necesitas acceso aleatorio o
244 | sólo streaming? Si necesitas acceso a entradas individuales pero no quieres descomprimirlo todo,
245 | puedes comprimir los datos en pequeños bloques y mantener un índice que indique qué rangos de
246 | entradas hay en cada bloque. El acceso a una única entrada solo requiere consultar el
247 | índice y descomprimir ese bloque pequeño.
248 |
249 | Si tus datos no estan sólo en memoria, sino que vas a escribirlos a disco, ¿qué sucede con la
250 | migración o agregár o eliminár campos?. Estarás ahora lidiando simplemente con []byte en vez de
251 | los convenientes tipos estructurados de Go, por lo que necesitaras hacer uso del paquete unsafe
252 | y considerar opciones de serialización.
253 |
254 | Hablaremos más sobre la disposición de datos más adelante.
255 |
256 | Las computadoras modernas y la jerarquía de memoria hacen que el compromiso
257 | entre memoria/espacio sea menos claro. Es muy fácil para las tablas de búsqueda estar
258 | "lejos" en memoria (y por lo tanto ser costosas de acceder) haciendo que sea más rápido
259 | simplemente recalcular un valor cada vez que se necesita.
260 |
261 | Esto también significa que el benchmarking frecuentemente mostrará mejoras que no
262 | aparecen en producción debido a la contención de cache (e.g., tablas de
263 | búsqueda que estan en la cache del procesador durante el benchmarking pero
264 | que siempre son purgadas cuando son utilizadas por un sistema real.)
265 | El [Jump Hash paper](https://arxiv.org/pdf/1406.2294.pdf) de Google de hecho
266 | abordó esto directamente, comparando la performance de caché con y sin problemas
267 | de contención. (Ver gráficos 4 y 5 del artículo)
268 |
269 | TODO: how to simulate a contended cache, show incremental cost
270 |
271 | Otro aspecto a considerar es el tiempo de transmisión de datos. Generalmente el
272 | acceso a disco y de red es muy lento, y por lo tanto ser capaz de cargar un
273 | bloque comprimido va a resultar en un proceso mucho más rápido incluso teniendo
274 | en cuenta el tiempo que lleva descomprimirlo. Como siempre, realiza benchmarks.
275 | Un formato binario generalmente va a ser más liviano y rápido de parsear que uno
276 | de texto, pero el coste es que no será legible para un humano.
277 |
278 | Para la transferencia de datos, cambia a un protocol menos verboso, o
279 | mejora el API para aceptar consultas parciales. Por ejemplo, usa una consulta
280 | incremental en lugar de forzar a traer siempre el set de datos completo.
281 |
282 | ### Modificar los algoritmos
283 |
284 | Si no estás modificando los datos, la alternativa más importante es modificar el código.
285 |
286 | Es muy posible que las mejoras más importantes vengan de un cambio de algoritmo. Esto es equivalente a sustituir bubble sort (`O(n^2)`) con quicksort (`O(n log n)`) o reemplazar un acceso linear a un array (`O(n)`) con una búsqueda binaria (`O(log n)`) o una búsqueda en un mapa (`O(1)`).
287 |
288 | Así es como el software se vuelve lento. Estructuras originalmente diseñadas para un propósito se reusan para algo que no habían sido diseñadas. Esto ocurre gradualmente.
289 |
290 | Es importante tener un entendimiento intuitivo de los diferentes niveles de big-O. Elige la estructura de datos para tu problema. Esto no siempre ahorra ciclos de CPU, pero previene problemas de rendimiento que pueden no ser detectados hasta mucho más adelante.
291 |
292 | Las clases básicas de complejidad son:
293 |
294 | * O(1): acceso a un campo, un array o un mapa
295 |
296 | Consejo: no te preocupes por ellos
297 |
298 | * O(log n): búsqueda binaria
299 |
300 | Consejo: sólo es un problema si se hace en un bucle
301 |
302 | * O(n): bucle simple
303 |
304 | Consejo: lo haces todo el tiempo
305 |
306 | * O(n log n): divide y vencerás, ordenación
307 |
308 | Consejo: sigue siendo bastante rapido
309 |
310 | * O(n\*m): bucles anidados / cuadrático
311 |
312 | Consejo: ten cuidado y limita el tamaño de tu conjunto de datos
313 |
314 | * Cualquier cosa entre cuadrático y subexponencial
315 |
316 | Consejo: no lo ejecutes en un millón de filas
317 |
318 | * O(b ^ n), O(n!): exponencial y mayor
319 |
320 | Consejo: vas a necesitar suerte si tienes más de una o dos docenas de datos
321 |
322 | Link:
323 |
324 | Supongamos que tienes que buscar en un conjunto desordenado de datos. "Debería usar búsqueda binaria" piensas, sabiendo que una búsqueda binaria es O(log n) que es más rapido que el O(n) de una búsqueda linear. Sin embargo, una búsqueda binaria requiere que los datos estén ordenados, lo que significa que tendrás que ordenarlos antes, que tarda O(n log n). Si haces muchas búsquedas, el coste inicial de la ordenación merecerá la pena. Pero, si sobre todo estas haciendo **lookups**, quizás usar un array fue una decisión equivocada y sería mejor usar un mapa con coste O(1).
325 |
326 | Si tu estructura de datos es estática, entonces generalmente podrás hacerlo mucho mejor que en el caso de que fuera dinámica. Resultará más facil construir una estructura de datos óptima para tus patrones de búsqueda. Soluciones como minimal perfect hashing pueden tener más sentido aquí, o filtros de Bloom precalculados. Esto también tiene sentido si tu estructura de datos es "estática" durante un periodo largo de manera que puedas amortizar el coste inicial de su construcción en muchas búsquedas.
327 |
328 | Escoje la estructura de datos más simple que sea razonable y continúa. Esto es elemental para escribir "software no lento". Este debe ser tu modo de desarrollar por defecto. Si sabes que necesitas acceso aleatorio, no escojas una lista enlazada. Si sabes
329 | que necesitas recorrer los datos en orden, no uses un mapa. Los requerimientos cambian
330 | y no siempre puedes averiguar el futuro. Haz una suposición razonable de la carga de trabajo.
331 |
332 |
333 |
334 | Estructuras de datos para problemas similares diferirán cuando hagan una parte de su trabajo. Un árbol binario se ordena a medida que se insertan elementos. Un array no-ordenado es más rapido al insertar pero no está ordenado: al acabar, para "finalizar", tienes que hacer la ordenación.
335 |
336 | Cuando escribas un paquete para ser usado por otros, evita la tentación de optimizar por adelantado para cada caso de uso individual. Esto resultará en código ilegible. Las estructura de datos tienen por diseño un solo proposito. No puedes ni leer mentes ni predecir el futuro. Si un usuario dice "Tu paquete es demasiado lento para este caso de uso", una respuesta razonable puede ser "Entonces usa este otro paquete". Un paquete debe "hacer una cosa bien".
337 |
338 | A veces, estructuras de datos hibridas proveerán las mejoras de rendimiento que necesitas. Por ejemplo, agrupando tus datos puedes limitar tu búsqueda a una sola agrupación. Esto todavía tiene un coste teórico de O(n), pero la constante será más pequeña. Volveremos a visitar estos tipos de ajustes cuando lleguemos a la parte de afinar programas.
339 |
340 | Dos cosas que la gente olvida cuando se discuten notaciones big-O:
341 |
342 | Primero, hay un factor constante. Dos algoritmos que tienen la misma complejidad algorítmica pueden tener diferentes factores constantes. Imagina que iteras una lista 100 veces frente a iterar una sola vez. Aunque ambas son O(n), una de ellas tiene un factor constante que es 100 veces mayor.
343 |
344 | Estos factores constantes explican que aunque merge sort, quicksort y heapsort son todos O(n log n), todo el mundo use quicksort porque es el más rapido. Tiene el factor constante más pequeño.
345 |
346 | La segunda cosa es que big-O solo dice "a medida que n crece a infinito". Habla de la tendencia de crecimiento, "A medida que los números crezcan, este es el factor de crecimiento que dominará el tiempo de ejecución". No dice nada sobre el rendimiento real o sobre como se comporta cuando n es pequeño.
347 |
348 | Con frecuencia hay un punto de corte por debajo del cual un algoritmo más tonto es más rápido. Un buen ejemplo del paquete `sort` de la librería estandar de Go. La mayoría del tiempo usa quicksort, pero hace una pasada con shell sort y luego con insertion sort cuando el tamaño de la partición está por debajo de 12 elementos.
349 |
350 | Para algunos algoritmos, el factor constante puede ser tan grande que este punto de corte puede ser mayor que cualquier input razonable. Esto es, el algoritmo O(n^2) es más rapido que el algoritmo O(n) para cualquier input con el que te vayas a encontrar.
351 |
352 | Esto también significa que necesitas tener muestras representativas del tamaño de tu input tanto para escoger el algoritmo más apropiado como para escribir buenos benchmarks. ¿10 elementos? ¿1000 elementos? ¿1000000 elementos?
353 |
354 | Esto también funciona en sentido contrario: por ejemplo, escoger una estructura de datos más compleja para obtener un crecimiento O(n) en lugar de O(n^2), aunque los benchmarks para inputs más pequeños sean más lentos. Esto también aplica para la mayoría de estructuras de datos que son lock-free. Son generalmente más lentas cuando se usan en un sólo hilo pero más escalables cuando hay muchos hilos usándolas.
355 |
356 | La jerarquía de memoria en los ordenadores modernos confunde un poco el tema, en el sentido de que las caches prefieren el predecible acceso linear al recorrer un slice que el acceso aleatorio de seguir un puntero. Aún así, es mejor empezar con un buen algoritmo. Hablaremos más de esto en la sección sobre hardware.
357 |
358 | > La pelea no siempre la ganará el más fuerte, ni la carrera el más rapido, pero esa es es la manera de apostar. -- Rudyard Kipling
359 |
360 | A veces el mejor algoritmo para un problema específico no es un único algoritmo, sino un conjunto de algoritmos especializados en tipos de input ligeramente diferentes. Este "polialgoritmo" primero detecta el tipo de input que tiene que tratar y luego sigue el code path apropiado. De esta manera funciona el paquete `sort` mencionado anteriormente: determina el tamaño del problema y elige un algoritmo distinto. Además de combinar quicksort, shell sort e insertion sort, también controla el nivel de recursividad de quicksort y usa heapsort si es necesario. Los paquetes `string` y `bytes` hacen algo similar, detectando y especializando para diferentes casos. Como con la compresión de datos, cuanto más sepas sobre las características de tu input, mejor será tu solución especifica. Incluso si una optimización no siempre se puede aplicar, complicar tu código determinando que es seguro de usar y ejecutando una lógica diferente puede valer la pena.
361 |
362 | Esto también aplica a los subproblemas que tu algoritmo tiene que solucionar. Por ejemplo, poder usar radix sort puede tener un impacto significativo en el rendimiento, o usar quicksort si sólo necesitas una ordenación parcial.
363 |
364 | A veces, en vez de una especialización para tu tarea, el mejor enfoque es abstraer la tarea a un categoría de problemas más general que ya haya sido estudiada. Así podrás aplicar la solución más general a tu caso concreto. Mapear tu problema a un dominio con implementaciones bien estudiadas puede resultar en una ganancia significativa.
365 |
366 | De manera similar, usar un algoritmo más simple significa que es más probable que las concesiones, analisis y detalles de la implementación hayan sido más estudiados y sean mejor entendidos que en otros algoritmos más esótericos, exóticos y complejos.
367 |
368 | Los algoritmos más simples pueden ser más rápidos. Estos dos ejemplos no son casos aislados:
369 | https://go-review.googlesource.com/c/crypto/+/169037
370 | https://go-review.googlesource.com/c/go/+/170322/
371 |
372 | TODO: notes on algorithm selection
373 |
374 | TODO:
375 | improve worst-case behaviour at slight cost to average runtime
376 | linear-time regexp matching
377 | randomized algorithms: MC vs. LV
378 | improve worse-case running time
379 | skip-list, treap, randomized marking,
380 | primality testing, randomized pivot for quicksort
381 | power of two random choices
382 | statistical approximations (frequently depend on sample size and not population size)
383 |
384 | TODO: batching to reduce overhead: https://lemire.me/blog/2018/04/17/iterating-in-batches-over-data-structures-can-be-much-faster/
385 |
--------------------------------------------------------------------------------
/performance-ptbr.md:
--------------------------------------------------------------------------------
1 | # Escrevendo e otimizando o código Go
2 |
3 | Este documento descreve boas práticas para escrever código Go de alto desempenho.
4 |
5 | Embora existam discussões focadas em tornar os serviços individuais mais rápidos (cache, etc), projetar sistemas distribuídos de alto desempenho vai além do escopo deste trabalho. Já existem bons textos sobre monitoramento e projeto de sistemas distribuídos. Eles englobam um conjunto totalmente diferente de decisões de pesquisa e design.
6 |
7 | Todo o conteúdo será licenciado sob o CC-BY-SA.
8 |
9 | Este livro está dividido em diferentes seções:
10 |
11 | 1. Dicas básicas para escrever software que não é lento
12 | * Material de nível introdutório (CS 101)
13 | 1. Dicas para escrever software rápido
14 | * Veja seções específicas sobre como obter o melhor do Go
15 | 1. Dicas avançadas para escrever software *realmente* rápido
16 | * Para quando o seu código otimizado não é rápido o suficiente
17 |
18 | Podemos resumir estas três seções como:
19 |
20 | 1. "Seja razoável"
21 | 1. "Seja intencional"
22 | 1. "Seja perigoso"
23 |
24 | ## Quando e onde otimizar
25 |
26 | Estou colocando isso em primeiro lugar porque é realmente o passo mais importante. Você deveria mesmo estar fazendo isso?
27 |
28 | Toda otimização tem um custo. Geralmente esse custo é expresso em termos de complexidade de código ou carga cognitiva - o código otimizado é raramente mais simples do que a versão sem otimizações.
29 |
30 | Mas há outro lado que chamarei de economia da otimização. Como programador, seu tempo é valioso. Há o custo da oportunidade de trabalhar em outras coisas no projeto, por exemplo, quais erros corrigir ou quais funcionalidades adicionar. Otimizar as coisas é divertido, mas nem sempre é a tarefa certa a escolher. O desempenho é uma característica (*feature*), mas entrega e corretude também são.
31 |
32 | Escolha a coisa mais importante para trabalhar. Às vezes não é em uma otimização da CPU, mas na experiência do usuário. Algo tão simples como adicionar uma barra de progresso, ou tornar uma página mais responsiva ao fazer cálculos no plano de fundo depois de renderizar a página.
33 |
34 | Às vezes isso será óbvio: um relatório de hora em hora que leva três horas para ficar pronto é, provavelmente, menos útil do que aquele que é concluído em menos de uma hora.
35 |
36 | Só porque algo é fácil de otimizar não significa que vale a pena ser otimizado. Ignorar os casos mais fáceis é uma estratégia de desenvolvimento válida.
37 |
38 | Pense nisso como uma otimização do *seu* tempo.
39 |
40 | Você pode escolher o que otimizar e quando otimizar. Você pode mover o controle deslizante entre "Software rápido" e "Implantação rápida".
41 |
42 | As pessoas ouvem e repetem "a otimização prematura é a raiz de todo mal", mas eles perdem o contexto completo da citação.
43 |
44 | "Os programadores gastam muito tempo pensando ou se preocupando com a velocidade de partes não-críticas de seus programas. Estas tentativas de conseguir eficiência tem um forte impacto negativo quando a depuração e manutenção são consideradas. Devemos esquecer pequenas eficiências, digamos cerca de 97% do tempo: a otimização prematura é a raiz de todo o mal. Porém, não devemos deixar passar nossas oportunidades nesses 3% críticos."
45 | -- Knuth (*tradução livre*)
46 |
47 | Adicione: https://www.youtube.com/watch?time_continue=429&v=RT46MpK39rQ
48 | * não ignore as otimizações fáceis
49 | * mais conhecimento de algoritmos e estruturas de dados torna mais otimizações "fáceis" ou "óbvias"
50 |
51 | "Você deve otimizar? Sim, mas somente se o problema for importante, o programa é realmente muito lento e há alguma expectativa de que pode ser feito mais rápido, mantendo a corretude, robustez e clareza".
52 | -- A prática da programação, Kernighan e Pike (*tradução livre*)
53 |
54 | [BitFunnel performance estimation] (http://bitfunnel.org/strangeloop) tem alguns números que tornam esta decisão mais explícita. Imagine uma máquina de busca hipotética que precisa de 30.000 máquinas em vários *datacenters*. Essas máquinas tem um custo de aproximadamente US$ 1.000 por ano. Se você pode dobrar a velocidade do software, isso pode economizar US$ 15 milhões por ano. Até mesmo um único desenvolvedor gastando um ano inteiro para melhorar o desempenho em apenas 1% irá se pagar.
55 |
56 | Na grande maioria dos casos, o tamanho e a velocidade de um programa não são uma preocupação. A otimização mais fácil é não ter que fazê-la. A segunda otimização mais fácil está apenas comprando hardware mais rápido.
57 |
58 | Uma vez decidido que você irá mudar seu programa, continue lendo.
59 |
60 | ## Como otimizar
61 |
62 | ### Fluxo de otimização
63 |
64 |
65 | Antes de entrarmos nos detalhes, vamos falar sobre o processo geral de otimização.
66 |
67 | Otimização é uma forma de refatoração. Entretanto, em vez de melhorar algum aspecto do código-fonte (duplicação de código, clareza, etc), ela melhora algum aspecto de desempenho como, por exemplo, reduzir o uso da CPU, reduzir a ocupação da memória, reduzir a latência, etc. Essas melhorias, geralmente, são implementadas a troco de alguma perda na legibilidade do código. Isso significa que além de um conjunto de testes unitários (para garantir que suas mudanças não irão quebrar nada), você também precisará de um bom conjunto de _benchmarks_ para garantir que suas mudanças estão, de fato, entregando o ganho de desempenho desejado. Você deve ser capaz de verificar se a alteração realmente está reduzindo o uso da CPU. Às vezes uma alteração que você pensava que iria melhorar o desempenho, na verdade não gera nenhum impacto ou, até, causa um impacto negativo. Nesses casos, sempre desfaça suas alterações.
68 |
69 | [Qual é o melhor comentário que você já encontrou em um código? - Stack Overflow](https://stackoverflow.com/questions/184618/what-is-the-best-comment-in-source-code-you-have-ever-encountered):
70 |
71 | //
72 | // Caro mantenedor:
73 | //
74 | // Assim que desistir de tentar "otimizar" essa rotina,
75 | // e perceber que terrível engano você cometeu,
76 | // por favor, incremente o contador a seguir como uma forma de aviso
77 | // à próxima pessoa:
78 | //
79 | // total_hours_wasted_here = 42
80 | //
81 |
82 |
83 | Os _benchmarks_ que você decidir usar devem ser precisos e devem oferecer números reproduzíveis em cargas relevantes. Se execuções individuais tiverem uma variância muito alta, isso tornará mais difícil a detecção de pequenas melhorias. Assim, você precisará usar o [benchstat](https://golang.org/x/perf/benchstat) ou uma solução equivalente para realizar testes estatísticos já que não conseguirá verificar as melhorias apenas via observação. (Note que a utilização de testes estatísticos é uma boa ideia em qualquer cenário). Os passos para executar os _benchmarks_ devem estar documentados e quaisquer scripts e/ou ferramentas adicionais devem ser incluídas no repositório com instruções de como utilizá-los. Esteja atento a grandes conjuntos de _benchmark_ que requerem muito tempo para sua execução: isso irá tornar o processo de desenvolvimento mais lento.
84 |
85 | Lembre-se, também, que tudo que pode ser medido pode ser otimizado. Tenha certeza de que está medindo a coisa certa.
86 |
87 | O próximo passo é decidir qual é o seu objetivo com a otimização. Se o objetivo é melhorar o uso da CPU, qual velocidade é aceitável? Você quer melhorar o desempenho em 2x ou em 10x? Você pode definir isso como "um problema grande como N que precisa ser resolvido num tempo menor que T"? Você está tentando reduzir o uso de memória? Em quanto? Para uma determinada redução de uso de memória, quão mais lento é aceitável? Do que você está disposto a abrir mão em troca de menos exigência de espaço?
88 |
89 | Otimização com foco em latência de serviços é uma proposta mais complicada. Livros inteiros foram escritos sobre como testar o desempenho de servidores web. A principal questão é que, para uma única função, o desempenho é bastante consistente para um problema de determinado tamanho. Para _web services_, você não tem um único número. Um bom conjunto de _benchmark_ para _web services_ fornecerá uma distribuição de latência para um dado nível de requisições por segundo. Esta palestra dá uma boa visão geral de alguns dos problemas:["How NOT to Measure Latency" by Gil Tene](https://youtu.be/lJ8ydIuPFeU)
90 |
91 | TODO: Veja a seção a seguir sobre otimização de _web services_.
92 |
93 | As metas de desempenho devem ser específicas. (Quase) sempre você será capaz de fazer algo ser mais rápido. Otimização é, frequentemente, um jogo de retornos decrescentes. Você precisa saber a hora de parar. Quanto esforço a mais você vai fazer para obter aquela pequena melhora? Quão disposto você está a fazer um código mais feio e mais difícil de manter?
94 |
95 | A palestra de Dan Luu mencionada anteriormente em [BitFunnel performance estimation](http://bitfunnel.org/strangeloop) apresenta um exemplo do uso de cálculo aproximados para determinar se as metas de desempenho estimadas são razoáveis.
96 |
97 | TODO: Programming Pearls tem "Problemas de Fermi". Conhecer os slides de Jeff Dean ajuda.
98 |
99 | Para o desenvolvimento de novos projetos, você não deve deixar o a avaliação de desempenho para o fim. É fácil dizer "depois eu faço", mas se o desempenho é realmente importante, isso deve ser considerado desde a concepção do projeto. Quaisquer alterações relevantes na arquitetura para consertar problemas de desempenho serão ainda mais arriscadas quando tiverem que ser feitas próximas do prazo final. Perceba que, *durante* o desenvolvimento, o foco deve ser um desenho coerente, algoritmos e estrutura de dados. Otimização em níveis mais baixos da estrutura devem aguardar uma fase mais avançada do ciclo de desenvolvimento, quando houver uma visão mais completa do desempenho do sistema. Qualquer perfil completo de sistema que você faz enquanto o sistema está incompleto oferecerá uma visão distorcida de onde os gargalos estarão, de fato, no sistema acabado.
100 |
101 | TODO: Como evitar/detectar "Morte por mil cortes (Lingchi)" por _software_ mal escrito.
102 |
103 | O _benchmarking_ como parte do CI é difícil devido a interferências causadas pelo compartilhamento de recursos. Difícil, também, de ser ativado em métricas de desempenho. Um bom meio termo é ter _benchmarks_ executados pelo desenvolvedor (em hardware apropriado) e incluídos nos _commits_ que abordam, especificamente, o desempenho. Para aqueles que são apenas patches gerais, tente identificar as potenciais degradações de desempenho "a olho nu", na revisão de código.
104 |
105 | TODO: Como acompanhar o desemepnho ao longo do tempo?
106 |
107 | Escreva código que você pode comparar. Você pode fazer perfilamento em sistemas maiores, porém em _benchmarking_ você quer testar partes isoladas. Você precisa ser capaz de extrair e configurar o contexto necessário para que os _benchmarks_ executem testes representativos e suficientes.
108 |
109 | A lacuna entre sua meta e o desempenho atual também te darão uma orientação de por onde começar. Se você precisa de apenas 10% a 20% de melhoria de desempenho, provavelmente você consegue alcançar isso com pequenos ajustes. Se você precisa de uma melhoria da ordem de 10x ou mais, isso vai exigir mudanças maiores em sua estrutura.
110 |
111 | Um bom trabalho em aprimoramento de desempenho exige conhecimentos dos mais variados níveis, desde desenho de sistemas, rede, _hardware_ (CPU, caches, armazenamento), algoritmos, ajustes e _debugging_. Com tempo e recursos limitados, considere aquele que lhe dará o maior ganho: nem sempre será o algoritmo ou um ajuste fino no programa.
112 |
113 | Em geral, as otimizações devem ocorrer de cima para baixo. Otimizações em nível de sistema terão mais impacto que aquelas em nível de código. Certifique-se de que você está resolvendo o problema no nível apropriado.
114 |
115 | Esse livro irá tratar, em sua maior parte, sobre redução de uso da CPU, redução de uso da memória e redução de latência. É interessante destacar que, raramente, você fará os três ao mesmo tempo. Talvez o tempo de CPU esteja mais rápido, mas agora seu programa usa mais memória. Talvez você precise reduzir o espaço de memória, mas agora o programa levará mais tempo.
116 |
117 | [Lei de Amdahl](https://en.wikipedia.org/wiki/Amdahl%27s_law) diz para nos concentrarmos nos gargalos. Se você dobra a velocidade da rotina que toma 5% do tempo de execução, houve um ganho de apenas 2,5% no tempo total. Por outro lado, aumentar a velocidade da rotina que toma 80% do tempo em apenas 10% oferece um ganho real de 8%. Perfilamento irá ajudar a identificar onde o tempo é realmente gasto.
118 |
119 | Quando se está otimizando, você quer reduzir o trabalho que a CPU precisa fazer. _Quicksort_ é mais rápido que _bubble sort_ porque resolve o mesmo problema em menos passos. É um algoritmo mais eficiente. Você reduziu o trabalho que a CPU tem para executar a mesma tarefa.
120 |
121 | O ajuste do programa, como as otimizações do compilador, geralmente trazem uma pequena melhora no tempo total de execução. Grandes vitórias quase sempre vêm de uma mudança algorítmica ou uma mudança na estrutura de dados ou uma mudança fundamental na forma como o seu
122 | programa é organizado. A tecnologia de compiladores melhora, mas lentamente. A [Lei de Proebsting](http://proebsting.cs.arizona.edu/law.html) diz que compiladores melhoram seu desempenho em 2x a cada 18 *anos*, um contraste gritante com a Lei de Moore que diz que o desempenho dos processadores dobra a cada 18 *meses*. Melhorias em algoritmos funcionam em magnitudes maiores. Algoritmos para programação inteira mista [melhoraram por um fator de 30.000 entre 1991 e 2008](https://agtb.wordpress.com/2010/12/23/progress-in-algorithms-beats-moore%E2%80%99s-law/). Para um exemplo mais concreto, considere [essa decisão](https://medium.com/@buckhx/unwinding-uber-s-most-efficient-service-406413c5871d) de substituir de um algoritmo geo-espacial de força bruta descrito em um post do blog do Uber por um mais especializado e mais adequado para a tarefa apresentada. Não há mudança de compilador que lhe dará um aumento equivalente no desempenho.
123 |
124 | Um perfilador pode mostrar que muito tempo é gasto em uma rotina específica. Pode ser que esta seja uma rotina cara ou uma rotina barata
125 | porém que é chamada muitas vezes. Em vez de imediatamente tentar aumentar a velocidade de uma rotina, veja se você pode reduzir o número de vezes que ela é chamada ou, até mesmo, eliminá-la completamente. Vamos discutir estratégias de otimização mais concretas na próxima seção.
126 |
127 | As três perguntas da otimização:
128 |
129 | * Nós precisamos fazer isso mesmo? O código mais rápido é aquele nunca executado.
130 | * Se sim, esse é o melhor algoritmo?
131 | * Se sim, essa é a melhor *implementação* desse algoritmo?
132 |
133 | ## Dicas concretas sobre otimização
134 |
135 | O trabalho de Jon Bentley em 1982, "Writing Efficient Programs", abordou a otimização de programas como um problema de engenharia: Benchmark. Analisar. Melhorar. Verificar. Iterar. Várias de suas dicas agora são feitas automaticamente por compiladores. Um dos trabalhos dos programadores é usar as "Transformações" que um compilador *Não pode* realizar.
136 |
137 |
138 | Há resumos deste livro:
139 |
140 | *
141 | *
142 |
143 | e as regras de ajuste de programas:
144 |
145 | *
146 |
147 | Ao pensar em mudanças que você pode fazer no seu programa, existem duas opções básicas:
148 | você pode alterar seus dados ou alterar seu código.
149 |
150 | ### Alterações nos dados
151 |
152 | Alterar seus dados significa adicionar ou alterar a representação dos dados que você está processando. Do ponto de vista de desempenho, alguns desses vão acabar mudando a complexidade O() que é associada a diferentes aspectos das estruturas de dados.
153 |
154 | Idéias para melhorar sua estrutura de dados:
155 |
156 | * Campos extras
157 |
158 | O exemplo clássico disso é armazenar o tamanho de uma lista encadeada em um campo
159 | do nó raiz. Temos um pouco mais de trabalho para mantê-la atualizada, porém, em seguida, consultar
160 | o comprimento se torna uma pesquisa de campo simples em vez de um percurso O (n). Sua estrutura de dados
161 | pode apresentar um ganho semelhante: um pouco de *bookkeeping* durante algumas
162 | operações em troca de um desempenho melhor em um caso de uso comum.
163 |
164 | Da mesma forma, armazenar ponteiros para nós frequentemente necessários em vez de executar
165 | pesquisas adicionais. Isso abrange coisas como os links "para trás" em uma
166 | lista duplamente ligada para fazer a remoção do nó ter complexidade O (1). Algumas Skip lists mantêm uma "search
167 | finger ", onde você armazena um ponteiro de onde você estava em sua estrutura no pressuposto de que é um bom ponto de partida para a sua próxima operação.
168 |
169 | * Índices extras de pesquisa
170 |
171 | A maioria das estruturas de dados é projetada para um único tipo de consulta. Se você precisar de dois
172 | tipos de consulta diferentes, ter uma "visualização" adicional nos seus dados pode ser uma grande
173 | melhoria. Por exemplo, um conjunto de estruturas pode ter um ID primário (inteiro)
174 | que você usa para procurar em uma fatia, mas às vezes precisa procurar com um
175 | ID secundário (string). Em vez de iterar sobre a fatia, você pode incrementar
176 | sua estrutura de dados com um mapa de string para ID ou diretamente para própria estrutura.
177 |
178 | * Informação extra sobre elementos
179 |
180 | Por exemplo, manter um bloom filter de todos os elementos inseridos pode fazer com que você retorne rapidamente consultas sem resultado. Estes precisam ser pequenos e rápidos para não sobrecarregar o resto da estrutura de dados. (Se uma pesquisa em seus dados principais é barata, o custo do *bloom filter* superará qualquer economia.)
181 |
182 | * Se as consultas forem caras, adicione um cache.
183 |
184 | Em um nível maior, um cache interno ou externo (como o memcache) pode ajudar. Isso pode soar excessivo para somente uma estrutura de dados. Falaremos mais sobre cache abaixo.
185 |
186 | Esses tipos de alterações são utéis quando os dados necessários são baratos para armanezar e fáceis de se manterem atualizados.
187 |
188 | Estes são exemplos claros de "Tenha menos trabalho" pensando no nível de estrutura de dados. Todos eles custam espaço. Na maior parte do tempo se você está otimizando pensando em uso de CPU, seu programa usará mais memória esta é a clássica [compensação espaço-temporal](https://en.wikipedia.org/wiki/Space%E2%80%93time_tradeoff).
189 |
190 | É importante pensar como essa compensação pode afetar as suas soluções -- de maneira indireta. Às vezes, uma pequena quantidade de memória pode resultar em uma melhoria significativa de velocidade, em outras situações este tradeoff é linear (2x o uso da memória == 2x a melhora de desempenho), em outras vezes é significativamente pior: uma enorme quantidade de memória fornece apenas uma pequena melhora de desempenho. Onde você precisa estar nesta curva de memória/desempenho podem afetar quais opções de algoritmos são razoáveis. Nem sempre é possível somente ajustar um parâmetro de um algoritmo. Diferentes usos de memória pode ter abordagens algorítimicas completamente diferentes.
191 |
192 |
193 | Tabelas de pesquisa também se enquadram nessa compensação espaço-temporal. Uma tabela de pesquisa simples
194 | pode ser apenas um cache de cálculos que foram solicitados anteriormente.
195 |
196 |
197 | Se o domínio for pequeno o suficiente, o conjunto * inteiro de resultados poderá ser
198 | pré-computado e armazenado na tabela.
199 |
200 | Como exemplo, essa poderia ser a abordagem adotada para uma implementação rápida de popcount, em que pelo número de bits ativos em um byte são armazenados em uma tabela de 256 entradas. Uma tabela maior pode armazenar os bits necessários para todas as palavras de 16 bits. Nesse caso, eles estão armazenando resultados exatos.
201 |
202 |
203 | Vários algoritmos para funções trigonométricas usam tabelas de pesquisa como um
204 | ponto de partida para realizar um cálculo.
205 |
206 |
207 | Se o seu programa usa muita memória, também é possível seguir outro caminho.
208 | Reduza o uso de espaço em troca do aumento da computação. Em vez de armazenar
209 | coisas, calcule-as sempre. Você também pode compactar os dados na memória
210 | e descompactar rapidamente quando precisar.
211 |
212 | Se os dados que você está processando estiverem em disco, em vez de carregar tudo na memória
213 | RAM, você pode criar um índice para as peças necessárias e mantê-las
214 | memória ou pré-processe o arquivo em pequenos pedaços viáveis.
215 |
216 | [Small Memory Software](http://smallmemory.com/book.html) é um livro disponível online que cobre técnicas utilizadas para o reduzir o espaço usado por seus programas.
217 | Embora tenha sido originalmente escrito para desenvolvedores de software embarcado, as idéias são
218 | aplicáveis a programas que rodam em hardware moderno que lidam com grandes quantidades de dados.
219 |
220 | * Reorganize seus dados
221 |
222 | Elimine o preenchimento da estrutura. Remova campos extras. Use um tipo de dados menor.
223 |
224 | * Mude para uma estrutura de dados mais lenta
225 |
226 | Estruturas de dados mais simples freqüentemente têm requisitos de memória mais baixos. Por exemplo, mudar de uma estrutura de árvore pesada com ponteiro para usar busca linear e slice em arrays.
227 |
228 | * Formato de compactação personalizado para seus dados
229 |
230 | Algoritmos de compressão dependem muito do que está sendo compactado. É
231 | melhor escolher um que combine com seus dados. Se você tiver [] byte, algo
232 | como snappy, gzip, lz4, se comporta bem. Para dados de ponto flutuante, existe go-tsz
233 | para séries temporais e fpc para dados científicos. Muita pesquisa foi feita
234 | compactar números inteiros, geralmente para recuperação de informações em motores de pesquisa. Exemplos incluem codificação delta e varints para esquemas mais complexos envolvendo Huffman códificado com diferenças de OU-exclusivo. Você também pode criar formatos de compactação otimizados para seus tipos exatos de dados.
235 |
236 | Você precisa inspecionar os dados ou eles podem permanecer compactados? Você precisa de acesso aleatório ou apenas streaming? Se você precisar acessar entradas individuais, mas não quer descomprimir a coisa toda, você pode compactar os dados em blocos menores e manter um índice indicando o intervalo de entradas em cada bloco. O acesso a uma única entrada só precisa verificar o índice e descompactar o bloco de dados menor.
237 |
238 | Se seus dados não estão apenas sendo processados, mas também serão gravados em disco, que tal migração de dados ou adição / remoção de campos. Agora você estará lidando com os [] byte em sua forma crua, em vez de bons tipos estruturados de Go, portanto, então você irá precisar do pacote unsafe e considerar as suas opções de serialização.
239 |
240 | Falaremos mais sobre layouts de dados posteriormente.
241 |
242 | Computadores modernos e a hierarquia de memória
243 | fazem o trade-off espaço / tempo menos claro. É fácil que as tabelas de pesquisa estejam "distantes" na memória (e
244 | portanto, tornam seu acesso custoso), tornando mais rápido apenas recalcular um valor toda vez que for necessário.
245 |
246 |
247 | Isso também significa que o benchmarking frequentemente mostrará melhorias que não são percebidos no sistema de produção devido à contenção de cache
248 | (por exemplo, tabelas de pesquisa estão no cache do processador durante o benchmarking, mas sempre são liberadas por "dados reais" quando usados em um sistema real). O artigo do Google sobre [Jump Hash paper](https://arxiv.org/pdf/1406.2294.pdf)
249 | abordou isso diretamente, comparando o desempenho em um cache de processador com e sem contenção. (Veja os gráficos 4 e 5 no artigo Jump Hash)
250 |
251 |
252 | TODO: como simular um cache contencioso, mostrar custos incrementais
253 | TODO: sync.Map como um exemplo go-ish de endereçamento de contenção de cache
254 |
255 | Outro aspecto a considerar é o tempo de transferência de dados. Geralmente, o acesso à rede e ao disco é muito lento e, portanto,poder carregar um conjunto de dados compactos será muito mais rápido que o tempo extra da CPU necessário para descomprimir estes dados quando carregados. Como sempre, benchmark. Um formato binário geralmente será menor e mais rápido de analisar do que um texto, mas com o custo de não ser mais legível por humanos.
256 |
257 |
258 | Para transferência de dados, vá para um protocolo menos falador ou aumente a API para permitir consultas parciais. Por exemplo, uma consulta incremental em vez de ser
259 | forçado a buscar o conjunto de dados inteiro a cada vez.
260 |
261 | ### Alterações algorítmicas
262 |
263 | Se você não estiver alterando os dados, a outra opção principal é alterar o código.
264 |
265 | A maior melhoria provavelmente virá de uma alteração algorítmica. Isso equivale a substituir um bubble sort (`O(n^2)`) por um quicksort (`O(n log n)`) ou substituir a varredura linear de uma matriz (`O(n)`) por uma pesquisa binária (`O (log n)`) ou pesquisa em um mapa (`O(1)`).
266 |
267 | É assim que o software se torna lento. As estruturas projetadas originalmente para um uso são reaproveitadas para algo que não foram projetadas. Isso acontece gradualmente.
268 |
269 |
270 | É importante ter uma compreensão intuitiva dos diferentes níveis Grande-O.
271 | Escolha a estrutura de dados certa para o seu problema. Você não precisa cortar ciclos sempre, mas isso evita problemas de desempenho bobos que podem não ser percebidos até muito mais tarde.
272 |
273 | As classes básicas de complexidade são:
274 |
275 | * O (1): um acesso ao campo, matriz ou pesquisa de mapa
276 |
277 | Conselho: não se preocupe (mas lembre-se do fator constante).
278 |
279 | * O (log n): pesquisa binária
280 |
281 | Conselho: apenas um problema se estiver em loop
282 |
283 | * O (n): loop simples
284 |
285 | Conselho: você está fazendo isso o tempo todo
286 |
287 | * O (n log n): dividir e conquistar, classificação
288 |
289 | Conselho: ainda bastante rápido
290 |
291 | * O(n\*m): loop aninhado / quadrático
292 |
293 | Conselho: tenha cuidado e restrinja os tamanhos.
294 |
295 | * Qualquer outra coisa entre quadrático e subexponencial
296 |
297 | Conselho: não execute isso em um milhão de linhas
298 |
299 | * O(b ^ n), O(n!): Exponencial e acima
300 |
301 | Conselho: boa sorte se você tiver mais de uma dúzia ou dois pontos de dados
302 |
303 | Link:
304 |
305 | Digamos que você precise pesquisar um conjunto de dados não classificado. "Eu devo usar uma pesquisa binária",
306 | você pensa, sabendo que uma pesquisa binária é O (log n) mais rápida que a varredura linear O (n).
307 | No entanto, uma pesquisa binária exige que os dados sejam classificados, o que significa que você precisará classificá-los primeiro, o que levará tempo O (n log n).
308 | Se você estiver fazendo muitas pesquisas, o custo inicial da classificação será recompensado.
309 | Por outro lado, se você estiver pesquisando principalmente em mapas, talvez ter uma matriz seja a escolha errada e seria melhor pagar o custo de pesquisa O (1) para um mapa.
310 |
311 | Ser capaz de analisar seu problema em termos de notação Grande-O também significa que você pode descobrir se já está no limite do que é possível para o seu problema e se precisa mudar de abordagem para acelerar as coisas. Por exemplo, encontrar o mínimo de uma lista não classificada é `O(n)`, porque você precisa examinar cada item. Não há como tornar isso mais rápido.
312 |
313 |
314 | Se sua estrutura de dados é estática, geralmente você pode fazer muito melhor do que o caso dinâmico.
315 | Torna-se mais fácil criar uma estrutura de dados ideal personalizada para exatamente seus padrões de pesquisa.
316 | Soluções como o hash perfeito mínimo podem fazer sentido aqui, ou filtros de bloom pré-computados.
317 | Isso também faz sentido se sua estrutura de dados for "estática" por tempo suficiente e você puder amortizar o custo inicial de construção em muitas pesquisas.
318 |
319 | Escolha a estrutura de dados razoável mais simples e siga em frente. Este é o CS 101 para escrever "software não lento".
320 | Esse deve ser o seu modo de desenvolvimento padrão. Se você sabe que precisa de acesso aleatório, não escolha uma lista vinculada.
321 | Se você sabe que precisa de uma travessia em ordem, não use um mapa.
322 | Os requisitos mudam e você nem sempre pode adivinhar o futuro. Faça um palpite razoável sobre a carga de trabalho.
323 |
324 |
325 |
326 | As estruturas de dados para problemas semelhantes diferem quando executam um trabalho. Uma árvore binária é classificada um pouco
327 | de cada vez à medida que as inserções acontecem. Uma matriz não ordenada é mais rápida de inserir, mas não é ordenada: ao final, para "finalizar", você precisa fazer a ordenação de uma só vez.
328 |
329 | Ao escrever um pacote para ser usado por outras pessoas, evite a tentação de otimizar antecipadamente todos os casos de uso. Isso resultará em código ilegível. Por projeto, as estruturas de dados são efetivamente de propósito único. Você não pode ler mentes nem prever o futuro. Se um usuário disser "Seu pacote está muito lento para este caso de uso", uma resposta razoável pode ser "Então use este outro pacote aqui". Um pacote deve "fazer uma coisa bem".
330 |
331 | Às vezes, estruturas de dados híbridas fornecem a melhoria de desempenho que você precisa. Por exemplo, ao reunir seus dados, você pode limitar sua pesquisa a um único intervalo. Isso ainda paga o custo teórico de O(n), mas a constante será menor. Revisitaremos esses tipos de ajustes quando chegarmos em otimizações.
332 |
333 | Duas coisas que as pessoas esquecem quando discutem a notação Grande-O:
334 |
335 | Primeiramente, há um fator constante envolvido. Dois algoritmos que têm a mesma complexidade algorítmica podem ter diferentes fatores constantes. Imagine repetir uma lista 100 vezes ou apenas repetir uma vez. Embora ambos sejam O (n), um tem um fator constante 100 vezes maior.
336 |
337 | Esses fatores constantes são o motivo pelo qual, embora merge sort, quick sort e heap sort sejam O (nlogn), todo mundo usa quick sort porque é a mais rápida. Este método de ordenação tem o menor fator constante.
338 |
339 | A segunda coisa é que Grande-O diz apenas "à medida que n cresce até o infinito". Ele fala sobre a tendência de crescimento: "À medida que os números aumentam, esse é o fator de crescimento que dominará o tempo de execução". Não diz nada sobre o desempenho real, ou como ele se comporta com um valor de n pequeno.
340 |
341 | Freqüentemente há um ponto de corte abaixo do qual um algoritmo ingênuo é mais rápido. Um bom exemplo do pacote sort da biblioteca padrão Go. Na maioria das vezes, ele usa o quicksort, mas ele passa por uma classificação por shell e depois por inserção quando o tamanho da partição cai abaixo de 12 elementos.
342 |
343 | Para alguns algoritmos, o fator constante pode ser tão grande que esse ponto de corte pode ser maior que todas as entradas razoáveis. Ou seja, o algoritmo O(n^2) é mais rápido que o algoritmo O(n) para todas as entradas com as quais você provavelmente lida.
344 |
345 |
346 | Isso também significa que você precisa conhecer os tamanhos de entrada representativos, tanto para escolher o algoritmo mais apropriado quanto para escrever boas avaliações. 10 itens? 1000 itens? 1000000 itens?
347 |
348 | Isso também acontece de outra maneira: por exemplo, optar por usar uma estrutura de dados mais complicada para fornecer o escalonamento de O (n) em vez de O (n^2),
349 | mesmo que com os parâmetros de referência para pequenas entradas tenham ficado mais lentos. Isso também se aplica à maioria das estruturas de dados sem contenção.
350 | Elas geralmente são mais lentas no caso de uma única thread, mas são mais escaláveis quando estão sendo usadas por muitas threads.
351 |
352 | A hierarquia de memória nos computadores modernos confunde um pouco o problema aqui, isto pois os caches preferem o acesso previsível de uma varredura ao acesso aleatório que temos com a perseguição de um ponteiro. Ainda assim, é melhor começar com um bom algoritmo. Falaremos sobre isso na seção específica de hardware.
353 |
354 | TODO: estendendo o último parágrafo, mencione a notação O () é um modelo em que cada
355 | operação tem custo fixo. Essa é uma suposição errada no hardware moderno.
356 |
357 | > A luta nem sempre é vencida pelo mais forte, nem a corrida pelo mais rápido,
358 | mas essa é a maneira de apostar.
359 | > -- Rudyard Kipling
360 |
361 | Às vezes, o melhor algoritmo para um problema específico não é um único algoritmo, mas uma coleção de algoritmos especializados para classes de entrada ligeiramente diferentes. Esse "polialgoritmo" detecta rapidamente com que tipo de entrada ele precisa lidar e despacha para o caminho de código apropriado. É isso que faz o pacote de classificação mencionado acima: determina o tamanho do problema e escolhe um algoritmo diferente. Além de combinar quicksort, classificação de shell,
362 | e classificação de inserção, também rastreia a profundidade de recursão do quicksort e chama heapsort, se necessário. Os pacotes `string` e` bytes` fazem algo semelhante, detectando e se especializando para casos diferentes. Assim como na compactação de dados, quanto mais você souber sobre a aparência de sua entrada, melhor poderá ser sua solução personalizada. Mesmo que uma otimização nem sempre seja aplicável, vale a pena complicar seu código, determinando que é seguro usar e executar lógicas diferentes.
363 |
364 | Isso também se aplica aos subproblemas que seu algoritmo precisa resolver.
365 | Por exemplo, ter o usar radix sort a disposição pode ter um impacto significativo no desempenho ou
366 | usar a seleção rápida se você precisar apenas de uma classificação parcial.
367 |
368 | Às vezes, em vez de especialização para sua tarefa específica, a melhor abordagem é abstraí-la para um espaço de
369 | problemas mais geral que foi bem estudado pelos pesquisadores.
370 | Em seguida, você pode aplicar a solução mais geral ao seu problema específico.
371 | Mapear seu problema em um domínio que já possui implementações bem pesquisadas pode ser uma vitória significativa.
372 |
373 | Da mesma forma, usar um algoritmo mais simples significa que os detalhes das trocas, análises e implementação
374 | têm mais probabilidade de serem mais estudados e bem compreendidos do que os mais esotéricos ou exóticos e complexos.
375 |
376 |
377 | Algoritmos mais simples também podem ser mais rápidos. Esses dois exemplos não são casos isolados
378 | https://go-review.googlesource.com/c/crypto/+/169037
379 | https://go-review.googlesource.com/c/go/+/170322/
380 |
381 | TODO: notas sobre seleção de algoritmo
382 |
383 | TODO:
384 | melhore o comportamento do pior caso a um custo leve para o tempo de execução médio
385 | da correspondência de expressão regular de tempo linear
386 |
387 |
388 | Embora a maioria dos algoritmos seja determinística, há uma classe de algoritmos que usam a aleatoriedade como uma maneira de simplificar etapas de tomada de decisão complexas.
389 | Em vez de ter um código que faz a coisa certa, você usa a aleatoriedade para selecionar uma coisa provavelmente não *ruim*. Por exemplo, um treap é uma árvore binária probabilisticamente equilibrada. Cada nó tem uma chave, mas também recebe um valor aleatório.
390 | Ao inserir na árvore, o caminho de inserção normal da árvore binária é seguido, mas os nós também obedecem à propriedade heap
391 | com base no peso atribuído aleatoriamente a cada nó. Essa abordagem mais simples substitui soluções de
392 | rotação de árvores complicadas (como árvores AVL e Rubro negras), mas ainda mantém uma árvore equilibrada com inserção/pesquisa de O (log n) "com alta
393 | probabilidade". As Skip lists são outra estrutura de dados simples e semelhante que usa aleatoriedade para produzir "provavelmente" inserção e pesquisas de O (log n).
394 |
395 | Da mesma forma, a escolha de um pivô aleatório para quicksort pode ser mais simples do que uma abordagem de média mediana mais complexa para encontrar um bom pivô,
396 | e a probabilidade de maus pivôs serem continuamente escolhidos (aleatoriamente) e degradar o desempenho do quicksort para O(n^2) é muito pequena.
397 |
398 | Os algoritmos aleatórios são classificados como algoritmos "Monte Carlo" ou "Las Vegas", a partir de dois locais de jogo bem conhecidos.
399 | Um algoritmo de Monte Carlo joga com exatidão: pode gerar uma resposta errada (ou, no caso acima, uma árvore binária desequilibrada). Um algoritmo de Las Vegas sempre
400 | gera uma resposta correta, mas pode levar muito tempo para terminar.
401 |
402 | Outro exemplo bem conhecido de um algoritmo aleatório é o algoritmo de teste de primalidade de Miller-Rabin. Cada iteração produzirá "não primo" ou "talvez primo". Enquanto "não primo" é certo, o "talvez primo" está correto com probabilidade de pelo menos 1/2. Ou seja, existem não primos para os quais "talvez primo" ainda será produzido. Ao executar muitas iterações de Miller-Rabin, podemos tornar a probabilidade de falha (ou seja, gerar "talvez primo" para um número composto) tão pequena quanto gostaríamos. Se passar 200 iterações, podemos dizer que o número é composto com probabilidade no máximo 1/(2^200).
403 |
404 |
405 | Outra área em que a aleatoriedade desempenha um papel é chamada "O poder de duas escolhas aleatórias". Embora inicialmente a pesquisa tenha sido aplicada ao balanceamento de carga, ela se mostrou amplamente aplicável a vários problemas de seleção. A idéia é que, em vez de tentar encontrar a melhor seleção dentre um grupo de itens, escolha dois aleatoriamente e selecione o melhor. Voltando ao balanceamento de carga (ou cadeias de tabelas de hash), o poder de duas opções aleatórias reduz a carga esperada (ou o comprimento da cadeia de hash) dos itens O(log n) para O(log log n)
406 | Itens. Para obter mais informações, consulte [The Power of Two Random Choices: A Survey of Techniques and Results (https://www.eecs.harvard.edu/~michaelm/postscripts/handbook2001.pdf)
407 |
408 | algoritmos aleatórios:
409 | outros algoritmos de armazenamento em cache
410 | aproximações estatísticas (frequentemente dependem do tamanho da amostra e não do tamanho da população)
411 |
412 | TODO: lote para reduzir a sobrecarga: https://lemire.me/blog/2018/04/17/iterating-in-batches-over-data-structures-can-be-much-faster/
413 |
414 | TODO: - Algorithm Design Manual: http://algorist.com/algorist.html
415 | - Como resolvê-lo por computador
416 | - até que ponto é este livro "como escrever algoritmos"? Se você vai mudar
417 | o código para acelerar, por definição, você está escrevendo novos algoritmos. Então ... talvez?
418 |
419 | ### Entradas para avaliação de desempenho
420 |
421 | As contribuições do mundo real raramente correspondem ao "pior caso" teórico.
422 | O benchmarking é vital para entender como o sistema se comporta na produção.
423 |
424 | Você precisa saber qual classe de entradas seu sistema verá depois de implantado e seus benchmarks devem usar instâncias extraídas dessa mesma distribuição.
425 | Como vimos, algoritmos diferentes fazem sentido em diferentes tamanhos de entrada.
426 | Se o seu intervalo de entrada esperado for <100, seus benchmarks devem refletir isso. Caso contrário, escolher um algoritmo ideal para n = 10^6 pode não ser o mais rápido.
427 |
428 | Ser capaz de gerar dados de teste representativos. Diferentes distribuições de dados podem provocar comportamentos diferentes em seu algoritmo:
429 | pense no exemplo clássico de "quicksort é O (n^2) quando os dados são classificados". Da mesma forma, a pesquisa de interpolação é O (log log n) para dados aleatórios
430 | uniformes, mas O (n) no pior caso. Saber como são as suas entradas é a chave para os benchmarks representativos e para escolher o melhor algoritmo.
431 | Se os dados que você está usando para testar não são representativos de cargas de trabalho reais, você pode facilmente otimizar um determinado conjunto de dados,
432 | "ajustando demais" seu código para funcionar melhor com um conjunto específico de entradas.
433 |
434 | Isso também significa que seus dados de referência precisam ser representativos do mundo real.
435 | O uso de entradas puramente aleatórias pode distorcer o comportamento do seu algoritmo.
436 | Os algoritmos de cache e compactação exploram distribuições distorcidas ausentes
437 | em dados aleatórios e, portanto, terá um desempenho pior, enquanto uma árvore binária executará
438 | melhor com valores aleatórios, pois eles tendem a manter a árvore equilibrada. (Isto é
439 | a ideia por trás de uma treap, a propósito.)
440 |
441 | Por outro lado, considere o caso de testar um sistema com um cache.
442 | Se sua entrada benchmark consiste apenas em uma única consulta, então cada solicitação atingirá o
443 | cache, fornecendo uma visão potencialmente muito irreal de como o sistema se comportará
444 | no mundo real com um padrão de solicitação mais variado.
445 |
446 | Além disso, observe que alguns problemas que não são aparentes no seu laptop podem estar visíveis
447 | depois de implantar na produção e atingir 250k reqs / segundo em um núcleo de 40
448 | servidor. Da mesma forma, o comportamento do coletor de lixo durante o benchmarking
449 | pode deturpar o impacto no mundo real. Existem casos (raros) em que um
450 | O microbenchmark mostrará uma desaceleração, mas o desempenho no mundo real melhora.
451 | Microbenchmarks podem ajudar a empurrá-lo na direção certa, mas ser capaz de
452 | testar completamente o impacto de uma mudança em todo o sistema é melhor.
453 |
454 | Escrever boas referências pode ser difícil.
455 |
456 | *
457 |
458 | Use média geométrica para comparar grupos de benchmarks.
459 |
460 | *
461 |
462 | Avaliando a precisão do benchmark:
463 |
464 | *
465 |
466 | ### Melhoria do programa.
467 |
468 | A melhoria do programa costumava ser uma forma de arte, mas os compiladores ficaram melhores. Portanto, agora os compiladores podem otimizar o código melhor do que complicar aquele código.
469 | O compilador Go ainda tem um longo caminho a percorrer para corresponder ao gcc e ao clang, mas isso significa que você precisa
470 | ter cuidado ao ajustar e principalmente ao atualizar as versões de Go para que seu código não se torne "pior".
471 | Definitivamente, há casos em que os ajustes para solucionar a falta de uma otimização específica do compilador
472 | façam o código se tornar mais lento depois que o compilador foi aprimorado.
473 |
474 |
475 | Minha implementação de cifra RC6 teve um aumento de velocidade de 10% para o loop interno apenas mudando para `encoding / binary` e` math / bits` em vez de usar uma de minhas versões feitas manualmente.
476 |
477 | Da mesma forma, o pacote `compress / bzip2` foi acelerado ao mudar para [código mais simples que o compilador conseguiu otimizar] (https://github.com/golang/go/commit/9eb219480e8de08d380ee052b7bff293856955f8)
478 |
479 | Se você estiver trabalhando em torno de um tempo de execução específico ou geração de código do compilador sempre documente sua alteração com um link para a edição anterior. Este link permitirá que você revisite rapidamente sua otimização depois que o bug for corrigido.
480 |
481 | Lute contra a tentação de "dicas de desempenho" baseadas no folclore cult, ou até
482 | generalizar demais a partir de sua própria experiência. Cada bug de desempenho precisa ser
483 | abordado por seus próprios méritos. Mesmo se algo funcionou anteriormente, certifique-se de criar um perfil para garantir que a correção ainda seja aplicável. Seu trabalho pode orientá-lo, mas não aplique otimizações anteriores às cegas.
484 |
485 | A melhoria do programa é um processo iterativo. Continue revisitando seu código e vendo
486 | que mudanças podem ser feitas. Verifique se você está progredindo em cada etapa.
487 | Freqüentemente, uma melhoria permitirá que outras sejam feitas. (Agora que eu não estou
488 | fazendo A, eu posso simplificar B fazendo C em vez disso). Isso significa que você precisa manter-se
489 | olhando para a foto inteira e não fique muito obcecado com um pequeno conjunto de
490 | linhas.
491 |
492 | Depois de escolher o algoritmo certo, a melhoria do programa é o processo de
493 | melhorar a implementação desse algoritmo. Na notação Grande-O, isso é
494 | o processo de redução das constantes associadas ao seu programa.
495 |
496 | Toda a melhoria no programa ou vai tornar mais rápido algo que era mais lento ou fazer algo que é lento uma menor quantidade de vezes.
497 | As mudanças algorítmicas também se enquadram nessas categorias, mas veremos mudanças menores. Exatamente como você faz isso varia conforme as tecnologias mudam.
498 |
499 | Como exemplo, fazer uma coisa lenta rapidamente pode ser substituir SHA1 ou `hash/fnv1` por uma função hash mais rápida. Fazer um processo lento menos vezes pode ser salvar o resultado do cálculo de hash de um arquivo grande para que você não precise fazer isso várias vezes.
500 |
501 | Mantenha comentários. Se algo não precisar ser feito, explique o porquê. Freqüentemente, ao otimizar um algoritmo, você descobrirá etapas que não precisam ser executadas sob algumas circunstâncias. Documente-as. Outra pessoa pode pensar que é um bug.
502 |
503 | > Programas vazios dão a resposta errada em pouco tempo.
504 | >
505 | > É fácil ser rápido se você não precisa estar correto.
506 |
507 | A "correção" pode depender do problema. Os algoritmos heurísticos mais corretos na maioria das vezes podem ser rápidos, assim como os algoritmos que adivinham e melhoram, permitindo que você pare quando atingir um limite aceitável.
508 |
509 |
510 | Casos comuns de cache:
511 |
512 | Todos conhecemos o memcache, mas também existem caches em processo. O uso de um cache em processo economiza o custo da chamada de rede e o custo da serialização. Por outro lado, isso aumenta a pressão do GC, pois há mais memória para acompanhar. Você também precisa considerar estratégias de remoção, invalidação de cache e segurança de threads.
513 | Um cache externo geralmente lida com o despejo, mas a invalidação do cache permanece um problema. As condições de corrida também podem ser um problema com caches externos, pois se torna um estado mutável efetivamente compartilhado entre goroutines diferentes no mesmo serviço ou até diferentes instâncias de serviço se o cache externo for compartilhado.
514 |
515 | Um cache salva as informações que você acabou de gastar em tempo de computação, na esperança de poder reutilizá-las novamente em breve e economizar tempo. Um cache não precisa ser complexo. Mesmo o armazenamento de um único item - a consulta / resposta vista mais recentemente - pode ser uma grande vitória, como visto no exemplo `time.Parse ()` abaixo.
516 |
517 | Com caches, é importante comparar o custo (em termos da complexidade real do relógio e do código) da sua lógica de cache para simplesmente buscar ou recalcular os dados.
518 |
519 | Os algoritmos mais complexos que oferecem taxas de acerto mais altas geralmente não são baratos. A remoção aleatória de cache é simples e rápida e pode ser eficaz em muitos
520 | casos. Da mesma forma, a inserção aleatória * de cache * pode limitar seu cache apenas a itens populares com lógica mínima. Embora estes possam não ser tão eficazes quanto os
521 | algoritmos mais complexos, a grande melhoria será adicionar um cache em primeiro lugar: escolher exatamente qual algoritmo de cache oferece apenas pequenas melhorias.
522 |
523 | É importante avaliar sua escolha do algoritmo de remoção de cache com rastreamentos do mundo real. Se, no mundo real, as solicitações repetidas são suficientemente raras, pode
524 | ser mais caro manter as respostas em cache do que simplesmente recalculá-las quando necessário. Eu tive serviços em que o teste com dados de produção mostrou que nem um cache ideal valeu a pena. simplesmente não tivemos solicitações repetidas suficientes para fazer sentido a complexidade adicional de um cache.
525 |
526 |
527 | A taxa de acertos esperados do cache é importante. Você deseja exportar a proporção para sua pilha de monitoramento. A alteração das taxas mostrará uma mudança no tráfego.
528 | Chegou a hora de revisitar o tamanho do cache ou a política de expiração.
529 |
530 | Um cache grande pode aumentar a pressão do GC. No extremo (pouca ou nenhuma remoção, cache de todas as solicitações para uma função cara), isso pode se transformar em [memorização] (https://en.wikipedia.org/wiki/Memoization)
531 |
532 | Melhoria do programa:
533 |
534 | Melhoria do programa é a arte de melhorar iterativamente um programa em pequenas etapas.
535 | Egon Elbre apresenta seu procedimento:
536 |
537 | * Crie uma hipótese de por que seu programa é lento.
538 | * Crie N soluções para resolvê-lo
539 | * Experimente todos e mantenha o mais rápido.
540 | * Mantenha o segundo mais rápido por precaução.
541 | * Repetir.
542 |
543 | As afinações podem assumir várias formas.
544 |
545 | * Se possível, mantenha a antiga implementação para teste.
546 | * Se não for possível, gere casos de teste de ouro suficientes para comparar a saída.
547 | * "Suficiente" significa incluir casos extremos, pois esses são os que provavelmente serão afetados pelo ajuste
548 | com o objetivo de melhorar o desempenho no caso geral.
549 | * Explorar uma identidade matemática:
550 | * Observe que implementar e otimizar cálculos numéricos é quase seu próprio campo
551 | *
552 | *
553 | * multiplicação com adição
554 | * use WolframAlpha, Maxima, sympy e similares para especializar, otimizar ou criar tabelas de pesquisa
555 | * (Além disso, https://users.ece.cmu.edu/~franzf/papers/gttse07.pdf)
556 | * movendo-se da matemática do ponto flutuante para a matemática inteira
557 | * ou mandelbrot removendo sqrt ou lttb removendo abs, `a ` a * c
606 | ```
607 |
608 | Para cada linha, nós iremos chamar `time.Parse()` para transformá-la em época. Se
609 | o perfilamento nos mostra que `time.Parse()` é um gargalo, nós temos algumas opções para
610 | speed things up.
611 |
612 | A mais fácil é manter um cache contendo uma única linha, o tempo anterior e a época associada. Enquanto o nosso arquivo de log tiver múltiplas linhas para um mesmo segundo, teremos uma vitória. Para o caso de um arquivo de log com 10 milhões de linhas, essa estratégia reduz o número de chamadas caras `time.Parse()` de 10.000.000 to 86.400 -- uma para cada segundo único.
613 |
614 | TODO: exemplo de código para cache de item único
615 |
616 | Podemos fazer mais? Como sabemos exatamente em que formato os carimbos de data e hora estão *e* em que todos caem em um único dia, podemos escrever uma lógica de análise de
617 | tempo personalizada que leva isso em consideração. Podemos calcular a época da meia-noite, extrair horas, minutos e segundos da sequência do carimbo de data/hora - todos eles
618 | estarão em desvios fixos na sequência - e fazer algumas contas inteiras.
619 |
620 | TODO: exemplo de código para versão de deslocamento de string
621 |
622 | Nas minhas avaliações, isso reduziu o tempo de análise de 275ns/op para 5ns/op. (Obviamente, mesmo com 275 ns/op, é mais provável que você seja bloqueado na E/S e não na CPU para analisar o tempo.)
623 |
624 | O algoritmo geral é lento porque precisa lidar com mais casos. Seu algoritmo pode ser mais rápido porque você sabe mais sobre o seu problema. Mas o código está mais intimamente
625 | ligado ao que você precisa. É muito mais difícil atualizar se o formato da hora mudar.
626 |
627 | Otimização é especialização, e códigos especializados são mais frágeis de serem alterados que códigos de uso geral.
628 |
629 | As implementações da biblioteca padrão precisam ser "rápidas o suficiente" para a maioria dos casos. Se você tiver necessidades de desempenho mais altas, provavelmente
630 | precisará de implementações especializadas.
631 |
632 | Faça um perfil regularmente para garantir o rastreamento das características de desempenho do seu sistema e esteja preparado para otimizar novamente conforme o tráfego muda.
633 | Conheça os limites do seu sistema e tenha boas métricas que permitem prever quando você atingirá esses limites.
634 |
635 | Quando o uso do aplicativo é alterado, diferentes partes podem se tornar pontos de acesso. Revise as otimizações anteriores e decida se ainda valem a pena e volte para um
636 | código mais legível, quando possível. Eu tinha um sistema que otimizava o tempo de inicialização do processo com um conjunto complexo de mmap, refletir e inseguro. Depois que
637 | alteramos a maneira como o sistema foi implantado, esse código não era mais necessário e eu o substituí por operações regulares de arquivos muito mais legíveis.
638 |
639 | ### Resumo do fluxo de trabalho de otimização ( Ou melhoria de perfomance!)
640 |
641 | Todas as otimizações devem seguir estas etapas:
642 |
643 | 1. determine seus objetivos de desempenho e confirme que não os está atingindo
644 | 1. Avalie para identificar as áreas a serem melhoradas.
645 | * Pode ser CPU, alocações de heap ou bloqueio de goroutine.
646 | 1. Avaliação para determinar a velocidade que sua solução fornecerá usando
647 | a estrutura de benchmarking integrada ()
648 | * Verifique se você está avaliando a coisa certa em seu alvo
649 | sistema operacional e arquitetura.
650 | 1. Avalie novamente depois para verificar se o problema desapareceu
651 | 1. use ou
652 | para verificar se um conjunto de checagens
653 | são 'suficientemente' diferentes para que uma otimização valha o acréscimo
654 | complexidade do código.
655 | 1. use para carregar serviços http de teste
656 | (+ outros extravagantes: k6, fortio, fbender)
657 | - se possível, teste a aceleração / desaceleração além da carga em estado estacionário
658 | 1. verifique se seus números de latência fazem sentido
659 |
660 | TODO: mencione github.com/aclements/perflock como ferramenta de redução de ruído da CPU
661 |
662 | O primeiro passo é importante. Informa quando e onde começar a otimizar.
663 | Mais importante, ele também informa quando parar. Praticamente todas as otimizações
664 | adicione complexidade de código em troca de velocidade. E você pode *sempre* criar código
665 | Mais rápido. É um ato de equilíbrio.
666 |
667 |
668 | ## Garbage Collection
669 |
670 | Você paga pela alocação de memória mais de uma vez. O primeiro é obviamente quando você o aloca. Mas você também paga sempre que o garbage collector é executado.
671 |
672 | > Reduce/Reuse/Recycle.
673 | > -- @bboreham
674 |
675 | * Alocações de pilha vs. heap
676 | * O que causa alocações de heap?
677 | * Compreendendo a análise de escape (e a limitação atual)
678 | * / debug / pprof / heap e -base
679 | * Design da API para limitar alocações:
680 | * permitir a passagem de buffers para que o chamador possa reutilizar em vez de forçar uma alocação
681 | * você pode até modificar uma fatia no lugar com cuidado enquanto a digitaliza
682 | * passar uma estrutura pode permitir que o chamador empilhe a alocação
683 | * redução de ponteiros para reduzir o tempo de verificação do gc
684 | * fatias sem ponteiro
685 | * mapas com chaves e valores sem ponteiro
686 | * GOGC
687 | * reutilização de buffer (sync.Pool vs ou personalizado via go-slab, etc)
688 | * fatiamento x deslocamento: as gravações do ponteiro enquanto o GC está sendo executado precisam de uma barreira de gravação: https://github.com/golang/go/commit/b85433975aedc2be2971093b6bbb0a7dc264c8fd
689 | * nenhuma barreira de gravação se estiver gravando na pilha https://github.com/golang/go/commit/2140975ebde164ea1eaa70fc72775c03567f2bc9
690 | * use variáveis de erro em vez de errors.New () / fmt.Errorf () no site de chamada (desempenho ou estilo? a interface requer ponteiro, para que ele escape para a pilha de qualquer maneira)
691 | * use erros estruturados para reduzir a alocação (passe o valor da estrutura), crie uma string no momento da impressão de erro
692 | * classes de tamanho
693 | * cuidado ao fixar alocação maior com substrings ou fatias menores
694 |
695 | ## Tempo de execução e compilador
696 |
697 | * custo de chamadas via interfaces (chamadas indiretas no nível da CPU)
698 | * runtime.convT2E / runtime.convT2I
699 | * asserções de tipo vs. comutadores de tipo
700 | * defer
701 | * implementações de mapas de casos especiais para ints, strings
702 | * mapa para byte / uint16 não otimizado; use uma fatia.
703 | * Você pode falsificar um float64 otimizado com math.Float {32,64} {from,} bits, mas cuidado com os problemas de igualdade de float
704 | * https://github.com/dgryski/go-gk/blob/master/exact.go diz 100x mais rápido; precisa de benchmarks
705 | * eliminação de verificação de limites
706 | * [] byte <-> cópias de string, otimizações de mapa
707 | * o intervalo de dois valores copiará uma matriz, use a fatia:
708 | *
709 | *
710 | * use concatenação de strings em vez de fmt.Sprintf sempre que possível; tempo de execução otimizou rotinas para ele
711 |
712 | ## Unsafe
713 |
714 | * E todos os perigos que a acompanham
715 | * Usos comuns para inseguros
716 | * mmap'ing arquivos de dados
717 | * struct padding
718 | * mas nem sempre suficientemente rápido para justificar o custo de complexidade / segurança
719 | * mas "fora da pilha", tão ignorado pelo gc (mas uma fatia sem ponteiros)
720 | * precisa pensar no formato de serialização: como lidar com ponteiros, indexação (mph, cabeçalho do índice)
721 | * desserialização rápida
722 | * protocolo de conexão binária para estruturar quando você já possui o buffer
723 | * string <-> conversão de fatia, [] byte <-> [] uint32, ...
724 | * int para boolear hack inseguro (mas cmov) (mas! = 0 também é livre de ramificação)
725 | * preenchimento:
726 | - https://dave.cheney.net/2015/10/09/padding-is-hard
727 | - http://www.catb.org/esr/structure-packing/#_go_and_rust
728 | - https://golang.org/ref/spec#Size_and_alignment_guarantees
729 | - https://github.com/dominikh/go-tools structlayout, structlayout-optimize
730 | - escreva testes para o layout da estrutura com inseguro. Offsetof para perceber quebras de inseguras ou asm
731 |
732 | ## Dicas comuns com a biblioteca padrão
733 |
734 | * time.After () vaza até disparar; use t: = NewTimer (); t.Stop () / t.Reset ()
735 | * Reutilizando conexões HTTP ...; garantir que o corpo seja drenado (questão nº?)
736 | * rand.Int () e seus amigos são 1) protegidos por mutex e 2) caros para criar
737 | * considere a geração alternativa de números aleatórios (go-pcgr, xorshift)
738 | * binary.Read e binary.Write usam reflexão e são lentos; faça à mão. (https://github.com/conformal/yubikey/commit/613e3b04ae2eeb78e6a19636b8ff8e9106d2e7bc)
739 | * use strconv em vez de fmt, se possível
740 | * Use `strings.EqualFold (str1, str2)` em vez de `strings.ToLower (str1) == strings.ToLower (str2)` ou `strings.ToUpper (str1) == strings.ToUpper (str2)` para comparar eficientemente cordas, se possível.
741 | * ...
742 |
743 | ## Implementações alternativas
744 |
745 | Substituições populares para pacotes de bibliotecas padrão:
746 |
747 | * codificação / json -> [ffjson] (https://github.com/pquerna/ffjson), [easyjson] (https://github.com/mailru/easyjson), [jingo] (https: // github. com / bet365 / jingo) (apenas codificador), etc
748 | * net / http
749 | * [fasthttp] (https://github.com/valyala/fasthttp/) (mas API incompatível, não compatível com RFC de maneiras sutis)
750 | * [activationprouter] (https://github.com/julienschmidt/httprouter) (tem outros recursos além da velocidade; nunca vi roteamento nos meus perfis)
751 | * regexp -> [ragel] (https://www.colm.net/open-source/ragel/) (ou outro pacote de expressões regulares)
752 | * serialização
753 | * encoding / gob ->
754 | * protobuf ->
755 | * todos os formatos de serialização têm vantagens: escolha um que corresponda ao que você precisa
756 | - Grave carga de trabalho pesada -> velocidade de codificação rápida
757 | - Carga de trabalho pesada -> velocidade de decodificação rápida
758 | - Outras considerações: tamanho codificado, compatibilidade de idioma / ferramentas
759 | - compensações de formatos binários compactos vs. formatos de texto autoexplicativos
760 | * database / sql -> possui trocas que afetam o desempenho
761 | * procure drivers que não o utilizem: jackx / pgx, crawshaw sqlite, ...
762 | * gccgo (referência!), gollvm (WIP)
763 | * container / list: use uma fatia (quase sempre)
764 |
765 | ## cgo
766 |
767 | > cgo não é go
768 | > - Rob Pike
769 |
770 | * Características de desempenho de chamadas cgo
771 | * Truques para reduzir os custos: lote
772 | * Regras sobre a passagem de ponteiros entre Go e C
773 | * arquivos syso (detector de corrida, dev.boringssl)
774 |
775 | ## Técnicas avançadas
776 |
777 | Técnicas específicas para a arquitetura executando o código
778 |
779 | * introdução aos caches da CPU
780 | * falésias de desempenho
781 | * construir intuição em torno das linhas de cache: tamanhos, preenchimento, alinhamento
782 | * Ferramentas do SO para visualizar erros de cache (perf)
783 | * mapas vs. fatias
784 | * Layouts SOA vs AOS: linha principal x coluna principal; quando você tem um X, precisa de outro X ou de Y?
785 | * localidade temporal e espacial: use o que você tem e o que está por perto o máximo possível
786 | * reduzindo a perseguição do ponteiro
787 | pré-busca explícita de memória; freqüentemente ineficaz; falta de intrínseca significa sobrecarga de chamada de função (removida do tempo de execução)
788 | * faça os primeiros 64 bytes da sua estrutura contar
789 | * previsão de ramificação
790 | * remova ramos dos circuitos internos:
791 | se um {for {}} else {for {}}
792 | ao invés de
793 | para {if a {} else {}}
794 | benchmark devido à previsão de ramificação
795 | estrutura para evitar ramificação
796 |
797 | se i% 2 == 0 {
798 | evens ++
799 | } outro {
800 | odds ++
801 | }
802 |
803 | conta [i & 1] ++
804 | "código sem ramificação", referência; nem sempre mais rápido, mas frequentemente mais difícil de ler
805 | TODO: classe ASCII conta exemplo, com benchmarks
806 |
807 | * a classificação dos dados pode ajudar a melhorar o desempenho por meio da localização do cache e da previsão de ramificação, mesmo levando em consideração o tempo necessário para classificar
808 | * sobrecarga de chamada de função: o inliner está melhorando
809 | * reduz cópias de dados (inclusive para grandes listas repetidas de parâmetros de função)
810 |
811 | * Comentário sobre os números de 2002 de Jeff Dean (mais atualizações)
812 | * cpus ficaram mais rápidos, mas a memória não se manteve
813 |
814 | TODO: pouco comentário sobre otimização livre de alinhamento de código (ou não otimização)
815 |
816 | ## Concorrência
817 |
818 | * Descubra quais peças podem ser feitas em paralelo e quais devem ser seqüenciais
819 | * goroutines são baratas, mas não gratuitas.
820 | * Otimizando código multiencadeado
821 | * compartilhamento falso -> tamanho do pad ao cache da linha
822 | * compartilhamento verdadeiro -> fragmentação
823 | * Sobreposição com a seção anterior sobre caches e compartilhamento falso / verdadeiro
824 | * Sincronização preguiçosa; é caro, portanto, duplicar o trabalho pode ser mais barato
825 | * coisas que você pode controlar: número de trabalhadores, tamanho do lote
826 |
827 | Você precisa de um mutex para proteger o estado mutável compartilhado. Se você tem muitos mutex
828 | contenção, você precisa reduzir o compartilhado ou o mutável. Dois
829 | maneiras de reduzir o compartilhado são 1) fragmentar os bloqueios ou 2) processar independentemente
830 | e combine depois. Para reduzir mutável: bem, faça sua estrutura de dados
831 | somente leitura. Você também pode reduzir o tempo que os dados precisam ser compartilhados, reduzindo
832 | a seção crítica - segure a trava o mínimo necessário. Às vezes, um RWMutex
833 | será suficiente, embora observe que eles são mais lentos, mas permitem vários
834 | leitores em.
835 |
836 | Se você estiver compartilhando os bloqueios, tenha cuidado com as linhas de cache compartilhadas. Você precisará preencher para evitar oscilações na linha de cache entre os processadores.
837 |
838 |
839 | var stripe [8] struct {sync.Mutex; _ [7] uint64} // mutex é de 64 bits; preenchimento preenche o restante do cacheline
840 |
841 | Não faça nada caro em sua seção crítica, se puder ajudá-lo. Isso inclui coisas como E / S (que são baratas, mas lentas).
842 |
843 | TODO: como decompor o problema por simultaneidade
844 | TODO: razões pelas quais a implementação paralela pode ser mais lenta (sobrecarga de comunicação, o melhor algoritmo é seqüencial, ...)
845 |
846 | ## Assembly
847 |
848 | * Coisas sobre como escrever código de montagem para o Go
849 | * compiladores melhoram; A barra está alta
850 | * substitua o mínimo possível para causar impacto; o custo de manutenção é alto
851 | * boas razões: instruções SIMD ou outras coisas fora do que o Go e o compilador podem fornecer
852 | * muito importante para avaliar: as melhorias podem ser enormes (10x para a rodovia)
853 | zero (go-speck / rc6 / farm32) ou até mais lento (sem inlining)
854 | * rebenchmark com novas versões para ver se você já pode excluir seu código
855 | * TODO: link para 1.11 patches removendo código asm
856 | * sempre tenha a versão pure-Go (purego build tag): testing, arm, gccgo
857 | * breve introdução à sintaxe
858 | * como digitar o ponto do meio
859 | * convenção de chamada: tudo está na pilha, seguido pelos valores de retorno.
860 | - tudo está na pilha, seguido pelos valores de retorno
861 | - isso pode mudar https://github.com/golang/go/issues/18597
862 | - https://science.raphael.poss.name/go-calling-convention-x86-64.html
863 | * usando opcodes não suportados pelo asm (asm2plan9, mas isso está ficando mais raro)
864 | * observações sobre por que a montagem em linha é difícil: https://github.com/golang/go/issues/26891
865 | * todas as ferramentas para facilitar isso:
866 | - asmfmt: gofmt para montagem https://github.com/klauspost/asmfmt
867 | - c2goasm: converte o assembly de gcc / clang para goasm https://github.com/minio/c2goasm
868 | - go2asm: converta ir para montagem, você pode vincular https://rsc.io/tmp/go2asm
869 | - peachpy / avo: assembler de nível superior em python (peachpy) ou Go (avo)
870 | - diferenças acima
871 | * https://github.com/golang/go/wiki/AssemblyPolicy
872 | * Design do Go Assembler: https://talks.golang.org/2016/asm.slide
873 |
874 | ## Otimizando um serviço inteiro
875 |
876 | Na maioria das vezes, você não recebe uma única rotina vinculada à CPU.
877 | Esse é o caso fácil. Se você possui um serviço para otimizar, precisa procurar
878 | em todo o sistema. Monitoramento. Métricas. Registre muitas coisas ao longo do tempo
879 | para que você possa vê-los piorando e para ver o impacto que seu
880 | mudanças têm em produção.
881 |
882 | tip.golang.org/doc/diagnostics.html
883 |
884 | * referências para o design do sistema: Livro SRE, design prático do sistema distribuído
885 | * ferramentas extras: mais registro + análise
886 | * As duas regras básicas: acelerar as coisas lentas ou fazê-las com menos frequência.
887 | * rastreamento distribuído para rastrear gargalos em um nível superior
888 | * padrões de consulta para consultar um único servidor em vez de em massa
889 | * seus problemas de desempenho podem não ser o seu código, mas você precisará contorná-los de qualquer maneira
890 | * https://docs.microsoft.com/en-us/azure/architecture/antipatterns/
891 |
892 | ## Ferramentas
893 |
894 | ### Introdução aoProfilingpr
895 |
896 | Este é um guia rápido para usar as ferramentas do pprof. Existem muitos outros guias disponíveis sobre isso.
897 | Confira https://github.com/davecheney/high-performance-go-workshop.
898 |
899 | TODO (dgryski): vídeos?
900 |
901 | 1. Introdução ao pprof
902 | * vá ferramenta pprof (e )
903 | 1. Escrever e executar (micro) benchmarks
904 | * pequenos, como testes de unidade
905 | * perfil, extrair código quente para benchmark, otimizar benchmark, perfil.
906 | * -cpuprofile / -memprofile / -benchmem
907 | * 0,5 ns / op significa que ele foi otimizado -> como evitar
908 | * dicas para escrever boas marcas de microbench (remova trabalhos desnecessários, mas adicione linhas de base)
909 | 1. Como ler saída pprof
910 | 1. Quais são as diferentes partes do tempo de execução que aparecem
911 | * malloc, trabalhadores do gc
912 | * tempo de execução. \ _ ExternalCode
913 | 1. Macro-benchmarks (criação de perfil na produção)
914 | * maiores, como testes de ponta a ponta
915 | * net / http / pprof, muxer de depuração
916 | * por amostragem, atingir 10 servidores a 100Hz é o mesmo que atingir 1 servidor a 1000Hz
917 | 1. Usando -base para observar diferenças
918 | 1. Opções de memória: -inuse_space, -inuse_objects, -alloc_space, -alloc_objects
919 | 1. Perfil na produção; localhost + tsh ssh, cabeçalhos de autenticação, usando curl.
920 | 1. Como ler gráficos de chama
921 | ### Tracer
922 |
923 | ### Veja algumas ferramentas mais interessantes / avançadas
924 |
925 | * outras ferramentas em /x/ perf
926 | * perf (perf2pprof)
927 | * intel vtune / amd codexl / apple instruments
928 | * https://godoc.org/github.com/aclements/go-perf
929 |
930 | Apêndice: Implementando documentos de pesquisa
931 |
932 | Dicas para implementar documentos: (Para `algoritmo`, leia também` estrutura de dados`)
933 |
934 | * Não. Comece com a solução óbvia e estruturas de dados razoáveis.
935 |
936 | Os algoritmos "modernos" tendem a ter complexidades teóricas mais baixas, mas alta constante
937 | fatores e muita complexidade de implementação. Um dos exemplos clássicos de
938 | isso é montes de Fibonacci. Eles são notoriamente difíceis de acertar e têm
939 | um enorme fator constante. Existem vários artigos publicados comparando
940 | implementações de heap diferentes em diferentes cargas de trabalho e, em geral, as
941 | ou montes implícitos de 8 árias aparecem sempre no topo. E mesmo nos casos
942 | onde a pilha de Fibonacci deve ser mais rápida (devido a O (1) "chave de diminuição"),
943 | experimentos com o algoritmo de busca profunda da Dijkstra mostram que é mais rápido
944 | quando eles usam a remoção e adição direta de heap.
945 |
946 | Da mesma forma, treaps ou skiplists vs. as árvores mais complexas de vermelho-preto ou AVL.
947 | No hardware moderno, o algoritmo "mais lento" pode ser rápido o suficiente ou até
948 | Mais rápido.
949 |
950 | > O algoritmo mais rápido pode frequentemente ser substituído por um que é quase tão rápido e muito mais fácil de entender.
951 | >
952 | > - Douglas W. Jones, Universidade de Iowa
953 |
954 | A complexidade adicional deve ser suficiente para que o retorno valha a pena.
955 | Outro exemplo são os algoritmos de remoção de cache. Algoritmos diferentes podem ter
956 | complexidade muito mais alta, apenas para uma pequena melhoria na taxa de acertos. Claro,
957 | talvez não seja possível testá-lo até que você tenha uma implementação funcional e
958 | integrou-o ao seu programa.
959 |
960 | Às vezes, o trabalho tem gráficos, mas é muito parecido com a tendência
961 | publicando apenas resultados positivos, estes tenderão a ser distorcidos em favor de
962 | mostrando o quão bom é o novo algoritmo.
963 |
964 | * Escolha o papel certo.
965 | * Procure o artigo que seu algoritmo pretende superar e implementar.
966 |
967 | Freqüentemente, artigos anteriores serão mais fáceis de entender e necessariamente terão
968 | algoritmos mais simples.
969 |
970 | Nem todos os papéis são bons.
971 |
972 | Veja o contexto em que o artigo foi escrito. Determine suposições sobre
973 | hardware: espaço em disco, uso de memória etc. Alguns papéis mais antigos
974 | tradeoffs diferentes que eram razoáveis nos anos 70 ou 80, mas não
975 | necessariamente se aplica ao seu caso de uso. Por exemplo, o que eles determinam ser
976 | memória "razoável" versus trocas de uso de disco. Os tamanhos de memória agora são pedidos de
977 | magnitude maior e os SSDs alteraram a penalidade de latência pelo uso do disco.
978 | Da mesma forma, alguns algoritmos de streaming são projetados para hardware de roteador, o que
979 | pode dificultar a tradução em software.
980 |
981 | Verifique se as suposições que o algoritmo faz sobre seus dados são mantidas.
982 |
983 | Isso vai demorar um pouco. Você provavelmente não deseja implementar o
984 | primeiro artigo que você encontrar.
985 |
986 | * Certifique-se de entender o algoritmo. Isso parece óbvio, mas será
987 | impossível depurar de outra maneira.
988 |
989 |
990 |
991 | Um bom entendimento pode permitir que você extraia a ideia-chave do documento
992 | e, possivelmente, aplique exatamente isso ao seu problema, que pode ser mais simples do que
993 | reimplementando a coisa toda.
994 |
995 | * O documento original de uma estrutura ou algoritmo de dados nem sempre é o melhor. Trabalhos posteriores podem ter melhores explicações.
996 |
997 | * Alguns trabalhos publicam código fonte de referência com o qual você pode comparar, mas
998 | 1) o código acadêmico é quase universalmente terrível
999 | 2) cuidado com as restrições de licença ("apenas para fins de pesquisa")
1000 | 3) cuidado com os erros; casos extremos, verificação de erros, desempenho etc.
1001 |
1002 | Também procure outras implementações no GitHub: elas podem ter os mesmos (ou diferentes!) Bugs que o seu.
1003 |
1004 | Outros recursos sobre este tópico:
1005 | *
1006 | *
1007 |
--------------------------------------------------------------------------------
/performance-zh.md:
--------------------------------------------------------------------------------
1 | # 编写和优化Go代码
2 |
3 | 本文档概述了编写高性能Go代码的最佳实践。
4 |
5 | 虽然有些讨论会提高单个服务的速度(通过缓存等),但设计高性能的分布式系统已经超出了这项工作的范围。在监控和分布式系统设计方面已经有很好的文章,它包含了一套完全不同的研究和设计权衡理论。
6 |
7 | 所有内容将根据CC-BY-SA进行许可。
8 |
9 | 本书分为以下几节:
10 |
11 | 1) 编写高性能软件的基本技巧
12 | * CS 101-level的东西
13 | 2) 编写快速软件的技巧
14 | * 关于如何从Go获得最佳效果的Go-specific章节
15 | 3) 编写*真正*快速软件的高级技巧
16 | * 当你优化的代码不够快时
17 |
18 | 我们可以总结这三个部分:
19 | - “合理的”
20 | - “慎重的”
21 | - “危险的”
22 |
23 |
24 | ### 何时何地做优化
25 |
26 | 我先把这个放在第一位,是因为这真的是最重要的一步。你真的应该这么做吗?
27 |
28 | 每个优化都有成本。通常,这个成本是用代码复杂度或认知负载来承担的 - 优化后的代码很少比优化前的版本简单。
29 |
30 | 但另一方面,我称之为“优化经济学”。作为程序员,你的时间是宝贵的。你可以为你的项目工作的机会成本,哪些Bug需要修复,以及需要添加哪些功能。优化的工作是很有趣的,但并不总是正确的选择。性能是一种特性,但交付和正确性也是如此。
31 |
32 | 选择最重要的工作。有时它不是一个实际的CPU优化,而是一个用户体验。就像添加进度条一样简单,或者在渲染页面后通过在后台执行计算来提高页面的响应速度。
33 |
34 | 有时这是显而易见的:在三小时内完成的报告在一小时完成可能不太有用。
35 |
36 | 仅仅因为容易优化并不意味着它是值得优化的。忽略low-hang的效果是一种有效的发展战略。
37 |
38 | 把这看作是优化*你的*时间。
39 |
40 | 选择要优化的内容以及何时优化,你可以在“软件质量”和“开发速度”之间移动滑块。
41 |
42 | 人们无意识地重复说名言——“过早的优化是万恶之源”,但他们错过了它的主要内容。
43 |
44 | “程序员浪费了大量的时间来思考或者担心程序中非关键部分的速度,而这些效率的尝试实际上在考虑调试和维护时会产生很大的负面影响。我们应该忘记为了小的性能使用的97%的时间:过早的优化是万恶之源,但我们不应该在这个关键的3%中放弃我们的优化机会。“ - Knuth
45 |
46 | 附:https : //www.youtube.com/watch?time_continue=429&v=3WBaY61c9sE
47 |
48 | - 不要忽视简单的优化
49 | - 更多的算法和数据结构知识使得更多的优化变得“容易”或“明显”
50 |
51 | “你应该优化吗?”是的,但是只有当问题很重要时,程序真的太慢了,并且能够在保证正确性,稳健性和清晰度的同时变得更快。“ - 编程实践,Kernighan and Pike
52 |
53 | [BitFunnel性能评估](http://bitfunnel.org/strangeloop) 有一些数字可以使这种权衡更加明确。想象一下假设搜索引擎需要跨越多个数据中心的30,000台机器,这些机器每年的成本约为1,000美元。如果你可以将软件的速度提高一倍,这可以为公司节省每年1500万美元。即使只有一个开发人员花费整整一年时间才能将性能提高也只会付出1%的代价。
54 |
55 | 在绝大多数情况下,程序的大小和速度不是问题。最简单的优化不必这样做。第二个最简单的优化就是购买更快的硬件。
56 |
57 | 如果你决定要改变你的程序,请继续阅读。
58 |
59 | ## 如何优化
60 |
61 | ### 优化工作流程
62 | 在介绍具体细节之前,我们先谈谈优化的一般过程。
63 |
64 | 优化是一种重构形式。但是,每一步不是改进源代码的某些方面(代码重复,清晰度等),而是可以提高性能的某些方面:降低CPU,内存使用率,延迟等。这种改进通常以可读性为代价。这意味着除了一套全面的单元测试(以确保你的更改没有破坏任何内容)之外,你还需要一套很好的基准测试,以确保您的更改对性能产生预期的影响。你必须能够验证您的更改是否真的在降低CPU。有时候你认为会改善性能的变化实际上会变成零或负变化。在这些情况下,务必确保撤消修改的程序。
65 |
66 | [源代码中遇到过的最好的评论是什么?- Stack Overflow](https://stackoverflow.com/questions/184618/what-is-the-best-comment-in-source-code-you-have-ever-encountered)
67 |
68 | ```go
69 | //
70 | //亲爱的维护者:
71 | //
72 | //当你完成试图“优化”这个程序,
73 | //并且已经意识到了什么可怕的错误时,
74 | //请增加以下计数器作为给后人的警告:
75 | //
76 | //total_hours_wasted_here = 42
77 | //
78 | ```
79 |
80 | 你使用的基准测试必须正确,并为代表性工作负载提供可重复的数字。如果单个的运行差异太大,则会使得小的改进更难以发现。你将需要使用[benchstat](https://golang.org/x/perf/benchstat)或等效的统计测试,而不能只是用眼睛去看(请注意,使用统计测试无论如何都是一个好主意)。应该记录运行基准测试的步骤,并且应该向存储库提交任何自定义脚本和工具,并提供如何运行它们的说明。要注意需要很长时间才能运行的大型基准测试套件:它会使开发迭代变慢。
81 |
82 | 还要注意,任何可以测量的东西都可以优化。确保你正在衡量正确的事情。
83 |
84 | 下一步是决定你正在优化什么。如果目标是改进CPU,那么什么是可接受的速度。你想要将当前的性能提高2倍吗?10倍?你能否说它是“小于时间T的大小为N的问题”?你想减少内存使用量吗?多少钱?对于内存使用情况的变化,可以接受的速度有多慢?你愿意放弃什么来换取较低的空间需求?
85 |
86 | 优化服务延迟是一个棘手的问题。整本书都是关于如何对Web服务器进行性能测试的。主要问题是:对于单个函数,对于给定的问题规模,性能相当一致。对于webservices,你没有一个单一的性能数字。一个适当的Web服务基准套件将为给定的需求/秒级别提供延迟分布。这篇演讲很好地概述了Gil Tene的一些问题:["如何不去测量延迟" by Gil Tene](https://youtu.be/lJ8ydIuPFeU)
87 |
88 | TODO:请参阅后面的关于优化Web服务的部分
89 |
90 | 绩效目标必须具体。你会(几乎)总是能够更快地做出一些事情。优化往往是一个收益递减的游戏。你需要知道何时停止。你要付出多少努力才能完成最后一点工作。你愿意做出这样的代码是多么难以维护?
91 |
92 | Dan Luu之前提到的[BitFunnel性能评估](http://bitfunnel.org/strangeloop)的演讲显示了一个使用粗略计算来确定目标性能数据是否合理的例子。
93 |
94 | TODO:编程珠玑有“Fermi Problems”。从Jeff Dean's 幻灯片可以了解
95 |
96 | 对于绿地开发,你不应该把所有的基准和性能数字都留到最后。很容易说“我们稍后会修复”,但如果性能非常重要,那么从一开始就将是一个设计考虑因素。在解决性能问题时所需的任何重大体系结构更改在截止日期前将过于冒险。请注意,在开发过程中,重点应放在合理的程序设计,算法和数据结构上。在更低层次的堆栈优化应该等到开发周期晚些时候才能获得更完整的系统性能视图。你在系统不完整时执行的任何完整系统配置文件都会对完成系统中瓶颈的位置给出偏斜视图。
97 |
98 | TODO:如何避免/发现软件写得不好的情况下的“凌迟”。
99 |
100 | 作为CI的一部分,基准测试是很难的,因为嘈杂的因素,甚至不同的CI盒子,那么很难获取性能指标。一个好的基础是让开发人员运行基准测试(在适当的硬件上)并将其包含在提交消息中,专门用于处理性能问题。对于那些只是提普通补丁的人来说,尽量在代码审查中捕捉性能下降。
101 |
102 | TODO:如何跟踪一段时间的性能表现?
103 |
104 | 编写你可以测试的代码。你可以在较大的系统上执行分析。你可以通过基准测试测试孤立的部分。你需要能够提取并设置足够的环境上下文,以便基准测试足够并具有代表性。
105 |
106 | 你的目标是什么和目前的表现之间的差异也会让你知道从哪里开始。如果你只需要10%-20%的性能改进,那么可以通过一些实施调整和较小的修复来实现。如果你需要一个10倍或更多的因子,那么用一个左移代替一个乘法不会削减它。这可能会要求你的堆栈上下进行更改。
107 |
108 | 良好的性能工作需要从系统设计,网络,硬件(CPU,缓存,存储),算法,调整和调试等多个不同层面的知识。在时间和资源有限的情况下,考虑哪个级别能够提供最大的改进:它并不总是算法或程序调优。
109 |
110 | 一般而言,优化应该从上到下进行。系统级别的优化将比表达级别的影响更大。确保你在适当的水平上解决问题。
111 |
112 | 本书主要讨论如何减少CPU使用率,减少内存使用量并减少延迟。很高兴指出你很少能做到这三点。也许CPU时间更快,但现在你的程序使用更多的内存。也许你需要减少内存空间,但现在该程序需要更长的时间。
113 |
114 | [阿姆达尔定律](https://en.wikipedia.org/wiki/Amdahl%27s_law)告诉我们要关注瓶颈。如果你将运行时间仅占5%的代码速度提高一倍,那么整个程序的速度只提升了2.5%。但是,将运行时间占80%的代码加速10%,整体运行时间将提高近8%。配置文件将有助于确定实际花费的时间。
115 |
116 | 在做优化时,你想减少CPU必须完成的工作量。快速排序比冒泡排序更快,因为它能以更少的步骤解决相同的问题(排序)。这是一个更高效的算法。这样就减少了CPU完成相同任务所需完成的工作。
117 |
118 | 像编译器优化一样,程序调优通常只会在整个运行时间中造成一点小小的负担。大的胜利几乎总是来自算法改变或数据结构的改变,这是你的程序组织方式的根本转变。编译器技术有所改进,但速度很慢。Proebsting定律表明,编译器每18 年的性能翻倍,这与摩尔定律(稍微误解了解释)形成鲜明对比,该定律使处理器性能每18 个月翻一番。算法改进在更大的范围内工作。从1991年到2008年,混合器整数规划算法提高了30,000倍 有关更具体的示例,请考虑[此故障](https://medium.com/@buckhx/unwinding-uber-s-most-efficient-service-406413c5871d)取代优步博客文章中描述的蛮力地理空间算法,使用更适合于所提交任务的更专业的算法。没有编译器开关可以提供相同的性能提升。
119 |
120 | TODO:在gttse07.pdf中优化浮点FFT和MMM算法的差异
121 |
122 | 分析器可能会告诉你,大量的时间都花在了特定的例程上。这可能是一个昂贵的例程,或者它可能是一个便宜的例程,只是被调用许多次。你可以先看看是否可以减少调用的次数或完全不调用,而不是立即尝试优化这个例程。我们将在下一节讨论更具体的优化策略。
123 |
124 | 三个优化问题:
125 |
126 | - 我们必须这样做吗?最快的代码是永远不会运行的代码。
127 | - 如果是的话,这是最好的算法。
128 | - 如果是的话,这是这个算法的最佳实现。
129 |
130 | ## 具体的优化技巧
131 |
132 | Jon Bentley在1982年的作品“编写高效程序”将程序优化视为一个工程问题:基准。分析。提高。校验。迭代。他的一些技巧现在由编译器自动完成。程序员的工作是使用编译器无法做到的转换。
133 |
134 | 本书的摘要如下:
135 | - http://www.crowl.org/lawrence/programming/Bentley82.html
136 | - http://www.geoffprewett.com/BookReviews/WritingEfficientPrograms.html
137 |
138 | 和程序调整规则:
139 | https://web.archive.org/web/20080513070949/http://www.cs.bell-labs.com/cm/cs/pearls/apprules.html
140 |
141 | 在考虑对程序进行更改时,有两个基本选项:你可以更改数据,也可以更改代码。
142 |
143 | ### 数据的更改
144 |
145 | 改变你的数据意味着增加或改变你正在处理的数据的表示。从性能角度来看,其中一些最终会改变与数据结构的不同方面相关的O()。
146 |
147 | 增加数据结构的想法:
148 |
149 | - 额外字段:例如,存储链接列表的大小,而不是在询问时迭代。或者将经常需要的其他节点的指针存储到多个搜索中(例如,双向链接列表中的“向后”链接以进行删除O(1))。当你需要的数据便于存储并保持最新时,这些更改很有用。
150 |
151 | - 额外的搜索索引:大多数数据结构都是为单一类型的查询而设计的。如果你需要两种不同的查询类型,对数据进行额外的“查看”可能会有很大的改进。例如,[] struct,由ID引用,但有时是string - > map [string] id(或* struct)
152 |
153 | - 有关元素的额外信息:例如布隆过滤器。这些数据结构必须小而快,以免压倒其余的数据结构。
154 |
155 | - 如果查询很昂贵,请添加一个缓存。我们都熟悉memcache,但还有进程内缓存。
156 | * 通过网络,网络+序列化成本将会受到影响
157 | * 进程内缓存,但现在你需要担心到期
158 | * 即使是单个项目也可以帮助(日志文件时间解析示例)
159 |
160 | TODO:“缓存”可能不是键值对,只是指向你工作的地方。这可以像“搜索手指”一样简单
161 |
162 | 这些都是数据结构层面“做更少工作”的明确例子。他们都花费空间。大多数情况下,如果你针对CPU进行优化,程序将使用更多的内存。这是经典的[时空交易](https://en.wikipedia.org/wiki/Space%E2%80%93time_tradeoff)
163 |
164 | 如果你的程序使用太多的内存,也可以换个方式。减少空间使用量以换取更多计算。而不是存储的东西,每次计算它们。你还可以压缩内存中的数据,并在需要时随时对其进行解压缩。
165 |
166 | [小内存软件](https://gamehacking.org/faqs/Small_Memory_Software.pdf)是一本网上可获取的书籍,涵盖了减少程序使用内存空间的技术。虽然它最初是针对嵌入式开发人员编写的,但这些想法适用于处理大量数据的现代硬件的程序。
167 |
168 | - 重新排列你的数据
169 | 消除结构填充。删除额外的字段。使用较小的数据类型
170 |
171 | - 更改为较慢的数据结构
172 | 较简单的数据结构通常具有较低的内存要求。例如,从一个指针重的树结构转向使用切片和线性搜索。
173 |
174 | - 为你的数据定制压缩格式
175 | []字节(snappy,gzip,lz4),浮点数(go-tsz),整数(delta,xor + huffman)大量的压缩资源。你需要检查数据还是可以保持压缩?你需要随机访问还是只有流媒体?压缩具有额外索引的块。如果不是在进程中,而是写入磁盘,那么迁移或添加/删除字段呢?你现在正在处理raw []字节而不是很好的结构化Go类型。
176 |
177 | 我们稍后会详细讨论数据布局。
178 |
179 | 现代计算机和存储器层次结构使空间/时间的权衡不太明确。查找表很容易在内存中“远离”(因此访问成本很高),使得每次需要时重新计算一次值都会更快。
180 |
181 | 这也意味着基准测试通常会显示由于缓存争用而导致生产系统无法实现的改进(例如,查找表在基准测试期间位于处理器缓存中,但在真实系统中使用时总是会被“真实数据”冲刷。哈希表实际上直接解决了这个问题,比较了满足和无约束的处理器缓存上的性能。参见[Jump Hash paper](https://arxiv.org/pdf/1406.2294.pdf)论文中的图4和图5。
182 |
183 | TODO:如何模拟满足的缓存,显示增量成本
184 |
185 | 另一个要考虑的方面是数据传输时间。通常,网络和磁盘访问非常缓慢,因此能够加载压缩块的速度将比获取数据后解压缩数据所需的额外CPU时间快得多。一如既往,基准。二进制格式通常比文本格式更小且更快解析,但代价是不再是人类可读的格式。
186 |
187 | 对于数据传输,转移到一个不那么有趣的协议,或者增加API以允许部分查询。例如,增量查询而不是每次都被迫获取整个数据集。
188 |
189 | ## 算法的更改
190 |
191 | 如果你不更改数据,另一个主要选项是更改代码。
192 |
193 | 最大的改进很可能来自算法变化。这与使用快速排序将气泡排序替换为从O(n ^ 2)排序到O(n log n)或使用映射查找替换通过过去是小O(n)的数组的线性扫描等效(O (1))。
194 |
195 | 最大的改进可能来自算法变化。这相当于用quicksort(O(nlogn))替换O(n^2)的冒泡排序或者通过哈希查找(O(1))替换数组(O(n))的线性扫描。
196 |
197 | 这就是软件如何变慢。最初设计用于一种用途的结构被重新用于未设计的东西。这是逐渐发生的。
198 |
199 | 直观地掌握不同的大O级别是很重要的。为你的问题选择正确的数据结构。你不必一直刮刮胡须,但是这样做可以防止很久以后才会发现的愚蠢的性能问题。
200 |
201 | 基本的复杂类别是:
202 |
203 | * O(1):字段访问,数组或地图查找
204 | 建议:不要担心
205 |
206 | * O(log n):二进制搜索
207 | 建议:如果处于循环状态,则只是一个问题
208 |
209 | * O(n):简单循环
210 | 建议:你一直在这样做
211 |
212 | * O(n log n):分而治之,排序
213 | 建议:还是相当快的
214 |
215 | * O(n * m):嵌套循环/二次方
216 | 建议:小心并限制你的大小
217 |
218 | * 二次和次指数之间的任何其他内容
219 | 建议:不要在一百万行上运行
220 |
221 | * O(b ^ n),O(n!):指数上升
222 | 建议:如果你有十几个或两个数据点,祝您好运
223 |
224 | 链接:
225 |
226 | 假设你需要搜索未分类的数据集。“我应该用二分搜索”,你知道一个二分搜索O(log n)比O(n)线性扫描快。但是,二分查找需要对数据进行排序,这意味着你需要先对它进行排序,这将花费O(n log n)时间。如果你正在进行大量搜索,那么分类的前期成本将会得到回报。另一方面,如果你主要做查询,也许有一个数组是错误的选择,你最好支付O(1)查找地图的代价。
227 |
228 | 选择最简单的合理数据结构并继续。这是用于编写“非慢速软件”的CS 101。这应该是您的默认开发模式。如果您知道需要随机访问,请不要选择链接列表。如果您知道需要按顺序遍历,请不要使用地图。需求变化,你不能总是猜测未来。对工作量做出合理的猜测。
229 |
230 | http://daslab.seas.harvard.edu/rum-conjecture/
231 |
232 | 类似问题的数据结构在做一件工作时会有所不同。随着插入的发生,二叉树每次排序一次。未排序的数组插入速度更快但未排序:最后,“敲定”你需要一次完成排序。
233 |
234 | 当编写一个供其他人使用的包时,避免每个用例都要优先考虑的诱惑。这将导致代码不可读。按设计的数据结构实际上是单一用途的。你既不能读懂头脑,也不能预测未来。如果用户说“你的软件包对于这个用例太慢”,一个合理的答案可能是“然后在这里使用这个软件包”。一揽子计划应该“做得很好”。
235 |
236 | 有时混合数据结构将提供你需要的性能改进。例如,通过分段数据,你可以将搜索范围限制在一个存储桶中。这仍然支付O(n)的理论成本,但常数会更小。当我们进行编程调整时,我们将重新审视这些调整。
237 |
238 | 在讨论大O符号时,人们忘记了两件事
239 |
240 | 其一,涉及到一个不变的因素。具有相同算法复杂度的两种算法可以具有不同的常数因子。想象一下,循环遍历一个列表100次,而仅循环一次。即使两者都是O(n),也有一个恒定的因子是100倍。
241 |
242 | 这些常数因素是为什么即使合并排序,快速排序和排列所有O(n log n),每个人都使用快速排序,因为它是最快的。它具有最小的常数因子。
243 |
244 | 第二件事是大O只说“随着n增长到无穷大”。它谈到了增长趋势,“随着数字变大,这是主导运行时间的增长因素。” 它没有提到实际的表现,也没有说明它如何表现小n。
245 |
246 | 经常有一个分界点,在这个分界点以下,木材算法更快。Go标准库sort包的一个很好的例子。大多数时候它使用快速排序,但是当分区大小降到12个元素以下时,它会进行shell排序传递,然后进行插入排序。
247 |
248 | 对于某些算法,常数因子可能非常大,以致此截点可能比所有合理的输入都大。也就是说,O(n ^ 2)算法对于所有可能处理的输入都比O(n)算法快。
249 |
250 | 这也是另一种方式:例如,即使小输入的基准变慢,选择使用更复杂的数据结构来给出O(n)缩放而不是O(n ^ 2)。这也适用于大多数无锁数据结构。它们通常在单线程情况下较慢,但在多线程使用它时更具可扩展性。
251 |
252 | 现代计算机中的存储器层次结构将问题混淆了一点,因为高速缓存更喜欢将片段扫描到追踪指针的有效随机访问的可预测访问。不过,最好从一个好的算法开始。我们将在硬件特定部分讨论这个问题。
253 |
254 | “这场斗争可能并不总是最强,也不是最快的比赛,但这是打赌的方式。” - 吉卜林。
255 |
256 | 有时,针对特定问题的最佳算法不是单一的算法,而是专门针对稍微不同的输入类的算法集合。这个“polyalgorithm”可以快速检测出需要处理的输入类型,然后发送到相应的代码路径。这就是上面提到的排序包所做的:确定问题的大小并选择不同的算法。除了结合quicksort,shell排序和插入排序之外,它还会跟踪快速排序的递归深度并在必要时调用堆排序。在string与bytes包做类似的事情,检测和专门处理不同的情况。与数据压缩一样,您对输入内容的了解越多,定制解决方案就越好。即使优化并不总是适用,通过确定使用和执行不同的逻辑是安全的,使代码复杂化可能是值得的。
257 |
258 | 这也适用于你的算法需要解决的子问题。例如,能够使用基数排序可以对性能产生重大影响,如果只需要部分排序,则可以使用快速选择。
259 |
260 | 有时候,而不是专门针对您的特定任务,最好的方法是将其抽象为研究人员已经充分研究的更一般的问题空间。然后,您可以将更一般的解决方案应用于您的特定问题。将你的问题映射到已经有很好研究实现的领域可能是一个重大的胜利。
261 |
262 | ## 基准输入
263 |
264 | 了解你的每种输入尺寸可能在生产中有多大。
265 |
266 | 你的基准测试必须使用适当大小的输入。正如我们所看到的,不同的算法在不同的输入大小下都有意义。如果你的预期输入范围<100,那么你的基准应该反映这一点。否则,选择最适合n = 10 ^ 6的算法可能不是最快的。
267 |
268 | 能够生成有代表性的测试数据。不同的数据分布会在你的算法中引发不同的行为:想想经典的“数据排序时快速排序为O(n ^ 2)”示例。类似地,对于均匀的随机数据,插值搜索是O(log log n),但是O(n)最差的情况。知道你的输入是什么样子是代表性基准和选择最佳算法的关键。如果你用来测试的数据不能代表实际工作负载,那么你可以轻松完成针对某个特定数据集的优化,“过度配置”你的代码以便使用一组特定的输入进行最佳工作。
269 |
270 | 这也意味着你的基准数据需要代表真实世界。如果重复的请求非常少见,保留它们比重新计算它们更昂贵。如果你的基准数据仅包含相同的重复请求,则缓存将提供不准确的性能视图。
271 |
272 | 另请注意,一旦部署到生产环境并且在40核心服务器上达到25万次/秒,笔记本电脑上可能看不到的一些问题就可以看到。
273 |
274 | 编写好的基准测试可能很困难。
275 | TODO:microbenchmarks显示速度减慢但宏观(现实世界)性能提高的情况。
276 |
277 | - https://timharris.uk/misc/five-ways.pdf
278 |
279 | ## 程序调整
280 |
281 | 程序调优曾经是一种艺术形式,但编译器变得更好。所以现在事实证明,编译器可以比复杂的代码更好地直接优化代码。Go编译器在匹配gcc和clang方面还有很长的路要走,但这确实意味着在调整时需要小心,特别是在升级Go版本时不要变得更糟。一旦编译器得到改进,肯定会出现一些针对缺少特定编译器优化工作的调整。
282 |
283 | TODO:https : //github.com/golang/go/commit/9eb219480e8de08d380ee052b7bff293856955f8)
284 |
285 | 如果你正在解决特定的运行时或编译器代码生成问题,请始终使用指向上游问题的链接记录你的更改。这可以让你在bug修复后快速重新访问你的优化。
286 |
287 | 打击基于民间传说的崇拜“性能提示”的诱惑,甚至是从你自己的经验中过度概括。每个性能缺陷都需要根据自身的优点加以处理。即使之前已经有效,确保配置文件确保修复仍然适用。你以前的工作可以指导你,但不要盲目应用以前的优化。
288 |
289 | 程序调优是一个迭代过程。继续重新访问你的代码并查看可以进行哪些更改。确保你在每一步都取得进展。经常有一项改进可以使其他人获得成功。(现在我没有做A,我可以通过做C来简化B)。这意味着你需要继续观察整个图片,而不是沉迷于一小组线。
290 |
291 | 一旦你确定了正确的算法,程序调优就是改进算法实现的过程。在Big-O表示法中,这是减少与程序相关的常量的过程。
292 |
293 | 所有的节目调整都要么让速度变慢,要么减慢速度。算法变化也属于这些类别,但我们将看到较小的变化。你的具体做法随技术变化而变化。
294 |
295 | 做一个缓慢的事情可能会用更快的散列函数替换SHA1或者hash/fnv1。少做一次缓慢的事情可能会节省一个大文件的哈希计算结果,因此你不必多次执行该操作。
296 |
297 | 保留意见。如果不需要做什么,请解释原因。通常,在优化算法时,你会发现在某些情况下不需要执行的步骤。记录它们。其他人可能会认为这是一个错误,需要放回去。
298 |
299 | 空程序立刻给出了错误的答案。
300 | 如果你不必是正确的,那么很快就会很快。
301 |
302 | “正确性”可以取决于问题。启发式算法大多数情况下是正确的,大部分时间都可以很快,而且猜测和改进的算法可以让您在达到可接受的限制时停下来。
303 |
304 |
305 | 缓存常见情况:
306 |
307 | * 你的缓存甚至不需要很大。
308 | * 参见下面的 time.Parse()例子; 只有一个价值观产生了影响
309 | * 但要注意缓存失效,线程问题等。
310 | * 随机缓存驱逐是快速且足够有效的。
311 | * 随机缓存插入可以用最少的逻辑将缓存限制为流行的项目。
312 | * 将缓存逻辑的成本与重新获取数据的成本进行比较。
313 | * 大容量缓存可能会增加GC压力并不断吹动处理器缓存。
314 | * 在极端情况下(很少或没有驱逐,将所有请求缓存到一个昂贵的函数),这可以变成记忆
315 |
316 |
317 | 我已经完成了一个网络跟踪实验,表明即使是最佳的缓存也不值得。你的预期命中率很重要。你需要将比率导出到你的监控堆栈。不断变化的比例将显示流量的变化。然后是重新访问缓存大小或过期策略的时候了。
318 |
319 | 程序调优:
320 |
321 | 程序调优是以小步骤迭代改进程序的艺术。Egon Elbre列出了他的程序:
322 | * 提出一个假设,为什么你的程序很慢。
323 | * 拿出N个解决方案来解决它
324 | * 尝试一切,并保持最快。
325 | * 以防万一。
326 | * 重复。
327 |
328 | 调整可以采取多种形式。
329 |
330 | * 如果可能,请保留旧的实现以进行测试。
331 | * 如果不可能,则生成足够的黄金测试用例来比较输出。
332 | “足够”意味着包括边缘案例,因为这些可能会受到调优的影响,因为您旨在提高一般情况下的性能。
333 | * 利用数学身份:
334 | https://github.com/golang/go/commit/ed6c6c9c11496ed8e458f6e0731103126ce60223
335 | https://gist.github.com/dgryski/67e6a7ff94c3a1add30eb26ec0ad8b0f
336 | * 与加法相乘
337 | * 使用WolframAlpha,Maxima,sympy和类似工具来专门化,优化或创建查找表
338 | (另外,https://users.ece.cmu.edu/~franzf/papers/gttse07.pdf)
339 | * “只为你使用的东西付费,而不是你可以使用的东西”
340 | * 零只是数组的一部分,而不是整个事物
341 | * 最好以微小的步骤完成,一次只做几个陈述
342 | * 从浮点数学到整数数学
343 | * 或者mandelbrot删除sqrt,或者lttb删除abs, a < b/c=>a * c < b
344 | * 在更昂贵的支票前进行廉价支票
345 | * 例如,在正则表达式之前的strcmp,(qv,在查询之前的布隆过滤器)“少花费更多时间”
346 | * 在罕见情况之前的常见情况,即避免总是失败的额外测试
347 | * 展开仍然有效:https://play.golang.org/p/6tnySwNxG6O
348 | * 代码大小。vs分支测试开销
349 | * 使用偏移而不是切片分配可以帮助进行边界检查,数据依赖性和代码生成(少于在内部循环中复制)。
350 | * 这就是Hacker's Delight的一部分
351 | * 考虑不同的数字表示法:定点,浮点,(小)整数,
352 | * 爱好者:带误差累加器的整数(如Bresenham的线和圆),多基数/冗余数字系统
353 |
354 |
355 | 许多针对调优的民间传说性能提示依赖于对编译器的优化不足,并鼓励程序员手动完成这些转换。编译器一直在使用更新,而不是用15年的时间乘以或除以2的幂 - 现在没有人应该亲自去做。类似地,提升循环中的不变计算,基本循环展开,常见子表达式消除等等都是由gcc和clang等自动完成的。Go的编译器完成了其中的许多工作,并继续改进。一如往常,在提交新版本之前进行基准测试。
356 |
357 | 编译器无法做到的转换依赖于你了解有关算法,输入数据,系统中的不变量以及可以做出的其他假设等事情,并将该隐式知识分解为删除或更改数据结构中的步骤。
358 |
359 | 每个优化都会对你的数据进行假设。这些必须记录下来,甚至更好地进行测试。这些假设将会在你的程序崩溃,放慢速度,或随着系统发展而开始返回错误数据的地方。
360 |
361 | 程序调整改进是累积的。5倍3%的改善是15%的改善。进行优化时,值得考虑预期的性能改进。用更快的替换哈希函数是一个不断改进的因素。
362 |
363 | 了解你的要求和可以改变的地方可以提高性能。在#performance Gophers Slack频道中呈现的一个问题是用于为字符串键/值对映射创建唯一标识的花的数量。最初的解决方案是提取键,对它们进行排序,并将结果字符串传递给散列函数。我们提出的改进解决方案是在键/值添加到地图时对其进行单独散列处理,然后将所有这些散列在一起以创建标识符。
364 |
365 | 这是一个专业化的例子。
366 |
367 | 假设我们正在处理一天中的大量日志文件,并且每行都以时间戳开始。
368 |
369 | Sun 4 Mar 2018 14:35:09 PST <...........................>
370 | 对于每行,我们调用time.Parse()把它变成一个格式。如果性能分析显示我们time.Parse()是瓶颈,那么我们有几种方法可以加快速度。
371 |
372 | 最简单的方法是保留先前看到的时间戳和相关历元的单项缓存。只要我们的日志文件在一秒钟内有多行,这将是一场胜利。对于1000万行日志文件的情况,这种策略将昂贵的呼叫数量time.Parse()从10,000,000减少到86400 - 每个独立的秒钟一个。
373 |
374 | TODO:单项缓存的代码示例
375 |
376 | 我们可以做更多吗?因为我们确切知道时间戳的格式, 并且它们都在一天内完成,所以我们可以编写自定义时间解析逻辑,将其考虑在内。我们可以计算午夜的时代,然后从时间戳字符串中提取小时,分钟和秒 - 它们都将在字符串中处于固定偏移量 - 并执行一些整数运算。
377 |
378 | TODO:字符串偏移版本的代码示例
379 |
380 | 在我的基准测试中,这将解析时间从275ns / op减少到5ns / op。(当然,即使在275 ns / op下,你也更有可能在I / O上被阻塞,而不是在时间解析上被CPU阻塞。)
381 |
382 | 一般算法很慢,因为它必须处理更多的案例。你的算法可以更快,因为你更了解你的问题。但是代码与您需要的密切关系更紧密。如果时间格式发生变化,更新更加困难。
383 |
384 | 优化是专业化的,专用代码比通用代码更易于改变。
385 |
386 | 对于大多数情况,标准库实现需要“足够快”。如果你有更高的性能需求,你可能需要专门的实现。
387 |
388 | 定期进行配置文件以确保跟踪系统的性能特征,并准备随着流量变化重新优化。了解你的系统的极限,并有好的指标,让你预测什么时候你会达到这些限制。
389 |
390 | 当你的应用程序的使用发生更改时,不同的部分可能会成为热点。重温先前的优化并决定它们是否仍然值得,并在可能的情况下恢复为更易读的代码。我有一个系统,我使用一组复杂的mmap优化了启动时间,反映了不安全性。一旦我们改变了系统的部署方式,这个代码就不再需要了,我用更可读的常规文件操作取代了它。
391 |
392 | 优化工作流程摘要
393 | 所有优化都应遵循以下步骤:
394 |
395 | 1. 确定你的表现目标,并确认你没有达到他们的目标
396 | 1. 配置文件来识别要改进的区域。
397 | 1. 这可以是CPU,堆分配或goroutine阻塞。
398 | 1. 基准来确定您的解决方案使用内置基准测试框架提供的加速
399 | 1. 确保您在目标操作系统和体系结构上进行正确的基准测试。
400 | 1. 之后再次进行配置以验证问题已消失
401 | 1. 使用或来验证一组时间“充分”不同,以便优化值得添加代码复杂性。
402 | 1. 使用负载测试http服务(+其他花哨的:k6,fortio,...)
403 | 1. 确保你的延迟数字是有意义的
404 | 1. 第一步很重要。它会告诉您何时何地开始优化。更重要的是,它还会告诉你何时停止。几乎所有优化都会增加代码的复杂性以换取速度。而且你总是可以更快地编写代码。这是一个平衡的行为。
405 |
406 | ## 工具
407 |
408 | ### 介绍性分析
409 | 一般适用于源代码的技术
410 |
411 | 1. 介绍pprof
412 | * Go工具pprof(https://github.com/google/pprof)
413 | 1. 编写和运行(微)基准
414 | * 简介,将hot code提取到基准,优化基准,配置文件。
415 | * -cpuprofile/-memprofile/-benchmem
416 | * 0.5 ns/op意味着它被优化了 ->如何避免
417 | * 编写好的基准测试的技巧(删除不必要的工作,但增加基准)
418 | 1. 如何读取它的pprof输出
419 | 1. 显示的运行系统有哪些不同的部分
420 | 1. 宏观基准(生产剖析)
421 | * net/HTTP/pprof
422 | 1. 使用-base查看差异
423 | 1. 内存选项:-inuse_space,-inuse_objects,-alloc_space,-alloc_objects
424 | 1. 生产分析; localhost + ssh隧道,auth头文件,使用curl。
425 |
426 | ## 追踪
427 |
428 | * 一些更有趣的/先进的工具
429 | * /x/perf中的其他工具
430 | * perf(perf2pprof)
431 | * 英特尔vtune/amd codexl/instruments
432 | https://godoc.org/github.com/aclements/go-perf
433 |
434 | ## 垃圾收集
435 |
436 | 你不止一次支付内存分配。第一个显然是你分配它的时候。但是,每次垃圾收集运行时,你也要付出代价。
437 |
438 |
439 | 减少回收再利用。 - @bboreham
440 |
441 | * 堆栈与堆分配
442 | * 什么导致堆分配?
443 | * 了解逃逸分析(和当前的限制)
444 | * /debug/pprof/heap和-base
445 | * API设计限制分配:允许传入缓冲区,因此调用者可以重用而不是强制分配
446 | * 你甚至可以在扫描时仔细修改切片
447 | * 减少指针以减少gc扫描时间
448 | * 无指针的map键
449 | * GOGC
450 | * 缓冲区重用(sync.Pool vs或通过go-slab等自定义)
451 | * 切片与偏移量:当GC运行时指针写入需要writebarrier:https : //github.com/golang/go/commit/b85433975aedc2be2971093b6bbb0a7dc264c8fd
452 | * 使用错误变量而不是errors.New()/ fmt.Errorf()在呼叫站点(性能或风格?接口需要指针,所以它转义为堆)
453 | * 使用结构化的错误来减少分配(传递结构值),在错误打印时创建字符串
454 | * 大小端
455 |
456 |
457 |
458 | ## 运行时和编译器
459 | * 通过接口调用的成本(在CPU级别上的间接调用)
460 | * runtime.convT2E/runtime.convT2I
461 | * 类型断言与类型切换
462 | * 延缓
463 | * 用于整数,字符串的特殊映射实现
464 | * byte/uint16的映射未优化; 改用切片。
465 | * 你可以使用math.Float{32,64}{from,}bits优化float64-optimized ,但要注意浮动平等问题
466 | * https://github.com/dgryski/go-gk/blob/master/exact.go 据说快100倍; 需要基准测试
467 | * 边界检查消除
468 | * []字节<->字符串副本,Map优化
469 | * 双值的range将复制一个数组,使用sclice替代:
470 | *
471 | *
472 | * 尽可能使用字符串连接而不是fmt.Sprintf; 运行时为它已经优化了例程
473 |
474 | ## Unsafe
475 | * 它所有的危险项
476 | * unsafe的常见用途
477 | * mmap数据文件
478 | * 结构填充
479 | * 但并不总是足够快以证明复杂性/安全成本
480 | * 但是“off-heap”,所以被gc忽略(但是没有指针的slice)
481 | * 快速反序列化
482 | * string <-> slice 转换,[]byte <-> []uint32,...
483 | * int到bool是不安全的hack (但 != 0是可以的)
484 | * 填充:
485 | - https://dave.cheney.net/2015/10/09/padding-is-hard
486 | - http://www.catb.org/esr/structure-packing/#_go_and_rust
487 | - https://golang.org/ref/spec#Size_and_alignment_guarantees
488 | - https://github.com/dominikh/go-tools 结构布局,结构布局优化
489 | - 通过Offsetof对结构布局进行编码以发现unsafe和asm破损
490 |
491 | ## 与标准库共同陷阱
492 | * time.After()泄漏,直到它被触发
493 | * 重用HTTP连接...
494 | * rand.Int()和朋友是1)互斥体保护和2)创建昂贵
495 | * 考虑交替随机数生成(go-pcgr,xorshift)
496 | * binary.Read和binary.Write使用反射并且很慢; 手动做
497 | * 如果可能,请使用strconv而不是fmt
498 | * ....
499 |
500 | ## 替代实现
501 | * 标准库软件包的普遍替代品:
502 | * encoding/json -> ffjson
503 | * net/http -> fasthttp(但不兼容的API)
504 | * regexp -> ragel(或其他正则表达式包)
505 | * 序列化
506 | * encoding/gob - > https://github.com/alecthomas/go_serialization_benchmarks
507 | * protobuf - > https://github.com/gogo/protobuf
508 | * 所有格式都有权衡:选择一种符合你需要的编码空间,解码速度,语言/工具兼容性......
509 | * database/sql - > jackx/pgx,...
510 | * gccgo
511 | * container/list:使用切片(几乎总是)
512 |
513 | ## CGO
514 |
515 | - cgo调用的性能特征
516 | - 降低成本的技巧:配料
517 | - Go和C之间传递指针的规则
518 | - syso文件
519 |
520 | ## 高级技术
521 |
522 | 特定于运行代码的体系结构的技术
523 | * CPU缓存介绍
524 | * 性能的悬崖
525 | * 围绕缓存行构建直觉:大小,填充,对齐
526 | * 共享假
527 | * 真正的共享 ->分片
528 | * OS工具来查看缓存未命中
529 | * Mao与切片
530 | * SOA vs AOS布局
531 | * 减少指针追逐
532 | * 分支预测
533 | 从内部循环中删除分支:
534 | if a { for { } } else { for { } }
535 | 代替
536 | for { if a { } else { } }
537 |
538 | 避免
539 |
540 | if i % 2 == 0 {
541 | evens++
542 | } else {
543 | odds++
544 | }
545 |
546 | counts[i & 1] ++并不总是更快,但通常更难以阅读
547 | TODO:ASCII类计数示例和基准
548 | * 排序数据可以通过缓存局部性和分支预测来帮助提高性能,即使考虑到排序所花费的时间
549 | * 函数调用开销
550 | * 关于Jeff Dean的2002年数字(加上更新)的评论
551 | * cpus变得更快了,但是内存没有跟上
552 |
553 | ## 并发
554 | * 找出哪些部分可以并行完成,哪部分必须是顺序的
555 | * goroutines很便宜,但不免费。
556 | * 优化多线程代码
557 | * 共享false -> 填充缓存行大小
558 | * 共享true -> 分片
559 | * 与上一节关于缓存和虚假/真实共享重叠
560 | * 延迟同步; 它很贵,所以复制工作可能会更便宜
561 | * 你可以控制的东西:worker的数量,批量大小
562 |
563 | 你需要一个互斥体来保护共享的可变状态。如果你有很多的互斥量争用,你需要减少共享,或者减少mutable。减少共享的两种方法是1)分割锁或2)独立处理,然后合并。为了减少mutable:好吧,让你的数据结构是只读的。你还可以通过减少关键部分来缩短数据共享的时间 - 尽可能少地锁定锁定。有时候RWMutex就足够了,但是请注意,它们比较慢,但是它们允许多个读者进入。
564 |
565 | 如果你正在分解锁,请注意共享缓存行。您需要填充以避免缓存行拥有权在处理器之间弹跳。
566 |
567 | var stripe [8]struct{ sync.Mutex; _ [7]uint64 } //互斥量为64位; 填充填充缓存行的其余部分
568 |
569 | ## 部件
570 | * 关于为Go编写汇编代码
571 | * 编译器改进; bar很高
572 | * 尽可能少地替换以产生影响
573 | * 很好的理由:SIMD指令或者Go和编译器可以提供的其他东西
574 | * 非常重要的基准:改进可能是巨大的(高速公路的10倍)零(小点),或甚至更慢(不内联)
575 | * 用新版本重新标记以查看是否可以删除代码
576 | * TODO:链接到1.11补丁删除汇编代码
577 | * 总是有纯粹的Go版本(noasm build tag):测试,arm,gccgo
578 | * 简要介绍语法
579 | * 调用的约定
580 | * 使用不受asm支持的操作码
581 | * 关于为什么内联很难
582 | * 使这更容易工具:asmfmt,peachpy,c2goasm,...
583 |
584 | ## 优化整个服务
585 | 大多数情况下,你不会看到一个CPU限制的例程。这是一个简单的例子。如果你有优化服务,则需要查看整个系统。监测。指标。随着时间的推移记录很多事情,这样你可以看到它们变得更糟,所以你可以看到你的更改对生产的影响。
586 |
587 | tip.golang.org/doc/diagnostics.html
588 |
589 | * 系统设计参考:SRE Book,实用的分布式系统设计
590 | * 额外的工具:更多日志记录+分析
591 | * 两条基本规则:加速缓慢的事情或减少频率。
592 | * 分布式跟踪以追踪更高级别的瓶颈
593 | * 用于查询单个服务器而不是批量查询模式
594 | * 你的性能问题可能不是你的代码,但是你仍然需要解决它们
595 |
596 | ## 附录:实施研究论文
597 |
598 | 实施论文的提示:( algorithm另请参阅data structure)
599 |
600 | * 别。从明显的解决方案和合理的数据结构开始。
601 | “现代”算法往往具有较低的理论复杂性,但具有较高的常数因子和很多实施复杂性。其中一个经典例子是斐波那契堆。他们很难得到正确的,并有一个巨大的不变因素。已经发表了多篇论文,比较了不同工作负载下堆的实现方式,总体而言,4或8元的隐含堆总是排在前列。即使在Fibonacci堆应该更快(由于O(1)“减少键”)的情况下,使用Dijkstra的深度优先搜索算法的实验表明,当他们使用直接堆去除和加法时,它的速度更快。
602 |
603 | 类似地,对比更复杂的红黑或AVL树也可以找到对照或者跳过列表。在现代硬件上,“较慢”的算法可能足够快,甚至更快。
604 |
605 | > 最快的算法经常可以被几乎一样快速且容易理解的算法取代。
606 | >
607 | > 道格拉斯W.琼斯,爱荷华大学
608 |
609 | 增加的复杂性必须足以使得回报实际上值得。另一个例子是缓存驱逐算法。不同的算法可以具有高得多的复杂度,仅命中率的小改进。当然,您可能无法在测试之前进行测试,直到您有一个可行的实施并将其整合到您的程序中。
610 |
611 | 有时候这篇论文会有图表,但很像只发布正面结果的趋势,但这些倾向往往会偏向于表示新算法的优点。
612 |
613 | * 选择正确的纸张。
614 | * 寻找他们的算法声称击败和实施的论文。
615 |
616 | 通常,早期的论文会更容易理解,并且必须具有更简单的算法。
617 |
618 | 并非所有的文件都很好。
619 |
620 | 查看论文写入的上下文。确定有关硬件的假设:磁盘空间,内存使用情况等。一些较旧的论文在70年代或80年代进行了合理的不同折衷,但不一定适用于您的使用案例。例如,他们认为什么是“合理的”内存与磁盘使用的权衡。内存大小现在增加了数量级,并且SSD改变了使用磁盘的延迟惩罚。同样,一些流媒体算法是为路由器硬件而设计的,这可能会使转换成软件变得非常痛苦。
621 |
622 | 确保算法对数据保持的假设。
623 |
624 | 这将需要一些挖掘。你可能不想实现你找到的第一篇论文。
625 |
626 | * 确保你了解算法。这听起来很明显,但是否则无法进行调试。
627 |
628 |
629 |
630 | 一个良好的理解可能会让你从论文中提取关键的想法,并且可能将这个想法应用于你的问题,这可能比重新实现整个事情更简单。
631 |
632 | * 数据结构或算法的原始文件并不总是最好的。后来的论文可能会有更好的解释。
633 |
634 | * 一些论文发布了可以与之比较的参考源代码,但是
635 |
636 | 1) 学术代码几乎普遍可怕
637 | 2) 谨防许可限制(仅限“研究目的”)
638 | 3) 提防错误; 边缘情况,错误检查,性能等。
639 |
640 | 还要注意GitHub上的其他实现:它们可能与您的bug相同(或不同)。
641 |
642 | 有关此主题的其他资源:
643 |
644 | *
645 | *
646 |
--------------------------------------------------------------------------------
/performance.md:
--------------------------------------------------------------------------------
1 | # Writing and Optimizing Go code
2 |
3 | This document outlines best practices for writing high-performance Go code.
4 |
5 | While some discussions will be made for making individual services faster
6 | (caching, etc), designing performant distributed systems is beyond the scope
7 | of this work. There are already good texts on monitoring and distributed
8 | system design. Optimizing distributed systems encompasses an entirely different
9 | set of research and design trade-offs.
10 |
11 | All the content will be licensed under CC-BY-SA.
12 |
13 | This book is split into different sections:
14 |
15 | 1. Basic tips for writing not-slow software
16 | * CS 101-level stuff
17 | 1. Tips for writing fast software
18 | * Go-specific sections on how to get the best from Go
19 | 1. Advanced tips for writing *really* fast software
20 | * For when your optimized code isn't fast enough
21 |
22 | We can summarize these three sections as:
23 |
24 | 1. "Be reasonable"
25 | 1. "Be deliberate"
26 | 1. "Be dangerous"
27 |
28 | ## When and Where to Optimize
29 |
30 | I'm putting this first because it's really the most important step. Should
31 | you even be doing this at all?
32 |
33 | Every optimization has a cost. Generally, this cost is expressed in terms of
34 | code complexity or cognitive load -- optimized code is rarely simpler than
35 | the unoptimized version.
36 |
37 | But there's another side that I'll call the economics of optimization. As a
38 | programmer, your time is valuable. There's the opportunity cost of what else
39 | you could be working on for your project, which bugs to fix, which features
40 | to add. Optimizing things is fun, but it's not always the right task to
41 | choose. Performance is a feature, but so is shipping, and so is correctness.
42 |
43 | Choose the most important thing to work on. Sometimes it's not an actual
44 | CPU optimization, but a user-experience one. Something as simple as adding a
45 | progress bar, or making a page more responsive by doing computation in the
46 | background after rendering the page.
47 |
48 | Sometimes this will be obvious: an hourly report that completes in three hours
49 | is probably less useful than one that completes in less than one.
50 |
51 | Just because something is easy to optimize doesn't mean it's worth
52 | optimizing. Ignoring low-hanging fruit is a valid development strategy.
53 |
54 | Think of this as optimizing *your* time.
55 |
56 | You get to choose what to optimize and when to optimize. You can move the
57 | slider between "Fast Software" and "Fast Deployment"
58 |
59 | People hear and mindlessly repeat "premature optimization is the root of all
60 | evil", but they miss the full context of the quote.
61 |
62 | > "Programmers waste enormous amounts of time thinking about, or worrying about,
63 | the speed of noncritical parts of their programs, and these attempts at
64 | efficiency actually have a strong negative impact when debugging and
65 | maintenance are considered. We should forget about small efficiencies, say
66 | about 97% of the time: premature optimization is the root of all evil. Yet we
67 | should not pass up our opportunities in that critical 3%."
68 | >
69 | > -- Knuth
70 |
71 | Add: https://www.youtube.com/watch?time_continue=429&v=3WBaY61c9sE
72 | * don't ignore the easy optimizations
73 | * more knowledge of algorithms and data structures makes more optimizations "easy" or "obvious"
74 |
75 | Should you optimize?
76 | > Yes, but only if the problem is important, the program
77 | is genuinely too slow, and there is some expectation that it can be made
78 | faster while maintaining correctness, robustness, and clarity."
79 | >
80 | > -- The Practice of Programming, Kernighan and Pike
81 |
82 | Premature optimization can also hurt you by tying you into certain decisions.
83 | The optimized code can be harder to modify if requirements change and harder to
84 | throw away (sunk-cost fallacy) if needed.
85 |
86 | [BitFunnel performance estimation](http://bitfunnel.org/strangeloop) has some
87 | numbers that make this trade-off explicit. Imagine a hypothetical search
88 | engine needing 30,000 machines across multiple data centers. These machines
89 | have a cost of approximately $1,000 USD per year. If you can double the speed
90 | of the software, this can save the company $15M USD per year. Even a single
91 | developer spending an entire year to improve performance by only 1% will pay
92 | for itself.
93 |
94 | In the vast majority of cases, the size and speed of a program is not a concern.
95 | The easiest optimization is not having to do it. The second easiest optimization
96 | is just buying faster hardware.
97 |
98 | Once you've decided you're going to change your program, keep reading.
99 |
100 | ## How to Optimize
101 |
102 | ### Optimization Workflow
103 |
104 | Before we get into the specifics, let's talk about the general process of
105 | optimization.
106 |
107 | Optimization is a form of refactoring. But each step, rather than improving
108 | some aspect of the source code (code duplication, clarity, etc), improves
109 | some aspect of the performance: lower CPU, memory usage, latency, etc. This
110 | improvement generally comes at the cost of readability. This means that in
111 | addition to a comprehensive set of unit tests (to ensure your changes haven't
112 | broken anything), you also need a good set of benchmarks to ensure your
113 | changes are having the desired effect on performance. You must be able to
114 | verify that your change really *is* lowering CPU. Sometimes a change you
115 | thought would improve performance will actually turn out to have a zero or
116 | negative change. Always make sure you undo your fix in these cases.
117 |
118 | [What is the best comment in source code you have ever encountered? - Stack Overflow](https://stackoverflow.com/questions/184618/what-is-the-best-comment-in-source-code-you-have-ever-encountered):
119 |
120 | //
121 | // Dear maintainer:
122 | //
123 | // Once you are done trying to 'optimize' this routine,
124 | // and have realized what a terrible mistake that was,
125 | // please increment the following counter as a warning
126 | // to the next guy:
127 | //
128 | // total_hours_wasted_here = 42
129 | //
130 |
131 |
132 | The benchmarks you are using must be correct and provide reproducible numbers
133 | on representative workloads. If individual runs have too high a variance, it
134 | will make small improvements more difficult to spot. You will need to use
135 | [benchstat](https://golang.org/x/perf/benchstat) or equivalent statistical tests
136 | and won't be able just to eyeball it.
137 | (Note that using statistical tests is a good idea anyway.) The steps to run
138 | the benchmarks should be documented, and any custom scripts and tooling
139 | should be committed to the repository with instructions for how to run them.
140 | Be mindful of large benchmark suites that take a long time to run: it will
141 | make the development iterations slower.
142 |
143 | Note also that anything that can be measured can be optimized. Make sure
144 | you're measuring the right thing.
145 |
146 | The next step is to decide what you are optimizing for. If the goal is to
147 | improve CPU, what is an acceptable speed? Do you want to improve the current
148 | performance by 2x? 10x? Can you state it as "a problem of size N in less than
149 | time T"? Are you trying to reduce memory usage? By how much? How much slower
150 | is acceptable for what change in memory usage? What are you willing to give
151 | up in exchange for lower space requirements?
152 |
153 | Optimizing for service latency is a trickier proposition. Entire books have
154 | been written on how to performance test web servers. The primary issue is
155 | that for a single function, performance is fairly consistent for a
156 | given problem size. For webservices, you don't have a single number. A proper
157 | web-service benchmark suite will provide a latency distribution for a given
158 | reqs/second level. This talk gives a good overview of some of the issues:
159 | ["How NOT to Measure Latency" by Gil Tene](https://youtu.be/lJ8ydIuPFeU)
160 |
161 | TODO: See the later section on optimizing web services
162 |
163 | The performance goals must be specific. You will (almost) always be able to
164 | make something faster. Optimizing is frequently a game of diminishing returns.
165 | You need to know when to stop. How much effort are you going to put into
166 | getting the last little bit of work. How much uglier and harder to maintain
167 | are you willing to make the code?
168 |
169 | Dan Luu's previously mentioned talk on [BitFunnel performance
170 | estimation](http://bitfunnel.org/strangeloop) shows an example of using rough
171 | calculations to determine if your target performance figures are reasonable.
172 |
173 | Simon Eskildsen has a talk from SRECon covering this topic in more depth:
174 | [Advanced Napkin Math: Estimating System Performance from First Principles](https://www.youtube.com/watch?v=IxkSlnrRFqc)
175 |
176 | Finally, Jon Bentley's "Programming Pearls" has a chapter titled "The Back of
177 | the Envelope" covering Fermi problems. Sadly, these kinds of estimation skills
178 | got a bad wrap thanks to their use in Microsoft style "puzzle interview
179 | questions" in the 1990s and early 2000s.
180 |
181 | For greenfield development, you shouldn't leave all benchmarking and
182 | performance numbers until the end. It's easy to say "we'll fix it later", but
183 | if performance is really important it will be a design consideration from the
184 | start. Any significant architectural changes required to fix performance
185 | issues will be too risky near the deadline. Note that *during* development,
186 | the focus should be on reasonable program design, algorithms, and data
187 | structures. Optimizing at lower-levels of the stack should wait until later
188 | in the development cycle when a more complete view of the system performance
189 | is available. Any full-system profiles you do while the system is incomplete
190 | will give a skewed view of where the bottlenecks will be in the finished system.
191 |
192 | TODO: How to avoid/detect "Death by 1000 cuts" from poorly written software.
193 | Solution: "Premature pessimization is the root of all evil". This matches with
194 | my Rule 1: Be deliberate. You don't need to write every line of code
195 | to be fast, but neither should by default do wasteful things.
196 |
197 | > "Premature pessimization is when you write code that is slower than it needs to
198 | be, usually by asking for unnecessary extra work, when equivalently complex code
199 | would be faster and should just naturally flow out of your fingers."
200 | >
201 | > -- Herb Sutter
202 |
203 | Benchmarking as part of CI is hard due to noisy neighbours and even different
204 | CI boxes if it's just you. Hard to gate on performance metrics. A good middle
205 | ground is to have benchmarks run by the developer (on appropriate hardware) and
206 | included in the commit message for commits that specifically address
207 | performance. For those that are just general patches, try to catch performance
208 | degradations "by eye" in code review.
209 |
210 | TODO: how to track performance over time?
211 |
212 | Write code that you can benchmark. Profiling you can do on larger systems.
213 | Benchmarking you want to test isolated pieces. You need to be able to extract
214 | and setup sufficient context that benchmarks test enough and are
215 | representative.
216 |
217 | The difference between what your target is and the current performance will
218 | also give you an idea of where to start. If you need only a 10-20%
219 | performance improvement, you can probably get that with some implementation
220 | tweaks and smaller fixes. If you need a factor of 10x or more, then just
221 | replacing a multiplication with a left-shift isn't going to cut it. That's
222 | probably going to call for changes up and down your stack, possibly redesigning
223 | large portions of the system with these performance goals in mind.
224 |
225 | Good performance work requires knowledge at many different levels, from
226 | system design, networking, hardware (CPU, caches, storage), algorithms,
227 | tuning, and debugging. With limited time and resources, consider which level
228 | will give the most improvement: it won't always be an algorithm or program
229 | tuning.
230 |
231 | In general, optimizations should proceed from top to bottom. Optimizations at
232 | the system level will have more impact than expression-level ones. Make sure
233 | you're solving the problem at the appropriate level.
234 |
235 | This book is mostly going to talk about reducing CPU usage, reducing memory
236 | usage, and reducing latency. It's good to point out that you can very rarely
237 | do all three. Maybe CPU time is faster, but now your program uses more
238 | memory. Maybe you need to reduce memory space, but now the program will take
239 | longer.
240 |
241 | [Amdahl's Law](https://en.wikipedia.org/wiki/Amdahl%27s_law) tells us to focus
242 | on the bottlenecks. If you double the speed of routine that only takes 5% of
243 | the runtime, that's only a 2.5% speedup in total wall-clock. On the other hand,
244 | speeding up routine that takes 80% of the time by only 10% will improve runtime
245 | by almost 8%. Profiles will help identify where time is actually spent.
246 |
247 | When optimizing, you want to reduce the amount of work the CPU has to do.
248 | Quicksort is faster than bubble sort because it solves the same problem
249 | (sorting) in fewer steps. It's a more efficient algorithm. You've reduced the
250 | work the CPU needs to do in order to accomplish the same task.
251 |
252 | Program tuning, like compiler optimizations, will generally make only a small
253 | dent in the total runtime. Large wins will almost always come from an
254 | algorithmic change or data structure change, a fundamental shift in how your
255 | program is organized. Compiler technology improves, but slowly. [Proebsting's
256 | Law](http://proebsting.cs.arizona.edu/law.html) says compilers double in
257 | performance every 18 *years*, a stark contrast with the (slightly
258 | misunderstood interpretation) of Moore's Law that doubles processor
259 | performance every 18 *months*. Algorithmic improvements work at larger magnitudes.
260 | Algorithms for mixed integer programming [improved by a factor of 30,000
261 | between 1991 and 2008](https://agtb.wordpress.com/2010/12/23/progress-in-algorithms-beats-moore%E2%80%99s-law/).
262 | For a more concrete example, consider [this breakdown](https://medium.com/@buckhx/unwinding-uber-s-most-efficient-service-406413c5871d)
263 | of replacing a brute force geo-spatial algorithm described in an Uber blog post with
264 | more specialized one more suited to the presented task. There is no compiler switch
265 | that will give you an equivalent boost in performance.
266 |
267 | TODO: Optimizing floating point FFT and MMM algorithm differences in gttse07.pdf
268 |
269 | A profiler might show you that lots of time is spent in a particular routine.
270 | It could be this is an expensive routine, or it could be a cheap routine that
271 | is just called many many times. Rather than immediately trying to speed up
272 | that one routine, see if you can reduce the number of times it's called or
273 | eliminate it completely. We'll discuss more concrete optimization strategies
274 | in the next section.
275 |
276 | The Three Optimization Questions:
277 |
278 | * Do we have to do this at all? The fastest code is the code that's never run.
279 | * If yes, is this the best algorithm.
280 | * If yes, is this the best *implementation* of this algorithm.
281 |
282 | ## Concrete optimization tips
283 |
284 | Jon Bentley's 1982 work "Writing Efficient Programs" approached program
285 | optimization as an engineering problem: Benchmark. Analyze. Improve. Verify.
286 | Iterate. A number of his tips are now done automatically by compilers. A
287 | programmer's job is to use the transformations compilers *can't* do.
288 |
289 | There are summaries of the book:
290 |
291 | *
292 | *
293 |
294 | and the program tuning rules:
295 |
296 | *
297 |
298 | When thinking of changes you can make to your program, there are two basic options:
299 | you can either change your data or you can change your code.
300 |
301 | ### Data Changes
302 |
303 | Changing your data means either adding to or altering the representation of
304 | the data you're processing. From a performance perspective, some of these
305 | will end up changing the O() associated with different aspects of the data
306 | structure. This may even include preprocessing the input to be in a
307 | different, more useful format.
308 |
309 | Ideas for augmenting your data structure:
310 |
311 | * Extra fields
312 |
313 | The classic example of this is storing the length of a linked list in a field in
314 | the root node. It takes a bit more work to keep it updated, but then querying
315 | the length becomes a simple field lookup instead of an O(n) traversal. Your data
316 | structure might present a similar win: a bit of bookkeeping during some
317 | operations in exchange for some faster performance on a common use case.
318 |
319 | Similarly, storing pointers to frequently needed nodes instead of performing
320 | additional searches. This covers things like the "backwards" links in a
321 | doubly-linked list to make node removal O(1). Some skip lists keep a "search
322 | finger", where you store a pointer to where you just were in your data
323 | structure on the assumption it's a good starting point for your next
324 | operation.
325 |
326 | * Extra search indexes
327 |
328 | Most data structures are designed for a single type of query. If you need two
329 | different query types, having an additional "view" onto your data can be large
330 | improvement. For example, a set of structs might have a primary ID (integer)
331 | that you use to look up in a slice, but sometimes need to look up with a
332 | secondary ID (string). Instead of iterating over the slice, you can augment
333 | your data structure with a map either from string to ID or directly to the
334 | struct itself.
335 |
336 | * Extra information about elements
337 |
338 | For example, keeping a bloom filter of all the elements you've inserted can let
339 | you quickly return "no match" for lookups. These need to be small and fast to
340 | not overwhelm the rest of the data structure. (If a lookup in your main data
341 | structure is cheap, the cost of the bloom filter will outweigh any savings.)
342 |
343 | * If queries are expensive, add a cache.
344 |
345 | At a larger level, an in-process or external cache (like memcache) can help.
346 | It might be excessive for a single data structure. We'll cover more about
347 | caches below.
348 |
349 | These sorts of changes are useful when the data you need is cheap to store and
350 | easy to keep up-to-date.
351 |
352 | These are all clear examples of "do less work" at the data structure level.
353 | They all cost space. Most of the time if you're optimizing for CPU, your
354 | program will use more memory. This is the classic [space-time trade-off](https://en.wikipedia.org/wiki/Space%E2%80%93time_tradeoff).
355 |
356 | It's important to examine how this tradeoff can affect your solutions -- it's
357 | not always straight-forward. Sometimes a small amount of memory can give a
358 | significant speed, sometimes the tradeoff is linear (2x memory usage == 2x
359 | performance speedup), sometimes it's significantly worse: a huge amount of
360 | memory gives only a small speedup. Where you need to be on this
361 | memory/performance curve can affect what algorithm choices are reasonable.
362 | It's not always possible to just tune an algorithm parameter. Different
363 | memory usages might be completely different algorithmic approaches.
364 |
365 | Lookup tables also fall into this space-time trade-off. A simple lookup table
366 | might just be a cache of previously requested computations.
367 |
368 | If the domain is small enough, the *entire* set of results could be
369 | precomputed and stored in the table. As an example, this could be the
370 | approach taken for a fast popcount implementation, where by the number of set
371 | bits in byte is stored in a 256-entry table. A larger table could store the
372 | bits required for all 16-bit words. In this case, they're storing exact
373 | results.
374 |
375 | A number of algorithms for trigonometric functions use lookup tables as a
376 | starting point for a calculation.
377 |
378 | If your program uses too much memory, it's also possible to go the other way.
379 | Reduce space usage in exchange for increased computation. Rather than storing
380 | things, calculate them every time. You can also compress the data in memory
381 | and decompress it on the fly when you need it.
382 |
383 | If the data you're processing is on disk, instead of loading everything into
384 | RAM, you could create an index for the pieces you need and keep that in
385 | memory, or pre-process the file into smaller workable chunks.
386 |
387 | [Small Memory Software](https://smallmemory.charlesweir.com/book.html) is a book available
388 | online covering techniques for reducing the space used by your programs.
389 | While it was originally written targeting embedded developers, the ideas are
390 | applicable for programs on modern hardware dealing with huge amounts of data.
391 |
392 | * Rearrange your data
393 |
394 | Eliminate structure padding. Remove extra fields. Use a smaller data type.
395 |
396 | * Change to a slower data structure
397 |
398 | Simpler data structures frequently have lower memory requirements. For
399 | example, moving from a pointer-heavy tree structure to use slice and
400 | linear search instead.
401 |
402 | * Custom compression format for your data
403 |
404 | Compression algorithms depend very heavily on what is being compressed. It's
405 | best to choose one that suites your data. If you have []byte, the something
406 | like snappy, gzip, lz4, behaves well. For floating point data there is go-tsz
407 | for time series and fpc for scientific data. Lots of research has been done
408 | around compressing integers, generally for information retrieval in search
409 | engines. Examples include delta encoding and varints to more complex schemes
410 | involving Huffman encoded xor-differences. You can also come up with custom
411 | compression formats optimized for exactly your data.
412 |
413 | Do you need to inspect the data or can it stay compressed? Do you need random
414 | access or only streaming? If you need access to individual entries but don't
415 | want to decompress the entire thing, you can compress the data in smaller
416 | blocks and keep an index indicating what range of entries are in each block.
417 | Access to a single entry just needs to check the index and unpack the smaller
418 | data block.
419 |
420 | If your data is not just in-process but will be written to disk, what about
421 | data migration or adding/removing fields. You'll now be dealing with raw
422 | []byte instead of nice structured Go types, so you'll need unsafe and to
423 | consider serialization options.
424 |
425 | We will talk more about data layouts later.
426 |
427 | Modern computers and the memory hierarchy make the space/time trade-off less
428 | clear. It's very easy for lookup tables to be "far away" in memory (and
429 | therefore expensive to access) making it faster to just recompute a value
430 | every time it's needed.
431 |
432 | This also means that benchmarking will frequently show improvements that are
433 | not realized in the production system due to cache contention (e.g., lookup
434 | tables are in the processor cache during benchmarking but always flushed by
435 | "real data" when used in a real system.
436 | Google's [Jump Hash paper](https://arxiv.org/pdf/1406.2294.pdf) in fact
437 | addressed this directly, comparing performance on both a contended and
438 | uncontended processor cache. (See graphs 4 and 5 in the Jump Hash paper)
439 |
440 | TODO: how to simulate a contended cache, show incremental cost
441 | TODO: sync.Map as a Go-ish example of cache-contention addressing
442 |
443 | Another aspect to consider is data-transfer time. Generally network and disk
444 | access is very slow, and so being able to load a compressed chunk will be
445 | much faster than the extra CPU time required to decompress the data once it
446 | has been fetched. As always, benchmark. A binary format will generally
447 | be smaller and faster to parse than a text one, but at the cost of no longer
448 | being as human readable.
449 |
450 | For data transfer, move to a less chatty protocol, or augment the API to
451 | allow partial queries. For example, an incremental query rather than being
452 | forced to fetch the entire dataset each time.
453 |
454 | ### Algorithmic Changes
455 |
456 | If you're not changing the data, the other main option is to change the code.
457 |
458 | The biggest improvement is likely to come from an algorithmic change. This
459 | is the equivalent of replacing bubble sort (`O(n^2)`) with quicksort (`O(n log n)`)
460 | or replacing a linear scan through an array (`O(n)`) with a binary search (`O(log n)`)
461 | or a map lookup (`O(1)`).
462 |
463 | This is how software becomes slow. Structures originally designed for one use
464 | is repurposed for something it wasn't designed for. This happens gradually.
465 |
466 | It's important to have an intuitive grasp of the different big-O levels.
467 | Choose the right data structure for your problem. You don't have to always
468 | shave cycles, but this just prevents dumb performance issues that might not
469 | be noticed until much later.
470 |
471 | The basic classes of complexity are:
472 |
473 | * O(1): a field access, array or map lookup
474 |
475 | Advice: don't worry about it (but keep in mind the constant factor.)
476 |
477 | * O(log n): binary search
478 |
479 | Advice: only a problem if it's in a loop
480 |
481 | * O(n): simple loop
482 |
483 | Advice: you're doing this all the time
484 |
485 | * O(n log n): divide-and-conquer, sorting
486 |
487 | Advice: still fairly fast
488 |
489 | * O(n\*m): nested loop / quadratic
490 |
491 | Advice: be careful and constrain your set sizes
492 |
493 | * Anything else between quadratic and subexponential
494 |
495 | Advice: don't run this on a million rows
496 |
497 | * O(b ^ n), O(n!): exponential and up
498 |
499 | Advice: good luck if you have more than a dozen or two data points
500 |
501 | Link:
502 |
503 | Let's say you need to search through of an unsorted set of data. "I should
504 | use a binary search" you think, knowing that a binary search is O(log n) which
505 | is faster than the O(n) linear scan. However, a binary search requires that
506 | the data is sorted, which means you'll need to sort it first, which will take
507 | O(n log n) time. If you're doing lots of searches, then the upfront cost of
508 | sorting will pay off. On the other hand, if you're mostly doing lookups,
509 | maybe having an array was the wrong choice and you'd be better off paying the
510 | O(1) lookup cost for a map instead.
511 |
512 | Being able to analyze your problem in terms of big-O notation also means you can
513 | figure out if you're already at the limit for what is possible for your problem,
514 | and if you need to change approaches in order to speed things up. For example,
515 | finding the minimum of an unsorted list is `O(n)`, because you have to look at
516 | every single item. There's no way to make that faster.
517 |
518 | If your data structure is static, then you can generally do much better than
519 | the dynamic case. It becomes easier to build an optimal data structure
520 | customized for exactly your lookup patterns. Solutions like minimal perfect
521 | hashing can make sense here, or precomputed bloom filters. This also make
522 | sense if your data structure is "static" for long enough and you can amortize
523 | the up-front cost of construction across many lookups.
524 |
525 | Choose the simplest reasonable data structure and move on. This is CS 101 for
526 | writing "not-slow software". This should be your default development
527 | mode. If you know you need random access, don't choose a linked-list.
528 | If you know you need in-order traversal, don't use a map.
529 | Requirements change and you can't always guess the future. Make a reasonable
530 | guess at the workload.
531 |
532 |
533 |
534 | Data structures for similar problems will differ in when they do a piece of
535 | work. A binary tree sorts a little at a time as inserts happen. A unsorted
536 | array is faster to insert but it's unsorted: at the end to "finalize" you
537 | need to do the sorting all at once.
538 |
539 | When writing a package to be used by others, avoid the temptation to
540 | optimize upfront for every single use case. This will result in unreadable
541 | code. Data structures by design are effectively single-purpose. You can
542 | neither read minds nor predict the future. If a user says "Your package is
543 | too slow for this use case", a reasonable answer might be "Then use this
544 | other package over here". A package should "do one thing well".
545 |
546 | Sometimes hybrid data structures will provide the performance improvement you
547 | need. For example, by bucketing your data you can limit your search to a
548 | single bucket. This still pays the theoretical cost of O(n), but the constant
549 | will be smaller. We'll revisit these kinds of tweaks when we get to program
550 | tuning.
551 |
552 | Two things that people forget when discussion big-O notation:
553 |
554 | One, there's a constant factor involved. Two algorithms which have the same
555 | algorithmic complexity can have different constant factors. Imagine looping
556 | over a list 100 times vs just looping over it once. Even though both are O(n),
557 | one has a constant factor that's 100 times higher.
558 |
559 | These constant factors are why even though merge sort, quicksort, and
560 | heapsort all O(n log n), everybody uses quicksort because it's the fastest.
561 | It has the smallest constant factor.
562 |
563 | The second thing is that big-O only says "as n grows to infinity". It talks
564 | about the growth trend, "As the numbers get big, this is the growth factor
565 | that will dominate the run time." It says nothing about the actual
566 | performance, or how it behaves with small n.
567 |
568 | There's frequently a cut-off point below which a dumber algorithm is faster.
569 | A nice example from the Go standard library's `sort` package. Most of the
570 | time it's using quicksort, but it has a shell-sort pass then insertion sort
571 | when the partition size drops below 12 elements.
572 |
573 | For some algorithms, the constant factor might be so large that this cut-off
574 | point may be larger than all reasonable inputs. That is, the O(n^2) algorithm
575 | is faster than the O(n) algorithm for all inputs that you're ever likely to
576 | deal with.
577 |
578 | This also means you need to know representative input sizes, both for
579 | choosing the most appropriate algorithm and for writing good benchmarks.
580 | 10 items? 1000 items? 1000000 items?
581 |
582 | This also goes the other way: For example, choosing to use a more complicated
583 | data structure to give you O(n) scaling instead of O(n^2), even though the
584 | benchmarks for small inputs got slower. This also applies to most lock-free
585 | data structures. They're generally slower in the single-threaded case but
586 | more scalable when many threads are using it.
587 |
588 | The memory hierarchy in modern computers confuses the issue here a little
589 | bit, in that caches prefer the predictable access of scanning a slice to the
590 | effectively random access of chasing a pointer. Still, it's best to begin
591 | with a good algorithm. We will talk about this in the hardware-specific
592 | section.
593 |
594 | TODO: extending last paragraph, mention O() notation is an model where each
595 | operation has fixed cost. That's a wrong assumption on modern hardware.
596 |
597 | > The fight may not always go to the strongest, nor the race to the fastest,
598 | but that's the way to bet.
599 | > -- Rudyard Kipling
600 |
601 | Sometimes the best algorithm for a particular problem is not a single
602 | algorithm, but a collection of algorithms specialized for slightly different
603 | input classes. This "polyalgorithm" quickly detects what kind of input it
604 | needs to deal with and then dispatches to the appropriate code path. This is
605 | what the sorting package mentioned above does: determine the problem size and
606 | choose a different algorithm. In addition to combining quicksort, shell sort,
607 | and insertion sort, it also tracks recursion depth of quicksort and calls
608 | heapsort if necessary. The `string` and `bytes` packages do something similar,
609 | detecting and specializing for different cases. As with data compression, the
610 | more you know about what your input looks like, the better your custom
611 | solution can be. Even if an optimization is not always applicable,
612 | complicating your code by determining that it's safe to use and executing
613 | different logic can be worth it.
614 |
615 | This also applies to subproblems your algorithm needs to solve. For example,
616 | being able to use radix sort can have a significant impact on performance, or
617 | using quickselect if you only need a partial sort.
618 |
619 | Sometimes rather than specialization for your particular task, the best
620 | approach is to abstract it into a more general problem space that has been
621 | well-studied by researchers. Then you can apply the more general solution to
622 | your specific problem. Mapping your problem into a domain that already has
623 | well-researched implementations can be a significant win.
624 |
625 | Similarly, using a simpler algorithm means that tradeoffs, analysis, and
626 | implementation details are more likely to be more studied and well understood
627 | than more esoteric or exotic and complex ones.
628 |
629 | Simpler algorithms can also be faster. These two examples are not isolated cases
630 | https://go-review.googlesource.com/c/crypto/+/169037
631 | https://go-review.googlesource.com/c/go/+/170322/
632 |
633 | TODO: notes on algorithm selection
634 |
635 | TODO:
636 | improve worst-case behaviour at slight cost to average runtime
637 | linear-time regexp matching
638 |
639 | While most algorithms are deterministic, there are a class of algorithms that
640 | use randomness as a way to simplify otherwise complex decision making step.
641 | Instead of having code that does the Right Thing, you use randomness to
642 | select a probably not *bad* thing. For example, a treap is a
643 | probabilistically balanced binary tree. Each node has a key, but also is
644 | assigned a random value. When inserting into the tree, the normal binary tree
645 | insertion path is followed but the nodes also obey the heap property based
646 | on each nodes randomly assigned weight. This simpler approach replaces
647 | otherwise complicated tree rotating solutions (like AVL and Red Black trees)
648 | but still maintains a balanced tree with O(log n) insert/lookup "with high
649 | probability. Skip lists are another similar, simple data structure that uses
650 | randomness to produce "probably" O(log n) insertion and lookups.
651 |
652 | Similarly, choosing a random pivot for quicksort can be simpler than a more
653 | complex median-of-medians approach to finding a good pivot, and the
654 | probability that bad pivots are continually (randomly) chosen and degrading
655 | quicksort's performance to O(n^2) is vanishingly small.
656 |
657 | Randomized algorithms are classed as either "Monte Carlo" algorithms or "Las
658 | Vegas" algorithms, after two well known gambling locations. A Monte Carlo
659 | algorithm gambles with correctness: it might output a wrong answer (or in the
660 | case of the above, an unbalanced binary tree). A Las Vegas algorithm always
661 | outputs a correct answer, but might take a very long time to terminate.
662 |
663 | Another well-known example of a randomized algorithm is the Miller-Rabin
664 | primality testing algorithm. Each iteration will output either "not prime" or
665 | "maybe prime". While "not prime" is certain, the "maybe prime" is correct
666 | with probability at least 1/2. That is, there are non-primes for which "maybe
667 | prime" will still be output. By running many iterations of Miller-Rabin, we
668 | can make the probability of failure (that is, outputing "maybe prime" for a
669 | composite number) as small as we'd like. If it passes 200 iterations, then we
670 | can say the number is composite with probability at most 1/(2^200).
671 |
672 | Another area where randomness plays a part is called "The power of two random
673 | choices". While initially the research was applied to load balancing, it
674 | turned out to be widely applicable to a number of selection problems. The
675 | idea is that rather than trying to find the best selection out of a group of
676 | items, pick two at random and select the best from that. Returning to load
677 | balancing (or hash table chains), the power of two random choices reduces the
678 | expected load (or hash chain length) from O(log n) items to O(log log n)
679 | items. For more information, see [The Power of Two Random Choices: A Survey of Techniques and Results](https://www.eecs.harvard.edu/~michaelm/postscripts/handbook2001.pdf)
680 |
681 | randomized algorithms:
682 | other caching algorithms
683 | statistical approximations (frequently depend on sample size and not population size)
684 |
685 | TODO: batching to reduce overhead: https://lemire.me/blog/2018/04/17/iterating-in-batches-over-data-structures-can-be-much-faster/
686 |
687 | TODO: - Algorithm Design Manual: http://algorist.com/algorist.html
688 | - How To Solve It By Computer
689 | - to what extent is this a "how to write algorithms" book? If you're going to change
690 | the code to speed it up, by definition you're writing new algorithms. Soo... maybe?
691 |
692 | ### Benchmark Inputs
693 |
694 | Real-world inputs rarely match the theoretical "worst case". Benchmarking is
695 | vital to understanding how your system behaves in production.
696 |
697 | You need to know what class of inputs your system will be seeing once deployed,
698 | and your benchmarks must use instances pulled from that same distribution. As
699 | we've seen, different algorithms make sense at different input sizes. If your
700 | expected input range is <100, then your benchmarks should reflect that.
701 | Otherwise, choosing an algorithm which is optimal for n=10^6 might not be the
702 | fastest.
703 |
704 | Be able to generate representative test data. Different distributions of data
705 | can provoke different behaviours in your algorithm: think of the classic
706 | "quicksort is O(n^2) when the data is sorted" example. Similarly,
707 | interpolation search is O(log log n) for uniform random data, but O(n) worst
708 | case. Knowing what your inputs look like is the key to both representative
709 | benchmarks and for choosing the best algorithm. If the data you're using to
710 | test isn't representative of real workloads, you can easily end up optimizing
711 | for one particular data set, "overfitting" your code to work best with one
712 | specific set of inputs.
713 |
714 | This also means your benchmark data needs to be representative of the real
715 | world. Using purely randomized inputs may skew the behaviour of your algorithm.
716 | Caching and compression algorithms both exploit skewed distributions not present
717 | in random data and so will perform worse, while a binary tree will perform
718 | better with random values as they will tend to keep the tree balanced. (This is
719 | the idea behind a treap, by the way.)
720 |
721 | On the other hand, consider the case of testing a system with a cache. If your
722 | benchmark input consists only a single query, then every request will hit the
723 | cache giving potentially a very unrealistic view of how the system will behave
724 | in the real world with a more varied request pattern.
725 |
726 | Also, note that some issues that are not apparent on your laptop might be visible
727 | once you deploy to production and are hitting 250k reqs/second on a 40 core
728 | server. Similarly, the behaviour of the garbage collector during benchmarking
729 | can misrepresent real-world impact. There are (rare) cases where a
730 | microbenchmark will show a slow-down, but real-world performance improves.
731 | Microbenchmarks can help nudge you in the right direction but being able to
732 | fully test the impact of a change across the entire system is best.
733 |
734 | Writing good benchmarks can be difficult.
735 |
736 | *
737 |
738 | Use geometric mean to compare groups of benchmarks.
739 |
740 | *
741 |
742 | Evaluating Benchmark Accuracy:
743 |
744 | *
745 |
746 | ### Program Tuning
747 |
748 | Program tuning used to be an art form, but then compilers got better. So now
749 | it turns out that compilers can optimize straight-forward code better than
750 | complicated code. The Go compiler still has a long way to go to match gcc and
751 | clang, but it does mean that you need to be careful when tuning and
752 | especially when upgrading Go versions that your code doesn't become "worse".
753 | There are definitely cases where tweaks to work around the lack of a particular
754 | compiler optimization became slower once the compiler was improved.
755 |
756 | My RC6 cipher implementation had a 10% speed up for the inner loop just by
757 | switching to `encoding/binary` and `math/bits` instead of my hand-rolled
758 | versions.
759 |
760 | Similarly, the `compress/bzip2` package was sped by switching to [simpler
761 | code the compiler was better able to
762 | optimize](https://github.com/golang/go/commit/9eb219480e8de08d380ee052b7bff293856955f8)
763 |
764 | If you are working around a specific runtime or compiler code generation
765 | issue, always document your change with a link to the upstream issue. This
766 | will allow you to quickly revisit your optimization once the bug is fixed.
767 |
768 | Fight the temptation to cargo cult folklore-based "performance tips", or even
769 | over-generalize from your own experience. Each performance bug needs to be
770 | approached on its own merits. Even if something has worked previously, make
771 | sure to profile to ensure the fix is still applicable. Your previous
772 | work can guide you, but don't apply previous optimizations blindly.
773 |
774 | Program tuning is an iterative process. Keep revisiting your code and seeing
775 | what changes can be made. Ensure you're making progress at each step.
776 | Frequently one improvement will enable others to be made. (Now that I'm not
777 | doing A, I can simplify B by doing C instead.) This means you need to keep
778 | looking at the entire picture and not get too obsessed with one small set of
779 | lines.
780 |
781 | Once you've settled on the right algorithm, program tuning is the process of
782 | improving the implementation of that algorithm. In Big-O notation, this is
783 | the process of reducing the constants associated with your program.
784 |
785 | All program tuning is either making a slow thing fast, or doing a slow thing
786 | fewer times. Algorithmic changes also fall into these categories, but we're
787 | going to be looking at smaller changes. Exactly how you do this varies as
788 | technologies change.
789 |
790 | Making a slow thing fast might be replacing SHA1 or `hash/fnv1` with a faster
791 | hash function. Doing a slow thing fewer times might be saving the result of
792 | the hash calculation of a large file so you don't have to do it multiple
793 | times.
794 |
795 | Keep comments. If something doesn't need to be done, explain why. Frequently
796 | when optimizing an algorithm you'll discover steps that don't need to be
797 | performed under some circumstances. Document them. Somebody else might think
798 | it's a bug and needs to be put back.
799 |
800 | > Empty programs gives the wrong answer in no time at all.
801 | >
802 | > It's easy to be fast if you don't have to be correct.
803 |
804 | "Correctness" can depend on the problem. Heuristic algorithms that are
805 | mostly-right most of the time can be fast, as can algorithms which guess and
806 | improve allowing you to stop when you hit an acceptable limit.
807 |
808 | Cache common cases:
809 |
810 | We're all familiar with memcache, but there are also in-process caches. Using
811 | an in-process cache saves the cost of both the network call and the cost of
812 | serialization. On the other hand, this increases GC pressure as there is more
813 | memory to keep track of. You also need to consider eviction strategies, cache
814 | invalidation, and thread-safety. An external cache will generally handle
815 | eviction for you, but cache invalidation remains a problem. Thread-safety can
816 | also be an issue with external caches as it becomes effectively shared mutable
817 | state either between different goroutines in the same service or even different
818 | service instances if the external cache is shared.
819 |
820 | A cache saves information you've just spent time computing in the hopes that
821 | you'll be able to reuse it again soon and save the computation time. A cache
822 | doesn't need to be complex. Even storing a single item -- the most recently
823 | seen query/response -- can be a big win, as seen in the `time.Parse()` example
824 | below.
825 |
826 | With caches it's important to compare the cost (in terms of actual wall-clock
827 | and code complexity) of your caching logic to simply refetching or recomputing
828 | the data. The more complex algorithms that give higher hit rates are generally
829 | not cheap themselves. Randomized cache eviction is simple and fast and can be
830 | effective in many cases. Similarly, randomized cache *insertion* can limit your
831 | cache to only popular items with minimal logic. While these may not be as effective
832 | as the more complex algorithms, the big improvement will be adding a cache in the first
833 | place: choosing exactly which caching algorithm gives only minor improvements.
834 |
835 | It's important to benchmark your choice of cache eviction algorithm with
836 | real-world traces. If in the real world repeated requests are sufficiently rare,
837 | it can be more expensive to keep cached responses around than to simply
838 | recompute them when needed. I've had services where testing with production data
839 | showed even an optimal cache wasn't worth it. we simply did't have sufficient
840 | repeated requests to make the added complexity of a cache make sense.
841 |
842 | Your expected cache hit ratio is important. You'll want to export the ratio to
843 | your monitoring stack. Changing ratios will show a shift in traffic. Then it's
844 | time to revisit the cache size or the expiration policy.
845 |
846 | A large cache can increase GC pressure. At the extreme (little or no eviction,
847 | caching all requests to an expensive function) this can turn into
848 | [memoization](https://en.wikipedia.org/wiki/Memoization)
849 |
850 | Program tuning:
851 |
852 | Program tuning is the art of iteratively improving a program in small steps.
853 | Egon Elbre lays out his procedure:
854 |
855 | * Come up with a hypothesis as to why your program is slow.
856 | * Come up with N solutions to solve it
857 | * Try them all and keep the fastest.
858 | * Keep the second fastest just in case.
859 | * Repeat.
860 |
861 | Tunings can take many forms.
862 |
863 | * If possible, keep the old implementation around for testing.
864 | * If not possible, generate sufficient golden test cases to compare output to.
865 | * "Sufficient" means including edge cases, as those are the ones likely to get
866 | affected by tuning as you aim to improve performance in the general case.
867 | * Exploit a mathematical identity:
868 | * Note that implementing and optimizing numerical calculations is almost its own field
869 | *
870 | *
871 | * multiplication with addition
872 | * use WolframAlpha, Maxima, sympy and similar to specialize, optimize or create lookup-tables
873 | * (Also, https://users.ece.cmu.edu/~franzf/papers/gttse07.pdf)
874 | * moving from floating point math to integer math
875 | * or mandelbrot removing sqrt, or lttb removing abs, `a < b/c` => `a * c < b`
876 | * consider different number representations: fixed-point, floating-point, (smaller) integers,
877 | * fancier: integers with error accumulators (e.g. Bresenham's line and circle), multi-base numbers / redundant number systems
878 | * "pay only for what you use, not what you could have used"
879 | * zero only part of an array, rather than the whole thing
880 | * best done in tiny steps, a few statements at a time
881 | * cheap checks before more expensive checks:
882 | * e.g., strcmp before regexp, (q.v., bloom filter before query)
883 | "do expensive things fewer times"
884 | * common cases before rare cases
885 | i.e., avoid extra tests that always fail
886 | * unrolling still effective: https://play.golang.org/p/6tnySwNxG6O
887 | * code size. vs branch test overhead
888 | * using offsets instead of slice assignment can help with bounds checks, data dependencies, and code gen (less to copy in inner loop).
889 | * remove bounds checks and nil checks from loops: https://go-review.googlesource.com/c/go/+/151158
890 | * other tricks for the prove pass
891 | * this is where pieces of Hacker's Delight fall
892 |
893 | Many folklore performance tips for tuning rely on poorly optimizing compilers
894 | and encourage the programmer to do these transformations by hand. Compilers
895 | have been using shifts instead of multiplying or dividing by a power of two
896 | for 15 years now -- nobody should be doing that by hand. Similarly, hoisting
897 | invariant calculations out of loops, basic loop unrolling, common
898 | sub-expression elimination and many others are all done automatically by gcc
899 | and clang and the like. Go's compiler does many of these and continues to
900 | improve. As always, benchmark before committing to the new version.
901 |
902 | The transformations the compiler can't do rely on you knowing things about
903 | the algorithm, about your input data, about invariants in your system, and
904 | other assumptions you can make, and factoring that implicit knowledge into
905 | removing or altering steps in the data structure.
906 |
907 | Every optimization codifies an assumption about your data. These *must* be
908 | documented and, even better, tested for. These assumptions are going to be
909 | where your program crashes, slows down, or starts returning incorrect data
910 | as the system evolves.
911 |
912 | Program tuning improvements are cumulative. 5x 3% improvements is a 15%
913 | improvement. When making optimizations, it's worth it to think about the
914 | expected performance improvement. Replacing a hash function with a faster one
915 | is a constant factor improvement.
916 |
917 | Understanding your requirements and where they can be altered can lead to
918 | performance improvements. One issue that was presented in the \#performance
919 | Gophers Slack channel was the amount of time that was spent creating a unique
920 | identifier for a map of string key/value pairs. The original solution was to
921 | extract the keys, sort them, and pass the resulting string to a hash
922 | function. The improved solution we came up was to individually hash the
923 | keys/values as they were added to the map, then xor all these hashes together
924 | to create the identifier.
925 |
926 | Here's an example of specialization.
927 |
928 | Let's say we're processing a massive log file for a single day, and each line
929 | begins with a time stamp.
930 |
931 | ```
932 | Sun 4 Mar 2018 14:35:09 PST <...........................>
933 | ```
934 |
935 | For each line, we're going to call `time.Parse()` to turn it into a epoch. If
936 | profiling shows us `time.Parse()` is the bottleneck, we have a few options to
937 | speed things up.
938 |
939 | The easiest is to keep a single-item cache of the previously seen time stamp
940 | and the associated epoch. As long as our log file has multiple lines for a single
941 | second, this will be a win. For the case of a 10 million line log file,
942 | this strategy reduces the number of expensive calls to `time.Parse()` from
943 | 10,000,000 to 86400 -- one for each unique second.
944 |
945 | TODO: code example for single-item cache
946 |
947 | Can we do more? Because we know exactly what format the timestamps are in
948 | *and* that they all fall in a single day, we can write custom time parsing
949 | logic that takes this into account. We can calculate the epoch for midnight,
950 | then extract hour, minute, and second from the timestamp string -- they'll
951 | all be in fixed offsets in the string -- and do some integer math.
952 |
953 | TODO: code example for string offset version
954 |
955 | In my benchmarks, this reduced the time parsing from 275ns/op to 5ns/op.
956 | (Of course, even at 275 ns/op, you're more likely to be blocked on I/O and
957 | not CPU for time parsing.)
958 |
959 | The general algorithm is slow because it has to handle more cases. Your
960 | algorithm can be faster because you know more about your problem. But the
961 | code is more closely tied to exactly what you need. It's much more difficult
962 | to update if the time format changes.
963 |
964 | Optimization is specialization, and specialized code is more fragile to
965 | change than general purpose code.
966 |
967 | The standard library implementations need to be "fast enough" for most cases.
968 | If you have higher performance needs you will probably need specialized
969 | implementations.
970 |
971 | Profile regularly to ensure to track the performance characteristics of your
972 | system and be prepared to re-optimize as your traffic changes. Know the
973 | limits of your system and have good metrics that allow you to predict when
974 | you will hit those limits.
975 |
976 | When the usage of your application changes, different pieces may become
977 | hotspots. Revisit previous optimizations and decide if they're still worth
978 | it, and revert to more readable code when possible. I had one system that I
979 | had optimized process startup time with a complex set of mmap, reflect, and
980 | unsafe. Once we changed how the system was deployed, this code was no longer
981 | required and I replaced it with much more readable regular file operations.
982 |
983 | TODO(dgryski): hash function work should fall here; manually inlining, removing structs,
984 | unrolling loops, removing bounds checks
985 |
986 | ### Optimization workflow summary
987 |
988 | All optimizations should follow these steps:
989 |
990 | 1. determine your performance goals and confirm you are not meeting them
991 | 1. profile to identify the areas to improve.
992 | * This can be CPU, heap allocations, or goroutine blocking.
993 | 1. benchmark to determine the speed up your solution will provide using
994 | the built-in benchmarking framework ()
995 | * Make sure you're benchmarking the right thing on your target
996 | operating system and architecture.
997 | 1. profile again afterwards to verify the issue is gone
998 | 1. use or
999 | to verify that a set of timings
1000 | are 'sufficiently' different for an optimization to be worth the added
1001 | code complexity.
1002 | 1. use for load testing http services
1003 | (+ other fancy ones: k6, fortio, fbender)
1004 | - if possible, test ramp-up/ramp-down in addition to steady-state load
1005 | 1. make sure your latency numbers make sense
1006 |
1007 | TODO: mention github.com/aclements/perflock as cpu noise reduction tool
1008 |
1009 | The first step is important. It tells you when and where to start optimizing.
1010 | More importantly, it also tells you when to stop. Pretty much all optimizations
1011 | add code complexity in exchange for speed. And you can *always* make code
1012 | faster. It's a balancing act.
1013 |
1014 |
1015 | ## Garbage Collection
1016 |
1017 | You pay for memory allocation more than once. The first is obviously when you
1018 | allocate it. But you also pay every time the garbage collection runs.
1019 |
1020 | > Reduce/Reuse/Recycle.
1021 | > -- @bboreham
1022 |
1023 | * Stack vs. heap allocations
1024 | * What causes heap allocations?
1025 | * Understanding escape analysis (and the current limitation)
1026 | * /debug/pprof/heap , and -base
1027 | * API design to limit allocations:
1028 | * allow passing in buffers so caller can reuse rather than forcing an allocation
1029 | * you can even modify a slice in place carefully while you scan over it
1030 | * passing in a struct could allow caller to stack allocate it
1031 | * reducing pointers to reduce gc scan times
1032 | * pointer-free slices
1033 | * maps with both pointer-free keys and values
1034 | * GOGC
1035 | * buffer reuse (sync.Pool vs or custom via go-slab, etc)
1036 | * slicing vs. offset: pointer writes while GC is running need writebarrier: https://github.com/golang/go/commit/b85433975aedc2be2971093b6bbb0a7dc264c8fd
1037 | * no writebarrier if writing to stack https://github.com/golang/go/commit/2140975ebde164ea1eaa70fc72775c03567f2bc9
1038 | * use error variables instead of errors.New() / fmt.Errorf() at call site (performance or style? interface requires pointer, so it escapes to heap anyway)
1039 | * use structured errors to reduce allocation (pass struct value), create string at error printing time
1040 | * size classes
1041 | * beware pinning larger allocation with smaller substrings or slices
1042 |
1043 | ## Runtime and compiler
1044 |
1045 | * cost of calls via interfaces (indirect calls on the CPU level)
1046 | * runtime.convT2E / runtime.convT2I
1047 | * type assertions vs. type switches
1048 | * defer
1049 | * special-case map implementations for ints, strings
1050 | * map for byte/uint16 not optimized; use a slice instead.
1051 | * You can fake a float64-optimized with math.Float{32,64}{from,}bits, but beware float equality issues
1052 | * https://github.com/dgryski/go-gk/blob/master/exact.go says 100x faster; need benchmarks
1053 | * bounds check elimination
1054 | * []byte <-> string copies, map optimizations
1055 | * two-value range will copy an array, use the slice instead:
1056 | *
1057 | *
1058 | * use string concatenation instead of fmt.Sprintf where possible; runtime has optimized routines for it
1059 |
1060 | ## Unsafe
1061 |
1062 | * And all the dangers that go with it
1063 | * Common uses for unsafe
1064 | * mmap'ing data files
1065 | * struct padding
1066 | * but not always sufficiently faster to justify complexity/safety cost
1067 | * but "off-heap", so ignored by gc (but so would a pointerless slice)
1068 | * need to think about serialization format: how to deal with pointers, indexing (mph, index header)
1069 | * speedy de-serialization
1070 | * binary wire protocol to struct when you already have the buffer
1071 | * string <-> slice conversion, []byte <-> []uint32, ...
1072 | * int to bool unsafe hack (but cmov) (but != 0 is also branch-free)
1073 | * padding:
1074 | - https://dave.cheney.net/2015/10/09/padding-is-hard
1075 | - http://www.catb.org/esr/structure-packing/#_go_and_rust
1076 | - https://golang.org/ref/spec#Size_and_alignment_guarantees
1077 | - https://github.com/dominikh/go-tools structlayout, structlayout-optimize
1078 | - write tests for struct layout with unsafe.Offsetof to notice breakage from unsafe or asm
1079 |
1080 | ## Common gotchas with the standard library
1081 |
1082 | * time.After() leaks until it fires; use t := NewTimer(); t.Stop() / t.Reset()
1083 | * Reusing HTTP connections...; ensure the body is drained (issue #?)
1084 | * rand.Int() and friends are 1) mutex protected and 2) expensive to create
1085 | * consider alternate random number generation (go-pcgr, xorshift)
1086 | * binary.Read and binary.Write use reflection and are slow; do it by hand. (https://github.com/conformal/yubikey/commit/613e3b04ae2eeb78e6a19636b8ff8e9106d2e7bc)
1087 | * use strconv instead of fmt if possible
1088 | * Use `strings.EqualFold(str1, str2)` instead of `strings.ToLower(str1) == strings.ToLower(str2)` or `strings.ToUpper(str1) == strings.ToUpper(str2)` to efficiently compare strings if possible.
1089 | * ...
1090 |
1091 | ## Alternate implementations
1092 |
1093 | Popular replacements for standard library packages:
1094 |
1095 | * encoding/json -> [ffjson](https://github.com/pquerna/ffjson), [easyjson](https://github.com/mailru/easyjson), [jingo](https://github.com/bet365/jingo) (only encoder), etc
1096 | * net/http
1097 | * [fasthttp](https://github.com/valyala/fasthttp/) (but incompatible API, not RFC compliant in subtle ways)
1098 | * [httprouter](https://github.com/julienschmidt/httprouter) (has other features besides speed; I've never actually seen routing in my profiles)
1099 | * regexp -> [ragel](https://www.colm.net/open-source/ragel/) (or other regular expression package)
1100 | * serialization
1101 | * encoding/gob ->
1102 | * protobuf ->
1103 | * all serialization formats have trade-offs: choose one that matches what you need
1104 | - Write heavy workload -> fast encoding speed
1105 | - Read-heavy workload -> fast decoding speed
1106 | - Other considerations: encoded size, language/tooling compatibility
1107 | - tradeoffs of packed binary formats vs. self-describing text formats
1108 | * database/sql -> has tradeoffs that affect performance
1109 | * look for drivers that don't use it: jackx/pgx, crawshaw sqlite, ...
1110 | * gccgo (benchmark!), gollvm (WIP)
1111 | * container/list: use a slice instead (almost always)
1112 |
1113 | ## cgo
1114 |
1115 | > cgo is not go
1116 | > -- Rob Pike
1117 |
1118 | * Performance characteristics of cgo calls
1119 | * Tricks to reduce the costs: batching
1120 | * Rules on passing pointers between Go and C
1121 | * syso files (race detector, dev.boringssl)
1122 |
1123 | ## Advanced Techniques
1124 |
1125 | Techniques specific to the architecture running the code
1126 |
1127 | * introduction to CPU caches
1128 | * performance cliffs
1129 | * building intuition around cache-lines: sizes, padding, alignment
1130 | * OS tools to view cache-misses (perf)
1131 | * maps vs. slices
1132 | * SOA vs AOS layouts: row-major vs. column major; when you have an X, do you need another X or do you need a Y?
1133 | * temporal and spacial locality: use what you have and what's nearby as much as possible
1134 | * reducing pointer chasing
1135 | * explicit memory prefetching; frequently ineffective; lack of intrinsics means function call overhead (removed from runtime)
1136 | * make the first 64-bytes of your struct count
1137 | * branch prediction
1138 | * remove branches from inner loops:
1139 | if a { for { } } else { for { } }
1140 | instead of
1141 | for { if a { } else { } }
1142 | benchmark due to branch prediction
1143 | structure to avoid branch
1144 |
1145 | if i % 2 == 0 {
1146 | evens++
1147 | } else {
1148 | odds++
1149 | }
1150 |
1151 | counts[i & 1] ++
1152 | "branch-free code", benchmark; not always faster, but frequently harder to read
1153 | TODO: ASCII class counts example, with benchmarks
1154 |
1155 | * sorting data can help improve performance via both cache locality and branch prediction, even taking into account the time it takes to sort
1156 | * function call overhead: inliner is getting better
1157 | * reduce data copies (including for repeated large lists of function params)
1158 |
1159 | * Comment about Jeff Dean's 2002 numbers (plus updates)
1160 | * cpus have gotten faster, but memory hasn't kept up
1161 |
1162 | TODO: little comment about code-aligment free optimization (or unoptimization)
1163 |
1164 | ## Concurrency
1165 |
1166 | * Figure out which pieces can be done in parallel and which must be sequential
1167 | * goroutines are cheap, but not free.
1168 | * Optimizing multi-threaded code
1169 | * false-sharing -> pad to cache-line size
1170 | * true sharing -> sharding
1171 | * Overlap with previous section on caches and false/true sharing
1172 | * Lazy synchronization; it's expensive, so duplicating work may be cheaper
1173 | * things you can control: number of workers, batch size
1174 |
1175 | You need a mutex to protect shared mutable state. If you have lots of mutex
1176 | contention, you need to either reduce the shared, or reduce the mutable. Two
1177 | ways to reduce the shared are 1) shard the locks or 2) process independently
1178 | and combine afterwards. To reduce mutable: well, make your data structure
1179 | read-only. You can also reduce the time the data needs be shared by reducing
1180 | the critical section -- hold the lock as little as needed. Sometimes a RWMutex
1181 | will be sufficient, although note that they're slower but they allow multiple
1182 | readers in.
1183 |
1184 | If you're sharding the locks, be careful of shared cache-lines. You'll need to pad
1185 | to avoid cache-line bouncing between processors.
1186 |
1187 | var stripe [8]struct{ sync.Mutex; _ [7]uint64 } // mutex is 64-bits; padding fills the rest of the cacheline
1188 |
1189 | Don't do anything expensive in your critical section if you can help it. This includes things like I/O (which are cheap but slow).
1190 |
1191 | TODO: how to decompose problem for concurrency
1192 | TODO: reasons parallel implementation might be slower (communication overhead, best algorithm is sequential, ... )
1193 |
1194 | ## Assembly
1195 |
1196 | * Stuff about writing assembly code for Go
1197 | * compilers improve; the bar is high
1198 | * replace as little as possible to make an impact; maintenance cost is high
1199 | * good reasons: SIMD instructions or other things outside of what Go and the compiler can provide
1200 | * very important to benchmark: improvements can be huge (10x for go-highway)
1201 | zero (go-speck/rc6/farm32), or even slower (no inlining)
1202 | * rebenchmark with new versions to see if you can delete your code yet
1203 | * TODO: link to 1.11 patches removing asm code
1204 | * always have pure-Go version (purego build tag): testing, arm, gccgo
1205 | * brief intro to syntax
1206 | * how to type the middle dot
1207 | * calling convention: everything is on the stack, followed by the return values.
1208 | - everything is on the stack, followed by the return values
1209 | - this might change https://github.com/golang/go/issues/18597
1210 | - https://science.raphael.poss.name/go-calling-convention-x86-64.html
1211 | * using opcodes unsupported by the asm (asm2plan9, but this is getting rarer)
1212 | * notes about why inline assembly is hard: https://github.com/golang/go/issues/26891
1213 | * all the tooling to make this easier:
1214 | - asmfmt: gofmt for assembly https://github.com/klauspost/asmfmt
1215 | - c2goasm: convert assembly from gcc/clang to goasm https://github.com/minio/c2goasm
1216 | - go2asm: convert go to assembly you can link https://rsc.io/tmp/go2asm
1217 | - peachpy/avo: higher-level assembler in python (peachpy) or Go (avo)
1218 | - differences of above
1219 | * https://github.com/golang/go/wiki/AssemblyPolicy
1220 | * Design of the Go Assembler: https://talks.golang.org/2016/asm.slide
1221 |
1222 | ## Optimizing an entire service
1223 |
1224 | Most of the time you won't be presented with a single CPU-bound routine.
1225 | That's the easy case. If you have a service to optimize, you need to look
1226 | at the entire system. Monitoring. Metrics. Log lots of things over time
1227 | so you can see them getting worse and so you can see the impact your
1228 | changes have in production.
1229 |
1230 | tip.golang.org/doc/diagnostics.html
1231 |
1232 | * references for system design: SRE Book, practical distributed system design
1233 | * extra tooling: more logging + analysis
1234 | * The two basic rules: either speed up the slow things or do them less frequently.
1235 | * distributed tracing to track bottlenecks at a higher level
1236 | * query patterns for querying a single server instead of in bulk
1237 | * your performance issues may not be your code, but you'll have to work around them anyway
1238 | * https://docs.microsoft.com/en-us/azure/architecture/antipatterns/
1239 |
1240 | ## Tooling
1241 |
1242 | ### Introductory Profiling
1243 |
1244 | This is a quick cheat-sheet for using the pprof tooling. There are plenty of other guides available on this.
1245 | Check out https://github.com/davecheney/high-performance-go-workshop.
1246 |
1247 | TODO(dgryski): videos?
1248 |
1249 | 1. Introduction to pprof
1250 | * go tool pprof (and )
1251 | 1. Writing and running (micro)benchmarks
1252 | * small, like unit tests
1253 | * profile, extract hot code to benchmark, optimize benchmark, profile.
1254 | * -cpuprofile / -memprofile / -benchmem
1255 | * 0.5 ns/op means it was optimized away -> how to avoid
1256 | * tips for writing good microbenchmarks (remove unnecessary work, but add baselines)
1257 | 1. How to read it pprof output
1258 | 1. What are the different pieces of the runtime that show up
1259 | * malloc, gc workers
1260 | * runtime.\_ExternalCode
1261 | 1. Macro-benchmarks (Profiling in production)
1262 | * larger, like end-to-end tests
1263 | * net/http/pprof, debug muxer
1264 | * because it's sampling, hitting 10 servers at 100hz is the same as hitting 1 server at 1000hz
1265 | 1. Using -base to look at differences
1266 | 1. Memory options: -inuse_space, -inuse_objects, -alloc_space, -alloc_objects
1267 | 1. Profiling in production; localhost+ssh tunnels, auth headers, using curl.
1268 | 1. How to read flame graphs
1269 |
1270 | ### Tracer
1271 |
1272 | ### Look at some more interesting/advanced tooling
1273 |
1274 | * other tooling in /x/perf
1275 | * perf (perf2pprof)
1276 | * intel vtune / amd codexl / apple instruments
1277 | * https://godoc.org/github.com/aclements/go-perf
1278 |
1279 | ## Appendix: Implementing Research Papers
1280 |
1281 | Tips for implementing papers: (For `algorithm` read also `data structure`)
1282 |
1283 | * Don't. Start with the obvious solution and reasonable data structures.
1284 |
1285 | "Modern" algorithms tend to have lower theoretical complexities but high constant
1286 | factors and lots of implementation complexity. One of the classic examples of
1287 | this is Fibonacci heaps. They're notoriously difficult to get right and have
1288 | a huge constant factor. There has been a number of papers published comparing
1289 | different heap implementations on different workloads, and in general the 4-
1290 | or 8-ary implicit heaps consistently come out on top. And even in the cases
1291 | where Fibonacci heap should be faster (due to O(1) "decrease-key"),
1292 | experiments with Dijkstra's depth-first search algorithm show it's faster
1293 | when they use the straight heap removal and addition.
1294 |
1295 | Similarly, treaps or skiplists vs. the more complex red-black or AVL trees.
1296 | On modern hardware, the "slower" algorithm may be fast enough, or even
1297 | faster.
1298 |
1299 | > The fastest algorithm can frequently be replaced by one that is almost as fast and much easier to understand.
1300 | >
1301 | > -- Douglas W. Jones, University of Iowa
1302 |
1303 | and
1304 |
1305 | > Rule 3. Fancy algorithms are slow when n is small, and n is usually small.
1306 | > Fancy algorithms have big constants. Until you know that n is frequently going
1307 | > to be big, don't get fancy.
1308 | >
1309 | > Rule 4. Fancy algorithms are buggier than simple ones, and they're much
1310 | > harder to implement. Use simple algorithms as well as simple data structures.
1311 | > -- "Notes on C Programming" (Rob Pike, 1989)
1312 |
1313 | The added complexity has to be enough that the payoff is actually worth it.
1314 | Another example is cache eviction algorithms. Different algorithms can have
1315 | much higher complexity for only a small improvement in hit ratio. Of course,
1316 | you may not be able to test this until you have a working implementation and
1317 | have integrated it into your program.
1318 |
1319 | Sometimes the paper will have graphs, but much like the trend towards
1320 | publishing only positive results, these will tend to be skewed in favour of
1321 | showing how good the new algorithm is.
1322 |
1323 | * Choose the right paper.
1324 | * Look for the paper their algorithm claims to beat and implement that.
1325 |
1326 | Frequently, earlier papers will be easier to understand and necessarily have
1327 | simpler algorithms.
1328 |
1329 | Not all papers are good.
1330 |
1331 | Look at the context the paper was written in. Determine assumptions about
1332 | the hardware: disk space, memory usage, etc. Some older papers make
1333 | different tradeoffs that were reasonable in the 70s or 80s but don't
1334 | necessarily apply to your use case. For example, what they determine to be
1335 | "reasonable" memory vs. disk usage tradeoffs. Memory sizes are now orders of
1336 | magnitude larger, and SSDs have altered the latency penalty for using disk.
1337 | Similarly, some streaming algorithms are designed for router hardware, which
1338 | can make it a pain to translate into software.
1339 |
1340 | Make sure the assumptions the algorithm makes about your data hold.
1341 |
1342 | This will take some digging. You probably don't want to implement the
1343 | first paper you find.
1344 |
1345 | * Make sure you understand the algorithm. This sounds obvious, but it will be
1346 | impossible to debug otherwise.
1347 |
1348 |
1349 |
1350 | A good understanding may allow you to extract the key idea from the paper
1351 | and possibly apply just that to your problem, which may be simpler than
1352 | reimplementing the entire thing.
1353 |
1354 | * The original paper for a data structure or algorithm isn't always the best. Later papers may have better explanations.
1355 |
1356 | * Some papers release reference source code which you can compare against, but
1357 | 1) academic code is almost universally terrible
1358 | 2) beware licensing restrictions ("research purposes only")
1359 | 3) beware bugs; edge cases, error checking, performance etc.
1360 |
1361 | Also look out for other implementations on GitHub: they may have the same (or different!) bugs as yours.
1362 |
1363 | Other resources on this topic:
1364 | *
1365 | *
1366 |
--------------------------------------------------------------------------------