Základní optimalizace v Go aneb pomáháme překladači: konstrukce řetězců

28. 2. 2023
Doba čtení: 28 minut

Sdílet

Autor: Depositphotos
Opět se seznámíme s některými dalšími optimalizacemi, které lze provádět na úrovni zdrojového kódu. Zabývat se budeme zdánlivě triviální úlohou: jak efektivně zkonstruovat řetězec skládáním jednotlivých znaků a/nebo kratších řetězců.

Obsah

1. Základní optimalizace v Go aneb pomáháme překladači (3)

2. Práce s řetězci v programovacím jazyku Go

3. Postupné skládání řetězce s využitím operátoru konkatenace

4. Skládání znaků do bufferu s následným převodem bufferu na řetězec

5. Využití objektu strings.Builder

6. Předalokace paměti pro řetězec: varianta s bufferem

7. Předalokace paměti pro řetězec: varianta s objektem strings.Builder

8. Která varianta je nejvýhodnější?

9. Realizace benchmarků pro skládání řetězce znak po znaku

10. Výsledky benchmarků

11. Grafická vizualizace výsledků benchmarků

12. Podrobnější analýza prováděných operací

13. Postupné skládání řetězce z menších řetězců

14. Realizace konstrukce řetězce s využitím objektů bytes.Buffer a strings.Builder

15. Realizace benchmarků pro skládání řetězce z menších řetězců

16. Výsledky benchmarků

17. Grafická vizualizace výsledků benchmarků

18. Kde funkce pro konstrukci řetězce stráví nejvíce času?

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Základní optimalizace v Go aneb pomáháme překladači (3)

V dnešní části seriálu o programovacím jazyku Go se opět seznámíme s některými dalšími optimalizacemi, které je v mnoha případech vhodné či nutné provádět na úrovni zdrojového kódu. Zabývat se budeme zdánlivě triviální úlohou: jak efektivně zkonstruovat řetězec skládáním jednotlivých znaků a/nebo kratších řetězců. Podobné operace se v praxi skutečně provádí, i když mohou být poněkud skryté. Typickým příkladem je šablonovací systém, který interně postupně „skládá“ výsledný řetězec (dokument); dalším příkladem jsou pak systémy pro generování a tisk logovacích informací – i zde je na konci celé činnosti nějaká forma řetězce získaná postupným skládáním z kratších statických či dynamicky počítaných částí.

2. Práce s řetězci v programovacím jazyku Go

Interně je řetězec v paměti uložen jako velmi jednoduchá struktura s ukazatelem na oblast paměti se sekvencí bajtů a s délkou řetězce. Samotná sekvence bajtů může být sdílená větším množstvím řetězců, protože řetězce jsou v Go neměnné (immutable):

type stringStruct struct {
        str unsafe.Pointer
        len int
}
Poznámka: pro zjednodušení exportu ladicích informací existuje ještě jedna definice struktury pro řetězec. Liší se pouze typem ukazatele (nyní je typový):
// Variant with *byte pointer type for DWARF debugging.
type stringStructDWARF struct {
        str *byte
        len int
}

Pro zajímavost se podívejme na dvě funkce, které se v runtime při operacích s řetězci volají velmi často. Ostatně tyto funkce můžeme vidět i ve výsledcích činnosti profileru (viz benchmarky uvedené níže). Jedná se o funkci volanou pro alokaci paměti pro řetězec a o funkci pro spojení dvou či více řetězců (což je funkce volaná při použití operátoru +):

// rawstring allocates storage for a new string. The returned
// string and byte slice both refer to the same storage.
// The storage is not zeroed. Callers should use
// b to set the string contents and then drop b.
func rawstring(size int) (s string, b []byte) {
        p := mallocgc(uintptr(size), nil, false)
        return unsafe.String((*byte)(p), size), unsafe.Slice((*byte)(p), size)
}
 
// concatstrings implements a Go string concatenation x+y+z+...
// The operands are passed in the slice a.
// If buf != nil, the compiler has determined that the result does not
// escape the calling function, so the string data can be stored in buf
// if small enough.
func concatstrings(buf *tmpBuf, a []string) string {
        idx := 0
        l := 0
        count := 0
        for i, x := range a {
                n := len(x)
                if n == 0 {
                        continue
                }
                if l+n < l {
                        throw("string concatenation too long")
                }
                l += n
                count++
                idx = i
        }
        if count == 0 {
                return ""
        }
 
        // If there is just one string and either it is not on the stack
        // or our result does not escape the calling frame (buf != nil),
        // then we can return that string directly.
        if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
                return a[idx]
        }
        s, b := rawstringtmp(buf, l)
        for _, x := range a {
                copy(b, x)
                b = b[len(x):]
        }
        return s
}

3. Postupné skládání řetězce s využitím operátoru konkatenace

Předpokládejme, že máme realizovat funkci, která zkonstruuje řetězec, a to takovým způsobem, že do něj bude postupně přidávat jednotlivé znaky (resp. přesněji řečeno runy). Pochopitelně se v této konkrétní podobě jedná o umělý příklad, jenž je ovšem postaven na reálných základech, protože podobně mohou pracovat i další algoritmy (dekomprimace, šablonovací systémy, některé logovací systémy atd.). Zde se navíc jedná o mezní případ, kdy se postupně přidávají jednotlivé znaky a ne celé sekvence znaků, ovšem tím líp bude celý problém patrný:

package main
 
func BuildStringUsingConcatenation(n int) string {
        // budeme pouzivat primo retezec
        s := ""
 
        // postupne pridavani prvku do vysledneho retezce
        for i := 0; i < n; i++ {
                s += "*"
        }
 
        // vysledny retezec
        return s
}

Toto řešení je z pohledu programátora velmi čisté a výsledek je snadno pochopitelný i testovatelný. Ovšem problém tohoto řešení spočívá v tom, že se interně bude prakticky neustále provádět realokace paměti, což ostatně bude velmi dobře patrné na výsledcích benchmarků.

Poznámka: mohlo by se zdát, že v tomto případě je výhodnější připojit k řetězci jednu runu. To však typový systém jazyka Go přímo neumožňuje:
s += '*'
 
./concatenation.go:9:3: invalid operation: s += '*' (mismatched types string and untyped rune)

4. Skládání znaků do bufferu s následným převodem bufferu na řetězec

Existují však alternativní řešení výše uvedeného problému? Ukazuje se, že existují a dokonce jich je několik; všechna tato řešení jsou přitom postavena na standardní knihovně programovacího jazyka Go. Jedno z těchto řešení používá standardní balíček bytes, konkrétně datový typ bytes.Buffer. Tento datový typ skutečně realizuje buffer, do kterého je možné přidávat jednotlivé znaky metodou WriteRune, přičemž přidávání sice taktéž vyžaduje realokaci paměti, ale ta by měla být provedena optimálnějším :-) způsobem (tedy s menší frekvencí). Tento buffer můžeme na konci převést na klasický řetězec metodou String, takže celý algoritmus můžeme přepsat do této podoby:

package main
 
import "bytes"
 
func BuildStringUsingStringBuffer(n int) string {
        // budeme pouzivat buffer
        var bb bytes.Buffer
 
        // postupne pridavani prvku do vysledneho retezce
        for i := 0; i < n; i++ {
                bb.WriteRune('*')
        }
 
        // prevod objektu na retezec
        return bb.String()
}
Poznámka: povšimněte si, že zde nedáváme programu k dispozici očekávanou délku řetězce a necháváme tedy celý odhad, jak buffer alokovat a jak často, na vnitřních algoritmech standardní knihovny.

5. Využití objektu strings.Builder

Další varianta spočívá ve využití objektu (datového typu) nazvaného strings.Builder. Jak již název napovídá, je tento datový typ součástí standardního balíčku strings; jeho popis nalezneme na adrese https://pkg.go.dev/strings@go1­.20.1#Builder. Práce s objektem strings.Builder je prakticky totožná s předchozím příkladem založeným na objektu bytes.Buffer. I zde totiž nalezneme metodu WriteRune určenou pro přidání dalšího znaku a metodu String určenou pro převod hodnoty uložené v tomto objektu na běžný řetězec. Úprava celého algoritmu je tedy triviální:

package main
 
import "strings"
 
func BuildStringUsingStringBuilder(n int) string {
        // budeme pouzivat String Builder
        var sb strings.Builder
 
        // postupne pridavani prvku do vysledneho retezce
        for i := 0; i < n; i++ {
                sb.WriteRune('*')
        }
 
        // prevod objektu na retezec
        return sb.String()
}

6. Předalokace paměti pro řetězec: varianta s bufferem

Vraťme se nyní k využití bufferu, tedy datového typu bytes.Buffer. Paměť pro buffer, tedy v našem případě pro sekvenci znaků, je totiž možné předalokovat, a to konkrétně zavoláním metody Grow. Díky tomu můžeme – samozřejmě za předpokladu, že dopředu známe či alespoň dokážeme odhadnout výslednou délku řetězce – provést alokaci paměťového bloku jen jedenkrát, a to na začátku celého algoritmu. Ostatně si to můžeme velmi snadno otestovat:

package main
 
import "bytes"
 
func BuildStringUsingPreallocatedStringBuffer(n int) string {
        // budeme pouzivat buffer
        var bb bytes.Buffer
 
        // alokace pameti pro pozdeji vkladane prvky
        bb.Grow(n)
 
        // postupne pridavani prvku do vysledneho retezce
        for i := 0; i < n; i++ {
                bb.WriteRune('*')
        }
 
        // prevod objektu na retezec
        return bb.String()
}

7. Předalokace paměti pro řetězec: varianta s objektem strings.Builder

A konečně – metoda Grow není dostupná pouze u objektů typu bytes.Buffer, ale i pro objekt strings.Builder, což nám umožňuje snadno předalokovat paměť i pro tento objekt. Samotný zápis programu se změní pouze nepatrně; vlastně se modifikuje pouze typ proměnné sb:

package main
 
import "strings"
 
func BuildStringUsingPreallocatedStringBuilder(n int) string {
        // budeme pouzivat String Builder
        var sb strings.Builder
 
        // alokace pameti pro pozdeji vkladane prvky
        sb.Grow(n)
 
        // postupne pridavani prvku do vysledneho retezce
        for i := 0; i < n; i++ {
                sb.WriteRune('*')
        }
 
        // prevod objektu na retezec
        return sb.String()
}

8. Která varianta je nejvýhodnější?

V předchozích kapitolách jsme si ukázali tři varianty postupného „lepení“ výsledného řetězce přidáváním dalších znaků resp. přesněji řečeno run. U variant postavených nad datovými strukturami bytes.Buffer a string.Builder jsme navíc mohli velmi snadno provést předalokaci bloku paměti, což vede (resp. by alespoň teoreticky mělo vést) ke zmenšení volání funkce pro kopii bloků dat při postupných paměťových realokacích. Ovšem která varianta konstrukce řetězce bude nejrychlejší a paměťově nejzajímavější v praxi? K tomuto účelu si vytvoříme sadu benchmarků, které budou postupně – tj. prozatím znak po znaku – konstruovat řetězec a vypisovat dosaženou rychlost. Pro realizaci benchmarků budou použity balíčky ze standardní knihovny jazyka Go, zejména balíček testing a konkrétně datový typ B představující realizaci benchmarku.

Poznámka: důležité je, že konkatenace spojená s realokací paměti nepoužívá žádný masivně paralelní kód, což je ostatně vidět i z výpisu top (ideálně bychom měli vidět hodnotu okolo 800 ve sloupci %CPU):
top - 13:07:52 up 65 days, 23:33,  3 users,  load average: 3,14, 2,97, 2,51
Tasks: 327 total,   1 running, 325 sleeping,   0 stopped,   1 zombie
%Cpu0  : 11,8 us,  5,9 sy,  0,0 ni, 76,5 id,  0,0 wa,  0,0 hi,  5,9 si,  0,0 st
%Cpu1  : 18,8 us,  0,0 sy,  0,0 ni, 81,2 id,  0,0 wa,  0,0 hi,  0,0 si,  0,0 st
%Cpu2  : 11,8 us,  5,9 sy,  0,0 ni, 76,5 id,  0,0 wa,  0,0 hi,  5,9 si,  0,0 st
%Cpu3  : 25,0 us,  0,0 sy,  0,0 ni, 75,0 id,  0,0 wa,  0,0 hi,  0,0 si,  0,0 st
%Cpu4  : 25,0 us,  0,0 sy,  0,0 ni, 75,0 id,  0,0 wa,  0,0 hi,  0,0 si,  0,0 st
%Cpu5  :  6,2 us,  0,0 sy,  0,0 ni, 93,8 id,  0,0 wa,  0,0 hi,  0,0 si,  0,0 st
%Cpu6  : 18,8 us,  0,0 sy,  0,0 ni, 81,2 id,  0,0 wa,  0,0 hi,  0,0 si,  0,0 st
%Cpu7  : 47,1 us, 23,5 sy,  0,0 ni, 29,4 id,  0,0 wa,  0,0 hi,  0,0 si,  0,0 st
MiB Mem :  31893,9 total,   4228,9 free,   5697,8 used,  21967,2 buff/cache
MiB Swap:    662,2 total,    659,8 free,      2,4 used.  21328,6 avail Mem
 
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
2605576 ptisnovs  20   0  982076  97100   2068 S 193,3   0,3  23:10.61 string-builders
      1 root      20   0 1300148  13312   8440 S   0,0   0,0   2366:31 systemd
      2 root      20   0       0      0      0 S   0,0   0,0   0:10.90 kthreadd
      3 root       0 -20       0      0      0 I   0,0   0,0   0:00.00 rcu_gp
      4 root       0 -20       0      0      0 I   0,0   0,0   0:00.00 rcu_par_gp
      6 root       0 -20       0      0      0 I   0,0   0,0   0:09.69 kworker/0:0H-events_highpri
      8 root       0 -20       0      0      0 I   0,0   0,0   0:00.00 mm_percpu_wq
      9 root      20   0       0      0      0 S   0,0   0,0  32:55.30 ksoftirqd/0

9. Realizace benchmarků pro skládání řetězce znak po znaku

Samotná realizace benchmarků je vlastně velmi přímočará, protože již máme připraveny všechny potřebné algoritmy a známe funkce a metody, které se musí použít. Každý konkrétní benchmark nejdříve zkonstruuje řetězec (svým vlastním způsobem) a následně zavolá funkci checkBuiltString, která zkontroluje, jestli má řetězec očekávanou délku a taktéž zda obsahuje očekávané znaky, což by měla být sekvence hvězdiček o délce N (kde N se zadává při spouštění benchmarků). Samotná kontrola obsahu řetězce se již nezapočítává do času benchmarku. Podívejme se nyní na úplný zdrojový kód benchmarků:

package main
 
import (
        "bytes"
        "strings"
 
        "testing"
)
 
func checkBuiltString(b *testing.B, s *string) {
        // kontrola delky vysledneho retezce
        if len(*s) != b.N {
                b.Fatal("Wrong string length")
        }
 
        // kontrola obsahu vysledneho retezce
        for i, r := range *s {
                if r != '*' {
                        b.Fatal("Wrong rune", r, "at index", i)
                }
        }
}
 
func BenchmarkBuildStringUsingConcatenation(b *testing.B) {
        // budeme pouzivat primo retezec
        s := ""
 
        // postupne pridavani prvku do vysledneho retezce
        for n := 0; n < b.N; n++ {
                s += "*"
        }
 
        // zkontrolovat obsah vytvoreneho retezce po zastaveni casovace
        b.StopTimer()
        checkBuiltString(b, &s)
}
 
func BenchmarkBuildStringUsingStringBuffer(b *testing.B) {
        // budeme pouzivat buffer
        var bb bytes.Buffer
 
        // postupne pridavani prvku do vysledneho retezce
        for n := 0; n < b.N; n++ {
                bb.WriteRune('*')
        }
 
        // prevod objektu na retezec
        s := bb.String()
 
        // zkontrolovat obsah vytvoreneho retezce po zastaveni casovace
        b.StopTimer()
        checkBuiltString(b, &s)
}
 
func BenchmarkBuildStringUsingPreallocatedStringBuffer(b *testing.B) {
        // budeme pouzivat buffer
        var bb bytes.Buffer
 
        // alokace pameti pro pozdeji vkladane prvky
        bb.Grow(b.N)
 
        // postupne pridavani prvku do vysledneho retezce
        for n := 0; n < b.N; n++ {
                bb.WriteRune('*')
        }
 
        // prevod objektu na retezec
        s := bb.String()
 
        // zkontrolovat obsah vytvoreneho retezce po zastaveni casovace
        b.StopTimer()
        checkBuiltString(b, &s)
}
 
func BenchmarkBuildStringUsingStringBuilder(b *testing.B) {
        // budeme pouzivat String Builder
        var sb strings.Builder
 
        // postupne pridavani prvku do vysledneho retezce
        for n := 0; n < b.N; n++ {
                sb.WriteRune('*')
        }
 
        // prevod objektu na retezec
        s := sb.String()
 
        // zkontrolovat obsah vytvoreneho retezce po zastaveni casovace
        b.StopTimer()
        checkBuiltString(b, &s)
}
 
func BenchmarkBuildStringUsingPreallocatedStringBuilder(b *testing.B) {
        // budeme pouzivat String Builder
        var sb strings.Builder
 
        // alokace pameti pro pozdeji vkladane prvky
        sb.Grow(b.N)
 
        // postupne pridavani prvku do vysledneho retezce
        for n := 0; n < b.N; n++ {
                sb.WriteRune('*')
        }
 
        // prevod objektu na retezec
        s := sb.String()
 
        // zkontrolovat obsah vytvoreneho retezce po zastaveni casovace
        b.StopTimer()
        checkBuiltString(b, &s)
}

10. Výsledky benchmarků

Podívejme se nyní na výsledky benchmarků, které byly zjištěny pro N (tedy výslednou délku řetězce) postupně rostoucí v řadě 1, 10, 100, 1000, 10000 a 100000, což znamená, že počet znaků ve výsledném řetězci je v každé další iteraci desetkrát větší oproti předchozí iteraci.

Pro malou výslednou délku řetězce jsou rozdíly minimální a ne zcela vypovídající o prováděných operacích:

goos: linux
goarch: amd64
pkg: string-builders
cpu: Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
BenchmarkBuildStringUsingConcatenation-8                       1               370.0 ns/op
BenchmarkBuildStringUsingStringBuffer-8                        1               765.0 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuffer-8            1               746.0 ns/op
BenchmarkBuildStringUsingStringBuilder-8                       1               635.0 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuilder-8           1               738.0 ns/op
PASS

Na tomto benchmarku a benchmarku navazujícím se ukazuje, že inicializace a popř. převod výsledku na řetězec se postupně eliminují a začne narůstat význam výsledné délky řetězce:

goos: linux
goarch: amd64
pkg: string-builders
cpu: Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
BenchmarkBuildStringUsingConcatenation-8                      10               197.60 ns/op
BenchmarkBuildStringUsingStringBuffer-8                       10                86.90 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuffer-8           10                95.10 ns/op
BenchmarkBuildStringUsingStringBuilder-8                      10               129.90 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuilder-8          10                72.70 ns/op
PASS
goos: linux
goarch: amd64
pkg: string-builders
cpu: Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
BenchmarkBuildStringUsingConcatenation-8                     100                95.02 ns/op
BenchmarkBuildStringUsingStringBuffer-8                      100               165.60 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuffer-8          100                13.55 ns/op
BenchmarkBuildStringUsingStringBuilder-8                     100                14.10 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuilder-8         100                32.82 ns/op
PASS

Už u relativně krátkého řetězce o délce 1000 znaků se začíná ukazovat nevýhoda prosté konkatenace a naopak přednost předalokace paměti:

goos: linux
goarch: amd64
pkg: string-builders
cpu: Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
BenchmarkBuildStringUsingConcatenation-8                    1000               242.80 ns/op
BenchmarkBuildStringUsingStringBuffer-8                     1000                34.08 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuffer-8         1000                 5.595 ns/op
BenchmarkBuildStringUsingStringBuilder-8                    1000                22.61 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuilder-8        1000                 4.322 ns/op
PASS

Rozdíl mezi naivní konkatenací a ostatními variantami se ještě dále zvětšuje:

goos: linux
goarch: amd64
pkg: string-builders
cpu: Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
BenchmarkBuildStringUsingConcatenation-8                   10000               693.60 ns/op
BenchmarkBuildStringUsingStringBuffer-8                    10000                 8.015 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuffer-8        10000                 4.251 ns/op
BenchmarkBuildStringUsingStringBuilder-8                   10000                 4.221 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuilder-8       10000                 3.420 ns/op
PASS
goos: linux
goarch: amd64
pkg: string-builders
cpu: Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
BenchmarkBuildStringUsingConcatenation-8                  100000              4318.000 ns/op
BenchmarkBuildStringUsingStringBuffer-8                   100000                 5.693 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuffer-8       100000                 3.219 ns/op
BenchmarkBuildStringUsingStringBuilder-8                  100000                 4.333 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuilder-8      100000                 2.797 ns/op
PASS

Zajímavé je, že u dlouhých řetězců (zde milion znaků) již není předalokace tak významným prvkem pro výsledný čas. Stále hraje svoji roli, ale ne již tak zásadní, jako například u varianty s řetězcem o délce 1000 znaků:

goos: linux
goarch: amd64
pkg: string-builders
cpu: Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
BenchmarkBuildStringUsingConcatenation-8                 1000000             46829.000 ns/op
BenchmarkBuildStringUsingStringBuffer-8                  1000000                 4.672 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuffer-8      1000000                 4.387 ns/op
BenchmarkBuildStringUsingStringBuilder-8                 1000000                 4.735 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuilder-8     1000000                 3.255 ns/op
PASS

11. Grafická vizualizace výsledků benchmarků

Výsledky benchmarků uvedené v číselné podobě si pochopitelně můžeme převést do grafické podoby, což nám umožní lepší porovnání jednotlivých algoritmů:

Obrázek 1: Grafická vizualizace výsledků benchmarků pro N=1 až 10000, přepočteno na čas pro přidání jediného znaku.

Obrázek 2: Grafická vizualizace výsledků benchmarků pro N=1 až 1000000, přepočteno na čas pro přidání jediného znaku.

Obrázek 3: Kumulativní časy celkové doby trvání operace konstrukce řetězce pro N=1 až 10000.

Poznámka: z těchto výsledků je jasně patrné, že standardní konkatenace není v žádném případě optimálním řešením.

12. Podrobnější analýza prováděných operací

Podrobnější informace o tom, které operace (funkce) se vlastně provádí při konkatenaci řetězců různými algoritmy, nám podá další standardní nástroj programovacího jazyka Go – profiler. Nejdříve ale musíme spustit benchmarky izolovaně a specifikovat přitom, že se má vygenerovat datový soubor s informacemi o volání funkcí:

$ go test -bench=BenchmarkBuildStringUsingConcatenation             -benchtime=1000000x -cpuprofile concatenation.out
$ go test -bench=BenchmarkBuildStringUsingStringBuffer              -benchtime=1000000x -cpuprofile string_buffer.out
$ go test -bench=BenchmarkBuildStringUsingStringBuilder             -benchtime=1000000x -cpuprofile string_builder.out
$ go test -bench=BenchmarkBuildStringUsingPreallocatedStringBuffer  -benchtime=1000000x -cpuprofile preallocated_string_buffer.out
$ go test -bench=BenchmarkBuildStringUsingPreallocatedStringBuilder -benchtime=1000000x -cpuprofile preallocated_string_builder.out

Dále se každý z vygenerovaných souborů analyzuje profilerem:

$ go tool pprof -http=:6060 profile.out

Obrázek 4: Konkatenace řetězců s využitím operátoru +: nejvíce času se stráví ve funkci pro přesuny bloků paměti.

Obrázek 5: Při použití objektu bytes.Buffer se čas stráví z 50% při přidávání znaků a dalších 50% času správcem paměti (což je ovšem spíše náhoda, že byl GC vůbec spuštěn).

Obrázek 6: Při použití objektu strings.Builder se čas stráví přidáváním znaků, tedy vlastním algoritmem.

13. Postupné skládání řetězce z menších řetězců

Ve druhé části dnešního článku si předchozí způsob konstrukce řetězců rozšíříme a nepatrně upravíme. Tentokrát nebudeme výsledný řetězec postupně skládat z jednotlivých znaků, ale z menších řetězců. V jazyce Go pro tento účel opět můžeme použít operátor spojování (konkatenace) řetězců, který je zcela korektní jak ze syntaktického, tak i sémantického pohledu, ovšem výsledek bude velmi pomalý (což si ověříme v benchmarku):

package main
 
func BuildStringUsingConcatenation(n int) string {
        // budeme pouzivat primo retezec
        s := ""
 
        // postupne pridavani prvku do vysledneho retezce
        for i := 0; i < n; i++ {
                s += "foo "
        }
 
        // vysledny retezec
        return s
}

14. Realizace konstrukce řetězce s využitím objektů bytes.Buffer a strings.Builder

Předchozí program lze upravit takovým způsobem, že namísto konkatenace řetězců použijeme objekty bytes.Buffer a strings.Builder. Ty totiž kromě výše použité metody WriteRune nabízí i metody WriteString, což umožňuje do bufferu resp. do builderu připojovat další řetězce. Výsledek pochopitelně na konci musíme převést na řetězec metodou String.

Varianta příkladu ze třinácté kapitoly převedená do podoby s objektem typu bytes.Buffer bude vypadat takto:

package main
 
import "bytes"
 
func BuildStringUsingStringBuffer(n int) string {
        // budeme pouzivat buffer
        var bb bytes.Buffer
 
        // postupne pridavani prvku do vysledneho retezce
        for i := 0; i < n; i++ {
                bb.WriteString("foo ")
        }
 
        // prevod objektu na retezec
        return bb.String()
}

Malou úpravou funkci změníme tak, aby se používal objekt typu strings.Builder:

package main
 
import "strings"
 
func BuildStringUsingStringBuilder(n int) string {
        // budeme pouzivat String Builder
        var sb strings.Builder
 
        // postupne pridavani prvku do vysledneho retezce
        for i := 0; i < n; i++ {
                sb.WriteString("foo ")
        }
 
        // prevod objektu na retezec
        return sb.String()
}

A pochopitelně máme opět pro oba datové typy k dispozici metodu Grow, která nám umožní předalokaci paměťového bloku:

package main
 
import "bytes"
 
func BuildStringUsingPreallocatedStringBuffer(n int) string {
        // budeme pouzivat buffer
        var bb bytes.Buffer
 
        // alokace pameti pro pozdeji vkladane prvky
        bb.Grow(n * len("foo "))
 
        // postupne pridavani prvku do vysledneho retezce
        for i := 0; i < n; i++ {
                bb.WriteString("foo ")
        }
 
        // prevod objektu na retezec
        return bb.String()
}

a:

package main
 
import "strings"
 
func BuildStringUsingPreallocatedStringBuilder(n int) string {
        // budeme pouzivat String Builder
        var sb strings.Builder
 
        // alokace pameti pro pozdeji vkladane prvky
        sb.Grow(n * len("foo "))
 
        // postupne pridavani prvku do vysledneho retezce
        for i := 0; i < n; i++ {
                sb.WriteString("foo ")
        }
 
        // prevod objektu na retezec
        return sb.String()
}
Poznámka: všech pět funkcí vrátí totožný řetězec:
package main
 
import (
        "fmt"
)
 
func main() {
        const n = 20
 
        s := BuildStringUsingConcatenation(n)
        fmt.Println("Concatenation:                   ", s)
 
        s2 := BuildStringUsingStringBuffer(n)
        fmt.Println("StringBuffer (initially empty):  ", s2)
 
        s3 := BuildStringUsingPreallocatedStringBuffer(n)
        fmt.Println("StringBuffer (preallocated):     ", s3)
 
        s4 := BuildStringUsingStringBuilder(n)
        fmt.Println("StringBuilder (initially empty): ", s4)
 
        s5 := BuildStringUsingStringBuilder(n)
        fmt.Println("StringBuilder (preallocated):    ", s5)
}

s výsledkem:

Concatenation:                    foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo
StringBuffer (initially empty):   foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo
StringBuffer (preallocated):      foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo
StringBuilder (initially empty):  foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo
StringBuilder (preallocated):     foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo

15. Realizace benchmarků pro skládání řetězce z menších řetězců

Na základě příkladů uvedených v předchozí kapitole můžeme sestavit benchmarky, které jednotlivé způsoby skládání řetězce z celé řady menších řetězců porovnají. Vstupní řetězec, který je neustále připojován ke konstruovanému řetězci, má délku sta znaků a vzhledem k tomu, že se jedná o znak „*“ z ASCII, je délka tohoto řetězce současně rovna 100 bajtům:

package main
 
import (
        "bytes"
        "strings"
 
        "testing"
)
 
const appendedString = "****************************************************************************************************"
 
func checkBuiltString(b *testing.B, s *string) {
        // kontrola delky vysledneho retezce
        if len(*s) != len(appendedString)*b.N {
                b.Fatal("Wrong string length", len(*s), b.N)
        }
 
        // kontrola obsahu vysledneho retezce
        for i, r := range *s {
                if r != '*' {
                        b.Fatal("Wrong rune", r, "at index", i)
                }
        }
}
 
func BenchmarkBuildStringUsingConcatenation(b *testing.B) {
        // budeme pouzivat primo retezec
        s := ""
 
        // postupne pridavani podretezce do vysledneho retezce
        for n := 0; n < b.N; n++ {
                s += appendedString
        }
 
        // zkontrolovat obsah vytvoreneho retezce po zastaveni casovace
        b.StopTimer()
        checkBuiltString(b, &s)
}
 
func BenchmarkBuildStringUsingStringBuffer(b *testing.B) {
        // budeme pouzivat buffer
        var bb bytes.Buffer
 
        // postupne pridavani podretezce do vysledneho retezce
        for n := 0; n < b.N; n++ {
                bb.WriteString(appendedString)
        }
 
        // prevod objektu na retezec
        s := bb.String()
 
        // zkontrolovat obsah vytvoreneho retezce po zastaveni casovace
        b.StopTimer()
        checkBuiltString(b, &s)
}
 
func BenchmarkBuildStringUsingPreallocatedStringBuffer(b *testing.B) {
        // budeme pouzivat buffer
        var bb bytes.Buffer
 
        // alokace pameti pro pozdeji vkladane prvky
        bb.Grow(b.N * len(appendedString))
 
        // postupne pridavani podretezce do vysledneho retezce
        for n := 0; n < b.N; n++ {
                bb.WriteString(appendedString)
        }
 
        // prevod objektu na retezec
        s := bb.String()
 
        // zkontrolovat obsah vytvoreneho retezce po zastaveni casovace
        b.StopTimer()
        checkBuiltString(b, &s)
}
 
func BenchmarkBuildStringUsingStringBuilder(b *testing.B) {
        // budeme pouzivat String Builder
        var sb strings.Builder
 
        // postupne pridavani podretezce do vysledneho retezce
        for n := 0; n < b.N; n++ {
                sb.WriteString(appendedString)
        }
 
        // prevod objektu na retezec
        s := sb.String()
 
        // zkontrolovat obsah vytvoreneho retezce po zastaveni casovace
        b.StopTimer()
        checkBuiltString(b, &s)
}
 
func BenchmarkBuildStringUsingPreallocatedStringBuilder(b *testing.B) {
        // budeme pouzivat String Builder
        var sb strings.Builder
 
        // alokace pameti pro pozdeji vkladane prvky
        sb.Grow(b.N * len(appendedString))
 
        // postupne pridavani podretezce do vysledneho retezce
        for n := 0; n < b.N; n++ {
                sb.WriteString(appendedString)
        }
 
        // prevod objektu na retezec
        s := sb.String()
 
        // zkontrolovat obsah vytvoreneho retezce po zastaveni casovace
        b.StopTimer()
        checkBuiltString(b, &s)
}

16. Výsledky benchmarků

Opět se podívejme na výsledky benchmarků všech pěti způsobů konstrukce řetězce z menších řetězů pro N rostoucí od 1 do 1000000, kde výsledná délka řetězce je rovna N×100 znakům.

Pro velmi malé počty iterací je inicializace potřebných datových struktur a popř. převod na klasický řetězec hlavním faktorem určujícím dobu trvání operace:

goos: linux
goarch: amd64
pkg: string-builders
cpu: Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
BenchmarkBuildStringUsingConcatenation-8                       1               322.0 ns/op
BenchmarkBuildStringUsingStringBuffer-8                        1              2740.0 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuffer-8            1              3579.0 ns/op
BenchmarkBuildStringUsingStringBuilder-8                       1              2475.0 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuilder-8           1               370.0 ns/op
PASS
ok      string-builders 0.002s
 
 
 
goos: linux
goarch: amd64
pkg: string-builders
cpu: Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
BenchmarkBuildStringUsingConcatenation-8                      10              1890.0 ns/op
BenchmarkBuildStringUsingStringBuffer-8                       10              2016.0 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuffer-8           10               123.8 ns/op
BenchmarkBuildStringUsingStringBuilder-8                      10              1013.0 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuilder-8          10                92.80 ns/op
PASS
ok      string-builders 0.003s

Ale už pro N=100 je jasně patrné, že konkatenace je nejpomalejší operací a naopak se ukazuje přednost použití objektu strings.Builder:

goos: linux
goarch: amd64
pkg: string-builders
cpu: Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
BenchmarkBuildStringUsingConcatenation-8                     100              1634.0 ns/op
BenchmarkBuildStringUsingStringBuffer-8                      100               344.7 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuffer-8          100               170.3 ns/op
BenchmarkBuildStringUsingStringBuilder-8                     100               186.3 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuilder-8         100                48.44 ns/op
PASS
ok      string-builders 0.003s
goos: linux
goarch: amd64
pkg: string-builders
cpu: Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
BenchmarkBuildStringUsingConcatenation-8                    1000              5430.0 ns/op
BenchmarkBuildStringUsingStringBuffer-8                     1000                55.94 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuffer-8         1000                44.43 ns/op
BenchmarkBuildStringUsingStringBuilder-8                    1000               152.4 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuilder-8        1000                28.23 ns/op
PASS
ok      string-builders 0.010s

Pro ještě větší hodnotu N se již stává prostá konkatenace neúnosnou operací (což ostatně odpovídá její teoretické časové složitosti):

goos: linux
goarch: amd64
pkg: string-builders
cpu: Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
BenchmarkBuildStringUsingConcatenation-8                   10000             41270.0 ns/op
BenchmarkBuildStringUsingStringBuffer-8                    10000                81.49 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuffer-8        10000                70.64 ns/op
BenchmarkBuildStringUsingStringBuilder-8                   10000               136.1 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuilder-8       10000                16.45 ns/op
PASS
ok      string-builders 0.423s
goos: linux
goarch: amd64
pkg: string-builders
cpu: Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
BenchmarkBuildStringUsingConcatenation-8                  100000            781287.0 ns/op
BenchmarkBuildStringUsingStringBuffer-8                   100000               126.2 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuffer-8       100000                42.36 ns/op
BenchmarkBuildStringUsingStringBuilder-8                  100000               138.0 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuilder-8      100000                20.56 ns/op
PASS
goos: linux
goarch: amd64
pkg: string-builders-2
cpu: Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz
BenchmarkBuildStringUsingConcatenation-8                 1000000           6508452.0 ns/op
BenchmarkBuildStringUsingStringBuffer-8                  1000000                42.56 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuffer-8      1000000                24.46 ns/op
BenchmarkBuildStringUsingStringBuilder-8                 1000000               125.4 ns/op
BenchmarkBuildStringUsingPreallocatedStringBuilder-8     1000000                22.31 ns/op
PASS
ok      string-builders-2       6509.271s
Poznámka: poslední benchmark běžel téměř dvě hodiny!

17. Grafická vizualizace výsledků benchmarků

Opět se podívejme na grafickou vizualizaci výsledků vybraných benchmarků. Na prvním grafu je patrné, že i pro řetězce o relativně malé délce nemá příliš význam uvažovat o použití operátoru +, protože se z časového hlediska jedná o velmi pomalou operaci:

Obrázek 7: Grafická vizualizace výsledků benchmarků pro N=1 až 10000, přepočteno na čas pro přidání jediného znaku.

Zajímavější a užitečnější bude porovnání ostatních čtyř přístupů pro větší hodnoty N. Ze zobrazených výsledků můžeme vyčíst, že použití objektu strings.Builder má význam především tehdy, pokud dokážeme dopředu odhadnout délku výsledného řetězce a tudíž můžeme provést předalokaci paměti. Bez této předalokace se zdá být výhodnější využít objekt bytes.Buffer:

Obrázek 8: Průměrně časy připojení jednoho kratšího řetězce (sto znaků) ke konstruovanému řetězci. Klasická konkatenace není zobrazena vzhledem ke značně odlišným (mnohem delším) časům.

18. Kde funkce pro konstrukci řetězce stráví nejvíce času?

Podrobnější pohled na operace prováděné při konstrukci dlouhého řetězce různými způsoby nám opět zpřístupní profiler, který dokáže vypsat kumulativní časy strávené v jednotlivých funkcích ve standardní knihovně a/nebo v samotném runtime programovacího jazyka Go:

Obrázek 9: Konkatenace řetězců; nejvíce času se stráví v přesunech znaků mezi různými verzemi konstruovaného řetězce. Zbytek času zabere činnosti správce paměti (GC).

Obrázek 10: Použití strings.Builder s předalokovanou pamětí. Nejvíce času zabere převod na řetězec, který vyžaduje paměťový přesun (paradoxně se více času stráví v kontrole výsledků, než samotnou konstrukcí řetězce).

bitcoin školení listopad 24

Obrázek 11: Použití bytes.Buffer s předalokovanou pamětí. Nejvíce času zaberou převody malých řetězců na sekvenci bajtů..

19. Repositář s demonstračními příklady

Zdrojové kódy všech demonstračních příkladů, na nichž byly popsány vybrané optimalizační techniky jazyka Go, byly uloženy do nového Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář, můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:

# Příklad/soubor Stručný popis Cesta
1 maps/map1_test.go benchmark pro mapy, jejichž klíče jsou typu UUID a hodnoty jsou časovými razítky https://github.com/tisnik/go-root/blob/master/article98/map­s/map1_test.go
2 maps/map2_test.go benchmark pro mapy, jejichž klíče jsou typu int a i hodnoty jsou stejného typu https://github.com/tisnik/go-root/blob/master/article98/map­s/map2_test.go
3 maps/map3_test.go benchmark pro mapy, jejichž prvky jsou prázdnými strukturami https://github.com/tisnik/go-root/blob/master/article98/map­s/map3_test.go
4 maps/map4_test.go benchmark pro mapy, jejichž klíče mají velikost 100 bajtů https://github.com/tisnik/go-root/blob/master/article98/map­s/map4_test.go
5 maps/go.mod projektový soubor s definicí modulu https://github.com/tisnik/go-root/blob/master/article98/map­s/go.mod
6 maps/go.sum seznam všech přímých i nepřímých závislostí https://github.com/tisnik/go-root/blob/master/article98/map­s/go.sum
       
7 map_or_slice/map_or_slice_test.go benchmark porovnávající použití řezů a map pro podobné operace vyhledání hodnoty https://github.com/tisnik/go-root/blob/master/article98/map_or_sli­ce/map_or_slice_test.go
8 map_or_slice/go.mod projektový soubor s definicí modulu https://github.com/tisnik/go-root/blob/master/article98/map_or_sli­ce/go.mod
9 map_or_slice/go.sum seznam všech přímých i nepřímých závislostí https://github.com/tisnik/go-root/blob/master/article98/map_or_sli­ce/go.sum
       
10 sets/map_or_slice_test.go benchmark: je lepší použít mapu nebo řez pro implementaci množiny? https://github.com/tisnik/go-root/blob/master/article98/set­s/map_or_slice_test.go
11 sets/go.mod projektový soubor s definicí modulu https://github.com/tisnik/go-root/blob/master/article98/set­s/go.mod
       
12 parameter_value_reference/main.go předávání rozsáhlých parametrů: hlavní program https://github.com/tisnik/go-root/blob/master/article98/pa­rameter_value_reference/ma­in.go
13 parameter_value_reference/config.toml konfigurační soubor, který je načítán programem https://github.com/tisnik/go-root/blob/master/article98/pa­rameter_value_reference/con­fig.toml
14 parameter_value_reference/go.mod projektový soubor s definicí modulu https://github.com/tisnik/go-root/blob/master/article98/pa­rameter_value_reference/go­.mod
15 parameter_value_reference/go.sum seznam všech přímých i nepřímých závislostí https://github.com/tisnik/go-root/blob/master/article98/pa­rameter_value_reference/go­.sum
16 parameter_value_reference/con­f/config.go definice datové struktury s načítanou konfigurací https://github.com/tisnik/go-root/blob/master/article98/pa­rameter_value_reference/con­f/config.go
17 parameter_value_reference/con­f/config.toml konfigurační soubor, který je načítán benchmarkem https://github.com/tisnik/go-root/blob/master/article98/pa­rameter_value_reference/con­f/config.toml
18 parameter_value_reference/con­f/config_benchmark_test.go benchmark: předávání velké struktury hodnotou nebo referencí? https://github.com/tisnik/go-root/blob/master/article98/pa­rameter_value_reference/con­f/config_benchmark_test.go
       
19 config_to_asm.go soubor, jehož překlad do assembleru se použil v dnešním článku https://github.com/tisnik/go-root/blob/master/article98/con­fig_to_asm.go
       
20 pass_by_value1.go předání řezu hodnotou, změna řezu uvnitř funkce https://github.com/tisnik/go-root/blob/master/article99/pas­s_by_value1.go
21 pass_by_value2.go předání struktury obsahující řez hodnotou https://github.com/tisnik/go-root/blob/master/article99/pas­s_by_value2.go
22 pass_by_value3.go předání struktury obsahující pole hodnotou https://github.com/tisnik/go-root/blob/master/article99/pas­s_by_value3.go
23 parameters_benchmark1/para­meters_test.go benchmark: předání řezu a pole do funkce, která řez/pole modifikuje https://github.com/tisnik/go-root/blob/master/article99/pa­rameters_benchmark1/parame­ters_test.go
24 parameters_benchmark1/go.mod projektový soubor s definicí modulu https://github.com/tisnik/go-root/blob/master/article99/pa­rameters_benchmark1/go.mod
25 parameters_benchmark2/para­meters_test.go benchmark: předání řezu a pole do funkce, která vrací prvek z pole https://github.com/tisnik/go-root/blob/master/article99/pa­rameters_benchmark2/parame­ters_test.go
26 parameters_benchmark2/go.mod projektový soubor s definicí modulu https://github.com/tisnik/go-root/blob/master/article99/pa­rameters_benchmark2/go.mod
27 range-val-copy-1/range_val_copy_test.go iterace přes všechny prvky pole https://github.com/tisnik/go-root/blob/master/article99/range-val-copy-1/range_val_copy_test.go
28 range-val-copy-1/go.mod projektový soubor s definicí modulu https://github.com/tisnik/go-root/blob/master/article99/range-val-copy-1/go.mod
29 range-val-copy-2/range_val_copy_test.go iterace přes všechny prvky mapy https://github.com/tisnik/go-root/blob/master/article99/range-val-copy-2/range_val_copy_test.go
30 range-val-copy-2/go.mod projektový soubor s definicí modulu https://github.com/tisnik/go-root/blob/master/article99/range-val-copy-2/go.mod
31 mutex_channel/mutex_channel_test.go synchronizace přes mutex nebo kanál https://github.com/tisnik/go-root/blob/master/article99/mu­tex_channel/mutex_channel_tes­t.go
32 mutex_channel/go.mod projektový soubor s definicí modulu https://github.com/tisnik/go-root/blob/master/article99/mu­tex_channel/go.mod
       
33 concatenation.go postupné skládání řetězce ze znaků s využitím operátoru konkatenace https://github.com/tisnik/go-root/blob/master/article_A1/string-builders/concatenation.go
34 string_buffer.go skládání znaků do bufferu s následným převodem bufferu na řetězec https://github.com/tisnik/go-root/blob/master/article_A1/string-builders/string_buffer.go
35 string_builder.go skládání znaků, využití objektu strings.Builder https://github.com/tisnik/go-root/blob/master/article_A1/string-builders/string_builder.go
36 preallocated_string_buffer.go předalokace paměti pro řetězec: varianta s bufferem https://github.com/tisnik/go-root/blob/master/article_A1/string-builders/preallocated_string_buffer.go
37 preallocated_string_builder.go předalokace paměti pro řetězec: varianta s objektem strings.Builder https://github.com/tisnik/go-root/blob/master/article_A1/string-builders/preallocated_strin­g_builder.go
38 main.go otestování jednotlivé varianty postupného vytváření řetězců ze znaků https://github.com/tisnik/go-root/blob/master/article_A1/string-builders/main.go
39 string_builders_test.go benchmarky porovnávající jednotlivé varianty postupného vytváření řetězců přidáváním znaků https://github.com/tisnik/go-root/blob/master/article_A1/string-builders/string_builders_test.go
       
40 concatenation.go postupné skládání řetězce z řetězců s využitím operátoru konkatenace https://github.com/tisnik/go-root/blob/master/article_A1/string-builders-2/concatenation.go
41 string_buffer.go skládání řetězců do bufferu s následným převodem bufferu na řetězec https://github.com/tisnik/go-root/blob/master/article_A1/string-builders-2/string_buffer.go
42 string_builder.go skládání řetězců, využití objektu strings.Builder https://github.com/tisnik/go-root/blob/master/article_A1/string-builders-2/string_builder.go
43 preallocated_string_buffer.go předalokace paměti pro řetězec: varianta s bufferem https://github.com/tisnik/go-root/blob/master/article_A1/string-builders-2/preallocated_string_buffer.go
44 preallocated_string_builder.go předalokace paměti pro řetězec: varianta s objektem strings.Builder https://github.com/tisnik/go-root/blob/master/article_A1/string-builders-2/preallocated_string_builder.go
45 main.go otestování jednotlivé varianty postupného vytváření řetězců z kratších řetězců https://github.com/tisnik/go-root/blob/master/article_A1/string-builders-2/main.go
46 string_builders_test.go benchmarky porovnávající jednotlivé varianty postupného vytváření řetězců přidáváním kratších řetězců https://github.com/tisnik/go-root/blob/master/article_A1/string-builders-2/string_builders_test.go

20. Odkazy na Internetu

  1. An Introduction to Benchmarking Your Go Programs
    https://tutorialedge.net/go­lang/benchmarking-your-go-programs/
  2. How the Go runtime implements maps efficiently (without generics)
    https://dave.cheney.net/2018/05/29/how-the-go-runtime-implements-maps-efficiently-without-generics
  3. Go18DS (Go 1.18+ Data Structures)
    https://github.com/daichi-m/go18ds
  4. TreeMap v2
    https://github.com/igrmk/treemap
  5. Go Data Structures: Binary Search Tree
    https://flaviocopes.com/golang-data-structure-binary-search-tree/
  6. Generics in Go
    https://bitfieldconsultin­g.com/golang/generics
  7. Tutorial: Getting started with generics
    https://go.dev/doc/tutorial/generics
  8. Gobs of data
    https://blog.golang.org/gobs-of-data
  9. Performance at Scale: MinIO Pushes Past 1.4 terabits per second with 256 NVMe Drives
    https://blog.min.io/performance-at-scale-minio-pushes-past-1–3-terabits-per-second-with-256-nvme-drives/
  10. Benchmarking MinIO vs. AWS S3 for Apache Spark
    https://blog.min.io/benchmarking-apache-spark-vs-aws-s3/
  11. Know Go: Generics (Kniha)
    https://bitfieldconsultin­g.com/books/generics
  12. Go 1.18 Generics based slice package
    https://golangexample.com/go-1–18-generics-based-slice-package/
  13. Highly extensible Go source code linter providing checks currently missing from other linters
    https://github.com/go-critic/go-critic
  14. Fast linters runner for Go
    https://github.com/golangci/golangci-lint
  15. Checkers from the “performance” group
    https://go-critic.com/overview#checkers-from-the-performance-group
  16. rangeValCopy
    https://go-critic.com/overview#rangeValCopy-ref
  17. C vs Rust vs Go: performance analysis
    https://medium.com/@marek.michalik/c-vs-rust-vs-go-performance-analysis-945ab749056c
  18. Golang Performance Comparison | Why is GO Fast?
    https://www.golinuxcloud.com/golang-performance/
  19. Go mutex vs channels benchmark
    https://github.com/danil/go_mu­tex_vs_channels_benchmark
  20. Techniques to Maximize Your Go Application’s Performance
    https://golangdocs.com/techniques-to-maximize-your-go-applications-performance
  21. Go language performance optimization
    https://www.programmerall­.com/article/8929467838/
  22. Ultimate Golang Performance Optimization Guide
    https://www.bacancytechno­logy.com/blog/golang-performance
  23. Optimizing a Golang service to reduce over 40% CPU
    https://medium.com/coralogix-engineering/optimizing-a-golang-service-to-reduce-over-40-cpu-366b67c67ef9
  24. Tutorial for optimizing golang program
    https://github.com/caibirdme/hand-to-hand-optimize-go/blob/master/README.md
  25. How to optimise your Go code
    https://codeburst.io/how-to-optimise-your-go-code-c6b27d4f1452
  26. Datový typ bytes.Buffer
    https://pkg.go.dev/bytes@go1­.20.1#Buffer
  27. Datový typ strings.Builder
    https://pkg.go.dev/strings@go1­.20.1#Builder

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.