«N'importe qui peut résoudre le paradoxe gris avec des dauphins, et vous essayez de le faire sans dauphins. "

En fait, j'avais prĂ©vu de passer le week-end d'une maniĂšre lĂ©gĂšrement diffĂ©rente, d'aller Ă Copter Huck (pas que j'Ă©tais un fan de copters, juste pour voir ce que les jeunes inventaient, pour passer du temps comme ça), mais la sĆur aĂźnĂ©e Ă©tait catĂ©goriquement contre. Bien sĂ»r, j'ai insistĂ© (c'est-Ă -dire que j'ai ri deux fois et dit: "Eh bien, peut-ĂȘtre ... ce sera amusant, de toute façon"), mais elle Ă©tait implacable, et quand ma femme a pris son parti, il n'y avait aucune chance de voyage. Bon, d'accord, "je n'en voulais pas vraiment", mais je me suis assis un peu sur un casse-tĂȘte drĂŽle du domaine de la programmation, que j'ai moi-mĂȘme inventĂ©, dont je fais rapport.
(Remarque nécessaire - le week-end précédent était destiné, c'est toujours comme ça - écrire un programme nécessite quelques heures, rédiger un rapport à ce sujet et cinq jours de voyage dans les transports publics ne sont pas terminés.)
Dans un article rĂ©cent, l'auteur a abordĂ© le problĂšme de l'accĂ©lĂ©ration (entre autres) du calcul des courbes de BĂ©zier (KB) sur MK avec des paramĂštres relativement faibles. Eh bien, en fait, ces paramĂštres sont au niveau de l'ordinateur central moyen des annĂ©es 70, mais Ă l'heure actuelle, ils sont considĂ©rĂ©s comme clairement insuffisants. Ă la suite de certaines actions, l'auteur a rĂ©ussi Ă accĂ©lĂ©rer quelque peu les calculs, Ă mon avis, n'est clairement pas suffisant, j'ai donc dĂ©cidĂ© d'Ă©crire comment cela devrait ĂȘtre fait en premiĂšre approximation. Je connais parfaitement la recette universelle pour rĂ©soudre les problĂšmes de vitesse - prendre MK avec une frĂ©quence plus Ă©levĂ©e ou passer Ă une autre famille, mais je viens de l'Ă©poque oĂč nous avons appris Ă nous dĂ©brouiller avec ce que nous avons, simplement parce qu'il n'y avait rien d'autre, du mot du tout. Ă l'heure actuelle, l'approche est dĂ©passĂ©e, mais il me semblait qu'elle ne serait pas sans intĂ©rĂȘt pour les lecteurs modernes de Habr.
Nous formulons le problĂšme - nous voulons calculer le plus rapidement possible les coordonnĂ©es des points de la courbe de BĂ©zier dĂ©finis par les points extrĂȘmes A et B et le foyer imaginaire C. La formule de calcul du point P sur la courbe est donnĂ©e par
oĂč T varie de 0 Ă 1 inclus. (Sur Wiki, ils Ă©crivent que cette formule Ă©tait secrĂšte Ă un moment donnĂ©, c'Ă©tait Ă©trange comme ça, mais tout est possible). Il est clair que nous ne le prendrons pas sous une forme complexe, au lieu de cela, nous rechercherons sĂ©parĂ©ment les coordonnĂ©es X et Y. Nous allons estimer la complexitĂ© du calcul en utilisant cette formule, simplement en comptant le nombre de signes d'opĂ©rations arithmĂ©tiques dans cette expression - 7 multiplications et 5 additions (=> 7 * 5 + ) Il est possible qu'un bon compilateur (et maintenant tous les compilateurs sont bons et optimisent parfaitement si vous ne les interdisez pas explicitement) rĂ©duira les coĂ»ts Ă 7 * 3 +, bien qu'il serait prĂ©fĂ©rable de l'aider en calculant (1-T) Ă l'avance. De maniĂšre gĂ©nĂ©rale, un bon compilateur peut gĂ©nĂ©ralement faire des merveilles si toutes les valeurs de la formule sont reprĂ©sentĂ©es par des constantes, mais nous supposons que toutes les valeurs sont statiquement indĂ©finies.
PremiÚre partie, mathématiques
Nous commençons le processus d'optimisation, pour lequel nous dĂ©veloppons les crochets et regroupons les termes en T (peut-ĂȘtre qu'un jour le compilateur pourra le faire pour nous, mais jusqu'Ă prĂ©sent cette partie du travail est affectĂ©e Ă l'intelligence naturelle), obtenant
=> 5 * 5 +, ce qui est clairement meilleur que la valeur initiale de 7 * 5 +, mais une amĂ©lioration relative de 7 * 3 + devrait encore ĂȘtre envisagĂ©e.
Si nous prenons le temps d'exĂ©cution de l'opĂ©ration d'addition comme un, le temps de multiplication ne sera exactement pas infĂ©rieur Ă un, en rĂšgle gĂ©nĂ©rale, plus, mais combien cela dĂ©pend de la mise en Ćuvre de MK (j'ai Ă©crit d'abord sur l'architecture, mais ce n'est pas entiĂšrement vrai). Lorsqu'il n'y a pas de multiplicateur matĂ©riel sur le cristal, le temps d'exĂ©cution de la multiplication sera dix (30+) fois supĂ©rieur Ă un, et lorsqu'il est prĂ©sent, il sera plusieurs fois (1-6). Par consĂ©quent, nous pouvons croire avec confiance que le remplacement de la multiplication par l'addition donne presque toujours un gain (et souvent significatif) de temps d'exĂ©cution. Eh bien, nous remarquerons immĂ©diatement que la transition des nombres Ă virgule fixe Ă un point flottant (nous laissons de cĂŽtĂ© la preuve de ce fait) conduit Ă une augmentation du temps d'exĂ©cution de plus de 20 fois pour l'addition (l'alignement est trĂšs influent ici), mais seulement Ă une lĂ©gĂšre augmentation pour la multiplication . Par consĂ©quent, pour les nombres Ă virgule flottante, les temps d'addition et de multiplication diffĂšrent peu, en particulier en termes relatifs (nous pouvons nous attendre Ă un maximum de 2 fois), mais ils diffĂšrent toujours et ne sont pas en faveur de la multiplication, donc il y a un gain ici.
En revenant au paragraphe prĂ©cĂ©dent, nous constatons que pour PT, la note 5 * 5 + ne devrait pas avoir un avantage significatif sur 7 * 3 +, mais nous avons encore des rĂ©serves. Faites attention au fait que nous devons calculer l'ensemble des points sur la courbe de BĂ©zier lorsque le paramĂštre T change, et tous les autres paramĂštres de la courbe sont fixes (mais pas constants, mais dĂ©solĂ©), alors le reste de la formule peut ĂȘtre calculĂ© Ă l'avance et obtenir
=> 3 * 2 +, oĂč
et
déjà bon, mais si vous vous souvenez du plan de Horner et écrivez
=> 2 * 2 +, alors par rapport à la décision «sur le front» nous devons gagner plus de 2 fois, presque 3, et ces optimisations sont tout à fait évidentes.
VĂ©rifions la thĂ©orie avec la pratique (bien que cela soit complĂštement redondant, nous sommes confiants dans nos estimations, mais soudain j'ai sous-estimĂ© le compilateur), pour lequel nous devons mesurer le temps rĂ©el d'exĂ©cution de diffĂ©rentes options sur du matĂ©riel rĂ©el. Eh bien, il se trouve que chez moi, j'ai beaucoup de toutes sortes de cartes de dĂ©bogage pour MK de diverses sociĂ©tĂ©s (y compris des raretĂ©s comme les dĂ©bogages de Luminary Micro ou Intel Edisson, essayez d'en acheter une maintenant), mais il n'y a pas une seule carte Arduino («Eh bien nous n'avons pas d'ananas »). Cela semblerait ĂȘtre une impasse, mais il existe des options - un site trĂšs intĂ©ressant tinkercad.com vient Ă notre aide, sur lequel vous pouvez construire votre circuit sur une planche Ă pain en utilisant le module Arduino, Ă©crire un croquis et l'exĂ©cuter immĂ©diatement. Dans le mĂȘme temps, vous pouvez dĂ©finir des points d'arrĂȘt, exĂ©cuter le programme Ă©tape par Ă©tape et mĂȘme (une chose sans prĂ©cĂ©dent pour un vrai Arduino) afficher les valeurs des variables aux points d'arrĂȘt.
Nous nous tournons vers ce site et commençons à mesurer. Pour commencer, nous vérifions nos hypothÚses sur le temps d'exécution des opérations et, aprÚs avoir éliminé les circonstances environnantes, nous obtenons les données suivantes pour les nombres entiers:
8 + 8 => 8 - 1 temps, 16 + 16 => 16 - 2,
8 * 8 => 16 - 2, 16 * 16 => 16 - 14 (la seule chose qui s'est avérée inattendue, j'ai pensé obtenir 4 * 2 + 4 * 2 = 16, il y a des optimisations intéressantes),
8/8 => 8 - 230, 16/16 => 16 - 230.
Faites attention aux deux derniers chiffres, il est clair d'aprÚs eux que l'opération de division est interdite si nous voulons vraiment compter rapidement. Maintenant (enfin) nous mesurons le temps nécessaire pour effectuer des opérations sur le nombre de PT avec une mantisse de 24 bits
a + b - 126 (et dépend fortement des opérandes), a * b - 140, a / b - 482.
Les données obtenues correspondent bien à nos hypothÚses théoriques, il est clair qu'il y a une implémentation matérielle à bord de ce MK: pour la multiplication, pour la division, pas pour les opérations, PT.
Maintenant, nous commençons à mesurer le temps de calcul complet. Nous fixons les valeurs A = 140, B = 120, C = 70 et construisons 170 points uniformément répartis sur le bureau d'études. Pourquoi précisément ces valeurs - elles ont été données dans le message spécifié lors de l'évaluation des performances. Voici les algorithmes et le temps d'exécution du test correspondant.
Formule (1) => 20 ms ou 1 900 cycles d'horloge par échantillon
Formule (1) => 18 ms ou 1660 cycles d'horloge par échantillon (considérer séparément 1-T)
Formule (2) => 16 ms ou 1540 cycles d'horloge par échantillon
Formule (3) => 10 ms ou 923 cycles d'horloge par échantillon
Formule (4) => 8 ms ou 762 mesures par comptage
On peut voir que la réduction du temps d'exécution qui en résulte (de 20 ms à 8 ms) correspond bien à celle attendue et nous avons pu accélérer les calculs de plus de 2 fois. A noter qu'en plus de considérations tout à fait évidentes et mathématiques, ne dépassant pas le cours du lycée, nous n'en avions pas besoin.
Et maintenant, parlons de quoi faire si le rĂ©sultat n'est pas suffisant, et nous avons dĂ©jĂ tout Ă©liminĂ© des formules de calcul. Ils m'ont Ă©crit ici (dans les commentaires d'un autre article) qu'en gĂ©nĂ©ral tout problĂšme peut ĂȘtre rĂ©duit Ă l'informatique avec FT et, malgrĂ© la controverse Ă©vidente de l'hypothĂšse (essayez de le faire pour la solution numĂ©rique des Ă©quations de Navier-Stokes), dans ce cas particulier cette recommandation est applicable Bien que, comme toujours, il existe des nuances.
DeuxiĂšme partie, Informatique
Une fois les modifications de l'algorithme Ă©puisĂ©es, seules les structures de donnĂ©es restent et nous entrons dans le sol des nombres Ă virgule fixe. Ici, nous trouverons de nombreux piĂšges auxquels nous n'avons pas pensĂ© pour la plage et la prĂ©cision du PT (en gĂ©nĂ©ral, pour le PT, il faut penser Ă ces problĂšmes, mais ici tout est plus simple, beaucoup a Ă©tĂ© fait pour nous). Il est nĂ©cessaire de mener une petite Ă©tude du problĂšme pour dĂ©terminer la reprĂ©sentation nĂ©cessaire de FT (sĂ©lectionnĂ©e dans le poste 9.7 susmentionnĂ©, Ă en juger par les rĂ©sultats, elle est clairement insuffisante), mais je propose de prendre un chemin lĂ©gĂšrement diffĂ©rent. Soit dit en passant, si nous ne faisons pas 170 pas sur l'intervalle, mais 128 (je ne vois aucune raison de nous interdire cette Ă©tape), cette idĂ©e nous conviendrait parfaitement. Si nous prenons en compte le fait que les constantes dĂ©finissant la KB sont donnĂ©es par des entiers, et que le seul paramĂštre T peut ĂȘtre reprĂ©sentĂ© par une fraction de la forme et / et nous utiliserons le rĂ©sultat pour le rendu Ă l'Ă©cran, c'est-Ă -dire qu'il se traduira en coordonnĂ©es entiĂšres, alors nous pouvons il suffit de tout faire en nombres entiers, qui traitent beaucoup plus rapidement.
Nous utilisons uniquement la derniÚre formule et la réécrivons dans la nouvelle notation
(=> 2 * 2 + 2 /), oĂč A1 et B1 sont calculĂ©s de la mĂȘme maniĂšre que pour PT. De toute Ă©vidence, tous les nombres sont des nombres entiers et les opĂ©rations correspondantes doivent ĂȘtre effectuĂ©es beaucoup plus rapidement. Afin de ne pas perdre de prĂ©cision lors de l'opĂ©ration de division entiĂšre (2/3 = 1! = 1.5) et de faire la division au tout dernier moment, on transforme lĂ©gĂšrement la formule en la forme
(=> 4 * 2 + 2 /). Tous les nombres FT, donc nous implémentons cet algorithme et obtenons ... vous voici, grand-mÚre et le jour de Yuryev ... 1869 cycles, mais c'est bien pire que pour FT, nous sommes partis de cela, une sorte de poubelle, car les entiers sont beaucoup plus rapides.
Nous commençons le dĂ©briefing et il s'avĂšre que le simple changement de type de variables ne suffit pas. PremiĂšrement, nous devons utiliser des nombres non pas 8 ou mĂȘme 16, mais 32 bits, sinon un dĂ©bordement se produira et des nombres longs, bien que plus rapides que PT, mais pas autant que pour compenser les dĂ©fauts de l'algorithme. DeuxiĂšmement, ces dĂ©fauts sont dans nous avons de nouveau calculĂ© des constantes sur chaque mesure - nous les supprimons par un calcul prĂ©liminaire B2 = B1 * I, A2 = A * I * I. Ensuite, nous obtenons
(=> 2 * 2 + 2 /) avec un résultat de 1684 est meilleur que le précédent, mais nous ne nous en sommes pas encore éloignés.
On exclut le calcul d'une autre constante And2 = Et * Et on obtient
(=> 2 * 2 + 1 /), avec un temps d'exécution de 956 cycles - mais c'est intéressant, l'exclusion d'une opération a conduit à une augmentation significative de la productivité.
C'est ce qui nous ralentit - la division, car c'est une opĂ©ration qui prend beaucoup de temps, mais nous avons une astuce intĂ©ressante pour y faire face. Pour calculer l'expression 1 / Et nous pouvons effectuer des transformations Ă©lĂ©mentaires 1 / = 1 / * ( / ) = 1 * ( / ) / . Si nous choisissons le degrĂ© de deux comme H, alors la division par H peut ĂȘtre remplacĂ©e par des dĂ©calages, et si l'exposant est un multiple de 8, alors mĂȘme des dĂ©calages ne seront pas nĂ©cessaires. Et la valeur de N / A devra ĂȘtre calculĂ©e honnĂȘtement, mais une seule fois, aprĂšs quoi seule la multiplication restera dans le cycle de calcul.
Faites attention au fait que nous avons fait une conversion pas tout Ă fait correcte et remplacĂ© le N / A par sa valeur arrondie K pour passer aux opĂ©rations exclusivement avec des entiers. L'inexactitude consiste dans la perte de prĂ©cision et des recherches supplĂ©mentaires doivent ĂȘtre effectuĂ©es pour prouver l'applicabilitĂ© de cette approche Ă notre cas. Nous Ă©crivons H / I sous la forme (K * I + d) / I = K + (d / I), oĂč q est infĂ©rieur Ă I. Ensuite, l'erreur absolue en allant de H / I Ă K sera d / I, et l'erreur relative sera d / I I / (K + d / I)> = d / I / (K + 1) ~ d / I / K, Ă condition que K >> 1 (ce n'est pas un dĂ©calage). Il s'ensuit que la valeur de H doit ĂȘtre choisie aussi grande que possible, car l'erreur de calcul absolue est Ă©gale Ă A * d / I / K> = A * 1 / N / I. Si nous voulons que l'erreur ne soit pas supĂ©rieure Ă l'unitĂ©, nous devons rĂ©sister Ă la condition A / K <= 1, puis K> = A, nous convertissons K * I> = A * I, ce qui signifie H> = A * I, alors nous ne le faisons pas perdre en prĂ©cision. Dans notre cas, A <= 256 et I <= 256, nous obtenons H> = 2 ** 16, ce qui est tout Ă fait acceptable. Ăvidemment, dans les formules ci-dessus, les modules des nombres originaux doivent ĂȘtre utilisĂ©s.
Nous notons pour l'avenir que si nous arrondissons non pas vers le bas, mais vers l'entier le plus proche, alors les exigences sont quelque peu rĂ©duites et H devrait ĂȘtre suffisamment moitiĂ© moins, bien qu'il y ait des nuances.
Dans tous les cas, nous pouvons fournir la précision requise et obtenir l'algorithme suivant: H = 2 ** 16; K = [N / A] (I <256); 0 <= et <= AND;
(=> 4 * 2 + 2 >> 16) oĂč toutes les opĂ©rations sont effectuĂ©es sur des entiers longs. Nous implĂ©mentons cet algorithme et obtenons 583 cycles d'horloge ... mais cela est dĂ©jĂ proche de l'idĂ©al, mais pas encore idĂ©al.
Viennent ensuite les petits paramÚtres pour un MK spécifique - travailler avec des variables globales est plus rapide. qu'avec les locaux, mais encore plus rapide avec les registres locaux, ce qui entraßne une réduction du temps à 506 cycles d'horloge.
De plus, nous notons que la derniĂšre multiplication avant le dĂ©calage peut ĂȘtre effectuĂ©e avec des nombres de 16 bits, ce qui donnera 504 - une bagatelle, mais agrĂ©able.
Au total, nous avons accéléré les calculs par rapport à l'implémentation «front» en 1900/504 - plus de 3 fois, et nous n'avons pas du tout perdu le mot. C'est le résultat que j'appelle l'optimisation du temps, et non 20% reçu dans le message d'origine.
Est-il possible d'atteindre des indicateurs encore meilleurs - c'est possible, mais c'est le sujet du prochain billet.