Dobrý den, máte nějaké zkušenosti s výkonem Gonum na velkých datech? Má zkušenost s Go není v této oblasti nijak velká, ale porovnání při zpracování astronomických dat s C++ nedopadlo pro Go dobře (při implementaci výpočtů přímo), tak bych rád věděl, jestli je kód Gonum nějak optimalizovaný (konkrétně třeba použitím asembleru s vektorovými instrukcemi, Go má ostatně vlastní, poměrně specifický asembler).
Chystám nějaké benchmarky, ale (prozatím) ne vůči C++, ale spíš Fortranu (FFT, Lapack) a jazyku Julia. Zpět k otázce - co jsem se díval na zdrojáky, tak implementace Gonum je dost přímočará, žádné velké optimalizace tam vidět nejsou, už vůbec ne na úrovni assembleru.
Ten kód, který jste zkoušel v Go, byl nějak upravený alespoň pro použití gorutin? Nebo to byl jednovláknový výpočet (tam je C/C++ překladač většinou lepší i bez vektorových intrinsic)
(ale připomnělo mi to, že o Go asm mám něco napsat :-)
> ... máte nějaké zkušenosti s výkonem Gonum na velkých datech?
Jaká je Vaše definice "velkých dat"? Musíte zpracovávat data najednou nebo přicházejí v nějakém proudu? Také mějte na paměti, že náš mat balíček je (alespoň v současnosti) zaměřen na výpočty s hustými maticemi, kde se s jejich velikostí moc vysoko nedostanete.
Můžete ale zkusit jiný BLAS a LAPACK backend (balíček mat je interně využívá k vlastním výpočtům). Defaultně nabízíme kompletní implementaci BLAS v Go a částečnou implementaci LAPACK, kde implementujeme jen to, co potřebujeme (ruční re-implementace referenční 1-based column-major Fortraního kódu do 0-based row-major Go je gigantické množství práce). Je to samozřejmě pomalejší než C, ale nazávisíte na ne-Go kódu. Pokud Vám nevadí cgo, můžete použít jinou implementaci BLAS/LAPACK jako třeba OpenBLAS nebo Intel MKL: nastavte náležitě CGO parametry (viz https://github.com/gonum/netlib), nainstalujte balíček gonum.org/v1/netlib/lapack/netlib a ve svém kódu pak aktivujte:
package main
import (
"gonum.org/v1/gonum/lapack/lapack64"
"gonum.org/v1/netlib/lapack/netlib"
)
func main() {
lapack64.Use(netlib.Implementation{})
// Nyní mat a lapack64 budou používat externí LAPACK implementaci.
}
Vše má ale svou cenu. V tomto případě opouštíte území bezpečného Go a statických binárek a kráčíte do země cgo, C, asembleru a závislosti na externí knihovně. Volání do LAPACK půjdou přes LAPACKE, což je open source knihovna od Intelu, která převádí matice z row-major formátu (používaným v Gonum) do column-major formátu (používaným Fortranovským LAPACK rozhraním). LAPACKE není dokumentována, neobsahuje žádné testy (Gonum naštěstí testuje zevrubně) a při vývoji Gonum jsme v ní vychytali množství neuvěřitelných chyb, které ukazují, že jsme byli nejspíš první, kdo některé funkce s row-major maticemi vůbec kdy volal. Možná to takhle zní příliš dramaticky, ale je třeba to mít na paměti.
> sice pár funkcí je přepsáno do asm (internal/asm/f64 atd.), ale zatím jsem tam našel jen několik
> rozbalených smyček, nic "vektorového" typu AVX, jen několik instrukcí z SSE ("paralelní" ADD například).
Asembleru se nebráníme, ale je to samozřejmě neuvěřitelné množství práce, které by vydalo na samostatný projekt - samotná implementace není snadná (nejen amd64, ale zájem je zdá se i o arm64; různé procesory podporují různé sady instrukcí, atd.), testování, údžba ... a je nás pár dobrovolníků, kteří projektu věnují svůj volný čas. V internal/asm/f64 pomalu přidáváme nízko vysící ovoce, tak abychom ho mohli používat jak v blas/gonum tak i ve floats. Nyní by bylo nejužitečnější mít implementaci GEMM aspoň pro amd64, ale i to není nic snadného. Komu současná rychlost Gonum v Go nestačí, musí linkovat přes cgo externí implementaci BLAS a LAPACK, viz výše.
Jsme vděčni za každý pull request a kdo chce nějakým kódem přispět, tak ho rádi zkontrolujeme.
Ten balíček vypadá pěkně, díky za link. Můžu o něm něco později napsat? A o kolik je to pomalejší vůči C nebo nativnímu FORTRANu? Jestli pár %, tak to se určitě dá zkousnout.
Jinak my se tváříme, že děláme "velká data", ale je to relativně nepatrné. 30000 nových záznamů za den, každý omezený řekněme na 50 MB max, ale průměr je o hodně níž.
Balíček "gonum.org/v1/netlib/lapack/netlib" nedělá nic výpočetně náročného, jen kontroluje vstupní parametry a převádí jejich typy na cgo ekvivalenty a pak volá odpovídající funkci z LAPACKE (přes automaticky generovaný balíček "gonum.org/v1/netlib/lapack/lapacke"). Takže overhead bude oproti LAPACKE naprosto zanedbatelný. LAPACKE sám pak má overhead v alokování matic, jejich převodu z row-major do column-major a pak vypočítaného výsledku zase zpět do row-major.
Článek určitě napiště, já myslím, že možnosti 1) změnit výpočetní backend v "gonum.org/v1/gonum/mat" a 2) volat libovolnou funkci v LAPACKu z Go (i když přesněji řečeno jen ty funkce, které poskytuje LAPACKE, ale to jsou všechny ty užitečné a většina pomocných) nejsou moc známy.
Děkuji za odpověď. Ten kód psal kolega a byl jednovláknový (“jednokorutinní”). Je vidět, že překladače pro C++ jsou za ta léta hodně efektivní. Já tam pak na zkoušku dopisoval v tom jejich pseudoasembleru instrukce pro AVX-512, ale moc se to neprojevilo (nejspíš kvůli pomalé paměti), ono to počítání bylo celkem primitivní.
P.S. O Go asm určitě napište, nejlépe (i) o ARM64, s tím jsem se natrápil až až a rád si přečtu o zkušenostech někoho jiného.
Tady jeden z autorů Gonum. Díky, že o projektu píšete, je to poprvé, co vidím o Gonum článek nebo vůbec zmínku v češtině.
Jen několik komentářů k článku.
> Následující dvě funkce nazvané Reverse a Scale změní obsah původního řezu. Nejedná se
> tedy o funkce v matematickém významu (což je zvláštní, protože toto chování neodpovídá pravidlům,
> které knihovna Gonum v jiných balíčcích dodržuje).
Toto chování je motivováno naší obecnou snahou redukovat alokaci paměti a tak i tlak na garbage collector. Kdyby Reverse a Scale byly funkce, museli bychom v nich alokovat, takto je rozhodnutí na uživateli. A pokud nechce vstupní slice přepsat, má možnost použít ScaleTo, kde první parametr je cílový slice. V tomto jsme myslím poměrně konzistentní: v Gonum se buď modifikuje receiver (použito především v mat, ve floats není žádný receiver, na který bychom mohli třeba Scale pověsit) nebo první parametr (a funkce/metoda pak má příponu To).
> Operace součtu dvou vektorů realizovaná metodou – modifikuje se v ní příjemce (receiver)
Kód
v := mat.NewVecDense(5, nil) v.AddVec(v1, v2)
bychom standardně psali
var v mat.VecDense v.AddVec(v1, v2)
Receiver pro operace, kde záleží na tvaru matice, musí být buď nulový (a bude přetvarován) nebo musí mít přesný tvar. Motivace je, že je velice jednoduché zařídit, aby receiver byl nulový, stačí zavolat metodu Reset nebo použít nulovou hodnotu.
> Symetrické matice ... Chování tohoto konstruktoru je ovšem poněkud zvláštní – předat je mu totiž nutné všechny prvky
> odpovídající velikosti matice.
To má původ v tom, jak jsou symetrické matice ukládány v paměti LAPACKem (který je používán interně) a opět v tom, že se vyhýbáme alokaci paměti. Kdyby uživatel předával jen horní nebo dolní trojúhelník, museli bychom (kvůli LAPACKu) alokovat, kopírovat prvky matice a tím pádem by navíc změny ve vytvořené matici nepromítly to předaného slicu. To samé platí pro trojúhelníkové matice.
> jazyk Go (ve verzi 1.x) nepodporuje přetěžování operátorů, takže například není možné
> implementovat maticové operace „přirozenou“ cestou
Já osobně tohle vnímám jako pozitivní vlastnost. Nutnost dělat vše explicitně vede ke kódu, ve kterém je jasnější, co se dělá a jak, a díky tomu i k vyšší pravděpodobnosti, že kód bude korektní. Zrovna numpy je dobrý příklad o rozdílném přístupu. Můj oblíbený příklad je numpy.dot: 5 naprosto rozdílných módů funkčnosti, mezi kterými se vybírá na základě runtime vlastností argumentů. Taková benevolence na vstupní data velice snadno skryje chybu. A zrovna numpy.dot učí uživatele špatné matematice: již několikrát jsme měli dotaz "chci násobit dvě matice pomocí mat.Dot ale program způsobuje panic".