Il y a 3 fonctions données ci-dessous qui trouvent le dernier mais le deuxième élément d'une liste. Celui qui utilise last . init
semble beaucoup plus rapide que les autres. Je n'arrive pas à comprendre pourquoi.
Pour les tests, j'ai utilisé une liste d'entrée de [1..100000000]
(100 millions). Le dernier s'exécute presque instantanément tandis que les autres prennent plusieurs secondes.
-- slow
myButLast :: [a] -> a
myButLast [x, y] = x
myButLast (x : xs) = myButLast xs
myButLast _ = error "List too short"
-- decent
myButLast' :: [a] -> a
myButLast' = (!! 1) . reverse
-- fast
myButLast'' :: [a] -> a
myButLast'' = last . init
init
a été optimisé pour éviter de "déballer" la liste plusieurs fois.myButLast
beaucoup plus lent?. Il semble que cela ne déballe aucune liste, mais la traverse simplement comme uneinit
fonction le fait ...[x, y]
abréviation de(x:(y:[]))
, donc il décompresse le contre externe, un deuxième contre, et vérifie si la queue du second l'cons
est[]
. De plus, la deuxième clause déballera à nouveau la liste dans(x:xs)
. Oui, le déballage est raisonnablement efficace, mais bien sûr, si cela se produit très souvent, cela ralentira le processus.init
ne pas vérifier à plusieurs reprises si son argument est une liste singleton ou une liste vide. Une fois que la récursion commence, elle suppose simplement que le premier élément sera cloué sur le résultat de l'appel récursif.myButLast
automatiquement. Je pense que c'est plus probablement la fusion de listes qui est à blâmer pour l'accélération.Réponses:
Lorsque vous étudiez la vitesse et l'optimisation, il est très facile d' obtenir des résultats complètement faux . En particulier, vous ne pouvez pas vraiment dire qu'une variante est plus rapide qu'une autre sans mentionner la version du compilateur et le mode d'optimisation de votre configuration d'analyse comparative. Même alors, les processeurs modernes sont si sophistiqués qu'ils comportent des prédicteurs de branche basés sur un réseau neuronal, sans parler de toutes sortes de caches, donc, même avec une configuration soignée, les résultats de l'analyse comparative seront flous.
Cela étant dit...
Le benchmarking est notre ami.
criterion
est un package qui fournit des outils d'analyse comparative avancés. J'ai rapidement rédigé une référence comme celle-ci:Comme vous le voyez, j'ai ajouté la variante qui correspond explicitement à deux éléments à la fois, mais sinon c'est le même code textuellement. J'exécute également les benchmarks à l'envers, afin d'être conscient du biais dû à la mise en cache. Alors, courons et voyons!
On dirait que notre version "lente" n'est pas du tout lente! Et les subtilités de la correspondance de motifs n'ajoutent rien. (Une légère accélération se produit entre deux exécutions consécutives de
match2
J'attribue les effets de la mise en cache.)Il existe un moyen d'obtenir plus de données "scientifiques" : nous pouvons
-ddump-simpl
et jeter un œil à la façon dont le compilateur voit notre code.L'inspection des structures intermédiaires est notre amie.
"Core" est un langage interne de GHC. Chaque fichier source Haskell est simplifié dans Core avant d'être transformé en graphique fonctionnel final pour que le système d'exécution s'exécute. Si nous regardons cette étape intermédiaire, cela nous le dira
myButLast
etbutLast2
sont équivalents. Il faut regarder, car, au stade du changement de nom, tous nos jolis identifiants sont mutilés au hasard.Il semble que
A1
etA4
sont les plus similaires. Une inspection approfondie montrera qu'en effet les structures de code dansA1
etA4
sont identiques. CelaA2
et le mêmeA3
sont également raisonnables car les deux sont définis comme une composition de deux fonctions.Si vous allez examiner la
core
sortie en profondeur, il est logique de fournir également des indicateurs tels que-dsuppress-module-prefixes
et-dsuppress-uniques
. Ils le rendent tellement plus facile à lire.Une courte liste de nos ennemis aussi.
Alors, qu'est-ce qui peut mal tourner avec l'analyse comparative et l'optimisation?
ghci
, étant conçu pour un jeu interactif et une itération rapide, compile la source Haskell avec une certaine saveur de code octet, plutôt que l'exécutable final, et évite les optimisations coûteuses en faveur d'un rechargement plus rapide.Cela peut paraître triste. Mais ce n'est vraiment pas la chose qui devrait concerner un programmeur Haskell, la plupart du temps. Histoire vraie: J'ai un ami qui a récemment commencé à apprendre Haskell. Ils avaient écrit un programme d'intégration numérique, et c'était très lent. Nous nous sommes donc assis ensemble et avons écrit une description catégorielle de l'algorithme, avec des diagrammes et des trucs. Quand ils ont réécrit le code pour l'aligner sur la description abstraite, il est devenu magiquement, comme, guépard rapide et mince en mémoire aussi. Nous avons calculé π en un rien de temps. Morale de l'histoire? Structure abstraite parfaite, et votre code s'optimisera.
la source
ghci
semble donner des résultats différents (en termes de vitesse) par rapport à la fabrication d'un exe en premier, comme vous l'avez dit.