La langue Go gagne en popularité. Tellement confiant qu'il y a de plus en plus de conférences, par exemple,
GolangConf , et la langue
est parmi les dix technologies les mieux payées. Par conséquent, il est déjà judicieux de parler de ses problèmes spécifiques, par exemple, les performances. En plus des problèmes communs à toutes les langues compilées, Go a la sienne. Ils sont associés à l'optimiseur, à la pile, au système de saisie et au modèle multitâche. Les moyens de les résoudre et les solutions de contournement sont parfois très spécifiques.
Daniel Podolsky , bien que l'évangéliste de Go, rencontre également beaucoup de choses étranges en lui. Tout ce qui est étrange et, surtout, intéressant, est collecté et testé, puis en parle dans HighLoad ++. La transcription du rapport comprendra des chiffres, des graphiques, des exemples de code, les résultats du profileur, une comparaison des performances des mêmes algorithmes dans différentes langues - et tout le reste, pour lequel nous détestons tellement le mot «optimisation». Il n'y aura aucune révélation dans la transcription - d'où viennent-ils dans un langage aussi simple - et tout ce qui peut être lu dans les journaux.
À propos des conférenciers. Daniil Podolsky : 26 ans d'expérience, 20 en opération, dont le leader du groupe, 5 ans de programmation sur Go.
Kirill Danshin : créateur de Gramework, Maintainer, Fast HTTP, Black Go-mage.
Le rapport a été préparé conjointement par Daniil Podolsky et Kirill Danshin, mais Daniel a fait un rapport et Cyril a aidé mentalement.Constructions linguistiques
Nous avons une norme de performance -
direct
. Il s'agit d'une fonction qui incrémente une variable et ne fait plus rien.
Le résultat de la fonction est de
1,46 ns par opération . Il s'agit de l'option minimale. Plus rapide que 1,5 ns par opération, ne fonctionnera probablement pas.
Reportez la façon dont nous l'aimons
Beaucoup savent et aiment utiliser la construction du langage différé. Très souvent, nous l'utilisons comme ça.
func BenchmarkDefer(b *testing.B) { for i := 0; i < bN; i++ { incDefer() } } func incDefer() { defer incDirect() }
Mais vous ne pouvez pas l'utiliser comme ça! Chaque report consomme 40 ns par opération.
// BenchmarkDirect-4 2000000000 1.46 / // defer BenchmarkDefer-4 30000000 40.70 /
Je pensais que c'était peut-être à cause de l'inline? Peut-être que l'inline est si rapide?
Direct est en ligne et la fonction de report ne peut pas être en ligne. Par conséquent, compilé une fonction de test séparée sans inline.
func BenchmarkDirectNoInline(b *testing.B) { for i := 0; i < bN; i++ { incDirectNoInline() } }
Rien n'a changé, le report a pris les mêmes 40 ns. Reportez-vous cher, mais pas catastrophique.
Lorsqu'une fonction prend moins de 100 ns, vous pouvez le faire sans différer.
Mais là où la fonction prend plus d'une microseconde, c'est tout de même - vous pouvez utiliser le report.
Passer un paramètre par référence
Considérez un mythe populaire.
func BenchmarkDirectByPointer(b *testing.B) { for i := 0; i < bN; i++ { incDirectByPointer(&testInt64) } } func incDirectByPointer(n *int64) { *n++ }
Rien n'a changé - rien ne vaut le coup.
// BenchmarkDirectByPointer-4 2000000000 1.47 / BenchmarkDeferByPointer-4 30000000 43.90 /
Sauf pour 3 ns par report, mais ceci est amorti pour les fluctuations.
Fonctions anonymes
Parfois, les débutants demandent: «Une fonction anonyme coûte-t-elle cher?»
func BenchmarkDirectAnonymous(b *testing.B) { for i := 0; i < bN; i++ { func() { testInt64++ }() } }
Une fonction anonyme n'est pas chère, elle prend 40,4 ns.
Interfaces
Il existe une interface et une structure qui le mettent en œuvre.
type testTypeInterface interface { Inc() } type testTypeStruct struct { n int64 } func (s *testTypeStruct) Inc() { s.n++ }
Il existe trois options pour utiliser la méthode d'incrémentation. Directement depuis Struct:
var testStruct = testTypeStruct{}
.
Depuis l'interface concrète correspondante:
var testInterface testTypeInterface = &testStruct
.
Avec conversion d'interface d'exécution:
var testInterfaceEmpty interface{} = &testStruct
.
Vous trouverez ci-dessous la conversion et l'utilisation de l'interface d'exécution directement.
func BenchmarkInterface(b *testing.B) { for i := 0; i < bN; i++ { testInterface.Inc() } } func BenchmarkInterfaceRuntime(b *testing.B) { for i := 0; i < bN; i++ { testInterfaceEmpty.(testTypeInterface).Inc() } }
L'interface, en tant que telle, ne coûte rien.
// BenchmarkStruct-4 2000000000 1.44 / BenchmarkInterface-4 2000000000 1.88 / BenchmarkInterfaceRuntime-4 200000000 9.23 /
La conversion de l'interface d'exécution en vaut la peine, mais pas chère - vous n'avez pas besoin de refuser spécifiquement. Mais essayez de vous en passer si possible.
Mythes:- Déréférencement - déréférencement des pointeurs - gratuit.
- Les fonctionnalités anonymes sont gratuites.
- Les interfaces sont gratuites.
- Conversion d'interface d'exécution - NON GRATUIT.
Basculer, mapper et découper
Chaque nouveau venu à Go demande ce qui se passe si vous remplacez le commutateur par une carte. Sera-ce plus rapide?
L'interrupteur est disponible en différentes tailles. J'ai testé sur trois tailles: petite pour 10 caisses, moyenne pour 100 caisses et grande pour 1000 caisses. Le commutateur pour 1000 cas se trouve dans le code de production réel. Bien sûr, personne ne les écrit avec ses mains. Il s'agit d'un code généré automatiquement, généralement un commutateur de type. Testé sur deux types: int et string. Il semblait que cela se révélerait plus clairement.
Petit interrupteur. L'option la plus rapide est le commutateur réel. La suite va immédiatement tranche, où l'index entier correspondant contient une référence à la fonction. La carte n'est pas un leader sur int ou chaîne.
Activer les chaînes est beaucoup plus lent que sur int. Si vous pouvez passer non pas à une chaîne, mais à un int, faites-le.
Interrupteur central. Switch lui-même règne toujours int, mais slice l'a un peu dépassé. La carte est toujours mauvaise. Mais sur une clé de chaîne, la carte est plus rapide que le commutateur - comme prévu.
Gros interrupteur. Un millier de cas témoignent de la victoire inconditionnelle de la carte dans la nomination «switch by string». Théoriquement, la tranche a gagné, mais en pratique je vous conseille d'utiliser le même switch ici. La carte est encore lente, même si la carte a des clés entières avec une fonction de hachage spéciale. En général, cette fonction ne fait rien. L’int lui-même a un hachage pour int.
Conclusions La carte n'est meilleure que sur de grandes quantités et non sur une condition entière. Je suis sûr que dans toutes les conditions, sauf int, il se comportera de la même manière que sur la chaîne. Slice dirige toujours lorsque les conditions sont entières. Utilisez-le si vous voulez «accélérer» votre programme de 2 ns.
Interaction inter-routine
Le sujet est complexe, j'ai effectué de nombreux tests et présenterai les plus révélateurs. Nous connaissons les
moyens d'interaction inter-agences suivants .
- Atomique Ce sont des moyens d'application limitée - vous pouvez remplacer le pointeur ou utiliser int.
- Mutex est largement utilisé depuis Java.
- La chaîne est unique à GO.
- Canal tamponné - canaux tamponnés.
Bien sûr, j'ai testé sur un nombre beaucoup plus important de goroutins qui se disputent une seule ressource. Mais il en a choisi trois à titre indicatif: un peu - 100, un moyen - 1000 et beaucoup - 10000.
Le profil de charge est différent . Parfois, tous les gorutins veulent écrire dans une variable, mais c'est rare. Habituellement, après tout, certains écrivent, certains lisent. Parmi les lecteurs, principalement - 90% lisent, parmi ceux qui écrivent - 90% écrivent.
C'est le code qui est utilisé pour que le goroutine qui dessert le canal puisse fournir à la fois la lecture et l'écriture d'une variable.
go func() { for { select { case n, ok := <-cw: if !ok { wgc.Done() return } testInt64 += n case cr <- testInt64: } } }()
Si un message nous parvient par le canal par lequel nous écrivons, nous l'exécutons. Si le canal est fermé, on termine le goroutin. À tout moment, nous sommes prêts à écrire sur le canal qui est utilisé par d'autres goroutines pour la lecture.
Ce sont des données pour un goroutine. Le test de canal est effectué sur deux goroutines: l'une traite le canal, l'autre écrit sur ce canal. Et ces options ont été testées sur un.
- Écriture directe dans une variable.
- Mutex prend un journal, écrit dans une variable et libère un journal.
- Atomic écrit dans une variable via Atomic. Ce n'est pas gratuit, mais toujours beaucoup moins cher que Mutex sur un garutin.
Avec une petite quantité de goroutine, l'Atomic est un moyen efficace et rapide de synchroniser, ce qui n'est pas surprenant. Direct n'est pas là, car nous avons besoin d'une synchronisation, qu'il ne fournit pas. Mais Atomic a des défauts, bien sûr.
La prochaine étape est Mutex. Je m'attendais à ce que Channel soit aussi rapide que Mutex, mais non.
Le canal est un ordre de grandeur plus cher que Mutex.
De plus, Channel et Channel tamponné sortent à peu près au même prix. Et il y a Channel, dans lequel le tampon ne déborde jamais. C'est un ordre de grandeur moins cher que celui dont le tampon déborde. Ce n'est que si le tampon de Channel n'est pas plein, qu'il en coûte à peu près le même prix par ordre de grandeur que Mutex. C'est ce que j'attendais du test.
Cette image avec la répartition de son coût est répétée sur n'importe quel profil de charge - à la fois sur MostlyRead et MostlyWrite. De plus, le canal MostlyRead complet coûte le même prix que le canal incomplet. Et le canal tamponné de MostlyWrite, dans lequel le tampon n'est pas plein, coûte le même prix que le reste. Je ne peux pas dire pourquoi il en est ainsi - je n'ai pas encore étudié cette question.
Passer des paramètres
Comment passer les paramètres plus rapidement - par référence ou par valeur? Voyons ça.
J'ai vérifié comme suit - fait des types imbriqués de 1 à 10.
type TP001 struct { I001 int64 } type TV002 struct { I001 int64 S001 TV001 I002 int64 S002 TV001 }
Le dixième type imbriqué aura 10 champs int64, et les types imbriqués de l'imbrication précédente seront également 10.
Il a ensuite écrit des fonctions qui créent un type d'imbrication.
func NewTP001() *TP001 { return &TP001{ I001: rand.Int63(), } } func NewTV002() TV002 { return TV002{ I001: rand.Int63(), S001: NewTV001(), I002: rand.Int63(), S002: NewTV001(), } }
Pour les tests, j'ai utilisé trois options du type: petit avec emboîtement 2, moyen avec emboîtement 3, grand avec emboîtement 5. J'ai dû mettre un très grand test avec emboîtement 10 la nuit, mais là, l'image est exactement la même que pour 5.
Dans les fonctions, le passage par valeur est au moins deux fois plus rapide que le passage par référence . Cela est dû au fait que le passage par la valeur ne charge pas l'analyse d'échappement. En conséquence, les variables que nous allouons sont sur la pile. C'est beaucoup moins cher pour l'exécution, pour le ramasse-miettes. Bien qu'il n'ait peut-être pas le temps de se connecter. Ces tests ont duré quelques secondes - le ramasse-miettes était probablement encore endormi.
Magie noire
Savez-vous ce que ce programme produira?
package main type A struct { a, b int32 } func main() { a := new(A) aa = 0 ab = 1 z := (*(*int64)(unsafe.Pointer(a))) fmt.Println(z) }
Le résultat du programme dépend de l'architecture sur laquelle il est exécuté. Sur le petit endian, par exemple, AMD64, le programme affiche
. Sur big endian, un. Le résultat est différent, car sur le petit endian, cette unité apparaît au milieu du nombre, et sur le gros endian - à la fin.
Il existe encore des processeurs dans le monde où des commutateurs endiens, par exemple, Power PC. Il sera nécessaire de savoir quel endian est configuré sur votre ordinateur au démarrage, avant de faire des inférences sur les astuces dangereuses de ce type. Par exemple, si vous écrivez un code Go qui sera exécuté sur un serveur multiprocesseur IBM.
J'ai cité ce code pour expliquer pourquoi je considère toute magie noire dangereuse. Vous n'avez pas besoin de l'utiliser. Mais Cyril pense que c'est nécessaire. Et voici pourquoi.
Il existe une fonction qui fait la même chose que GOB - Go Binary Marshaller. Ceci est Encoder, mais dangereux.
func encodeMut(data []uint64) (res []byte) { sz := len(data) * 8 dh := (*header)(unsafe.Pointer(&data)) rh := &header{ data: dh.data, len: sz, cap: sz, } res = *(*[]byte)(unsafe.Pointer(&rh)) return }
En fait, il prend un morceau de mémoire et en tire un tableau d'octets.
Ce n'est même pas une commande - ce sont deux commandes. Par conséquent, Cyril Danshin, lorsqu'il écrit un code performant, n'hésite pas à entrer dans les tripes de son programme et à le rendre dangereux.
Nous discuterons des caractéristiques plus spécifiques de Go le 7 octobre au GolangConf - une conférence pour ceux qui utilisent Go dans le développement professionnel et ceux qui considèrent ce langage comme une alternative. Daniil Podolsky n'est qu'un membre du comité du programme, si vous souhaitez discuter de cet article ou révéler des problèmes connexes - soumettez une demande de rapport.
Pour tout le reste, en ce qui concerne les hautes performances, bien sûr, HighLoad ++ . Nous acceptons également les candidatures. Inscrivez-vous à la newsletter et restez à jour avec les nouvelles de toutes nos conférences pour les développeurs Web.