Skip to content

Comment compiler efficacement ?

Ce souci peut être résolu de diverses manières, mais dans ce cas nous vous donnons la solution la plus complète pour nous.

Solution :

Je vais juste jeter quelques pensées aléatoires dans aucun ordre particulier, mais ce sera une vue plutôt de haut niveau sur les choses. C'est nécessairement une exposition subjective, donc traitez-la comme telle.

Cas d'utilisation typiques

A mon avis, Compile en tant que dispositif de renforcement de l'efficacité est efficace dans deux types de situations (et leurs mélanges) :

  • Le problème est résolu plus efficacement avec un style procédural, parce que, par exemple, un algorithme efficace pour cela est formulé de manière procédurale et n'a pas de contrepartie fonctionnelle simple / efficace (notez également que la programmation fonctionnelle en Mathematica est particulière à bien des égards, reflétant le fait que la couche fonctionnelle est une couche mince au-dessus du moteur basé sur des règles. Ainsi, certains algorithmes qui sont efficaces dans d'autres langages fonctionnels peuvent être inefficaces en Mathematica). Un signe très clair de cela est lorsque vous devez faire de l'indexation de tableau dans une boucle.

  • Le problème peut être résolu en joignant plusieurs Compile-Le problème peut être résolu en joignant plusieurs fonctions intégrées utilisables, mais il y a (peut-être plusieurs) "joints" où vous faites face à une perte de performance si vous utilisez le code de niveau supérieur, parce qu'il reste général et ne peut pas utiliser des versions spécialisées de ces fonctions, et pour quelques autres raisons. Dans de tels cas, Compile rend simplement le code plus efficace en spécialisant effectivement le type des arguments numériques et en n'utilisant pas l'évaluateur principal. Un exemple qui vient à l'esprit est lorsque nous compilons Select avec un prédicat personnalisé (compilable) et que nous pouvons obtenir une augmentation substantielle des performances (voici un exemple).

J'utilise cette règle empirique lorsque je détermine si je bénéficierai ou non de l'utilisation de Compile: plus mon code à l'intérieur de Compile ressemble à du code C que j'écrirais autrement, plus j'en tire profit (strictement parlant, ceci n'est vrai que pour la compilation vers C, pas pour MVM).

Il peut arriver que certaines portions de code de niveau supérieur soient le principal goulot d'étranglement et ne puissent pas être refondues dans une forme plus efficace, pour une approche donnée du problème. Dans un tel cas , Compile peut ne pas vraiment aider, et il est préférable de repenser l'ensemble de l'approche et d'essayer de trouver une autre formulation pour le problème. En d'autres termes, on gagne souvent du temps et de l'effort à faire du profilage et à se faire une bonne idée de l'approche du problème. réel endroits où se trouvent les goulots d'étranglement, avant de se tourner vers Compile.

Limites de Compile

Voici une liste (incomplète) des limitations, dont vous avez vous-même mentionné la plupart.

  • Ne peut accepter que des tableaux réguliers (tenseurs) de type numérique ou booléen. Cela exclut les tableaux ragged et les expressions Mathematica plus générales.
  • Dans la plupart des cas, ne peut retourner qu'un seul tenseur d'un certain type.
  • Uniquement arithmétique en précision machine
  • A partir des fonctions définies par l'utilisateur, seules les fonctions pures sont compilables, plus on peut inline d'autres fonctions compilées. Les règles et les "fonctions" définies avec des règles sont intrinsèquement non compilables.
  • Pas de possibilité de créer des fonctions avec de la mémoire (a-la variables statiques en C).
  • Seul un petit sous-ensemble de fonctions intégrées peut être compilé en byte-code (ou en C).
  • Les possibilités d'écrire des fonctions compilées récursives semblent être très limitées, et la plupart des cas intéressants semblent être exclus.
  • Pas de sémantique pass-by-reference décente, ce qui est un gros problème (pour moi en tout cas).
  • Vous ne pouvez pas vraiment utiliser des variables indexées dans Compilebien que cela soit possible apparaître que vous le pouvez.
  • ...

Compiler ou non en C ?

Je pense que cela dépend des circonstances. La compilation en C est coûteuse, donc cela n'a de sens que pour un code critique en termes de performances qui sera utilisé de nombreuses fois. Il y a aussi de nombreux cas où la compilation en MVM donnera des performances similaires, tout en étant beaucoup plus rapide. Un tel exemple peut être trouvé dans cette réponse, où la compilation juste à temps vers la cible MVM a conduit à une accélération majeure, alors que la compilation vers C aurait probablement détruit le but de celle-ci - dans ce cas particulier.

Une autre classe de situations où la compilation en C n'est peut-être pas la meilleure option est lorsque vous voulez "sérialiser" le fichier CompiledFunction et le distribuer à d'autres, par exemple dans un paquet, et que vous ne voulez pas compter sur un compilateur C installé sur la machine de l'utilisateur. Pour autant que je sache, il n'y a pas encore de mécanisme automatique pour saisir la bibliothèque partagée générée et l'empaqueter avec l'objet CompiledFunctionDe plus, il faudrait faire une compilation croisée pour toutes les plates-formes et envoyer automatiquement la bonne bibliothèque à charger. Tout cela est possible mais compliqué, donc, à moins que le gain de vitesse ne justifie de telles complications pour un problème donné, cela peut ne pas en valoir la peine, alors que la compilation vers la cible MVM crée la bibliothèque de haut niveau CompiledFunction qui est automatiquement multiplateforme, et ne nécessite aucune installation (sauf Mathematica).

Donc, cela dépend vraiment, bien que, le plus souvent, la compilation en C conduise à une exécution plus rapide et, si vous décidez d'utiliser l'option Compilesera justifiée.

Ce qu'il faut inclure dans Compile

Je partage une opinion selon laquelle, à moins d'avoir des exigences spécifiques, il est préférable de n'utiliser que Compile sur des fragments de code minimaux qui en bénéficieraient le plus, plutôt que d'avoir une grande Compile. C'est une bonne chose car :

  • Il vous permet de mieux comprendre où se trouvent les véritables goulots d'étranglement.
  • Cela rend votre code compilé plus testable et composable.
  • Si vous en avez vraiment besoin, vous pouvez ensuite combiner ces morceaux et utiliser les éléments suivants "InlineCompiledFunctions" -> True l'option setting, pour obtenir tous les avantages qu'une seule grande Compile vous donnerait
  • Puisque Compile est limité dans ce qu'il peut prendre, vous aurez moins de maux de tête sur la façon d'inclure certains morceaux non compilables, plus moins de chances de négliger un rappel à l'évaluateur principal....

Cela dit, vous pouvez bénéficier d'un grand Compile dans certaines situations, notamment :

  • Les cas où vous voulez saisir le code C résultant et l'utiliser de manière autonome (lié contre Wolfram RTL).
  • Les cas où vous voulez exécuter votre code compilé en parallèle sur plusieurs noyaux et ne voulez pas penser à d'éventuels problèmes de distribution de définitions, etc (cela a été noté par @halirutan).

Fonctions listables

Lorsque vous le pouvez, ce peut être une bonne idée d'utiliser la fonction RuntimeAttributes -> Listable afin que votre code puisse être exécuté sur (tout ou partie) des cœurs disponibles en parallèle. Je vais donner un exemple qui me semble plutôt intéressant, parce qu'il représente un problème qui peut ne pas sembler initialement se prêter directement à cela (bien qu'il ne soit sûrement pas du tout difficile de se rendre compte que la parallélisation peut fonctionner ici) - le calcul de... Pi comme une somme partielle, d'une représentation bien connue de la somme infinie. Voici une fonction à noyau unique :

Clear[numpi1];
numpi1 = 
   Compile[{{nterms, _Integer}}, 
      4*Sum[(-1)^k/(2 k + 1), {k, 0, nterms}], 
        CompilationTarget -> "C", RuntimeOptions -> "Speed"];

Voici une version parallèle :

numpiParallelC = 
  Compile[{{start, _Integer}, {end, _Integer}}, 
    4*Sum[(-1)^k/(2 k + 1), {k, start, end}], CompilationTarget -> "C",
       RuntimeAttributes -> Listable, RuntimeOptions -> "Speed"];

Clear[numpiParallel];
numpiParallel[nterms_, nkernels_] := 
  [email protected][numpiParallelC, 
     MapAt[# + 1 &, {Most[#], Rest[#] - 1}, {2, -1}] &@
        IntegerPart[Range[0, nterms, nterms/nkernels]]];

Maintenant, quelques benchmarks (sur une machine à 6 cœurs) :

(res0=numpiParallel[10000000,1])//AbsoluteTiming
(res1=numpiParallel[10000000,6])//AbsoluteTiming
(res2=numpi1[10000000])//AbsoluteTiming
Chop[{res0-res2,res0-res1,res2-res1}]

(*
 ==>
 {0.0722656,3.14159}
 {0.0175781,3.14159}
 {0.0566406,3.14159}
 {0,0,0}
*)

Quelques points à noter ici :

  • Il peut arriver que le temps nécessaire pour préparer les données à introduire dans une Listable fonction compilée, sera beaucoup plus important que le temps d'exécution de la fonction (par exemple, lorsque nous utilisons des fonctions Transpose ou Partition etc. sur des listes énormes), ce qui détruit en quelque sorte l'objectif. Donc, il est bon de faire une estimation si oui ou non ce sera le cas.
  • Une alternative plus "grossière" à ceci est d'exécuter une fonction compilée monofilaire en parallèle sur plusieurs noyaux Mathematica, en utilisant la fonctionnalité parallèle intégrée (ParallelEvaluate, ParallelMapetc). Ces deux possibilités sont utiles dans des situations différentes.

Compilation automatique

Bien que cela ne soit pas directement lié à l'utilisation explicite de l'option Compile, ce sujet a logiquement sa place ici. Il existe un certain nombre de fonctions intégrées (d'ordre supérieur), telles que Mapqui peuvent compiler automatiquement. Ce que cela signifie, c'est que lorsque nous exécutons

Map[f, list]

la fonction f est analysée par Map, qui tente d'appeler automatiquement Compile sur celui-ci (cela n'est pas fait au niveau supérieur, donc l'utilisation de Trace ne montrera pas un appel explicite à Compile). Pour en bénéficier, la fonction f doit être compilable. En règle générale, il faut qu'il s'agisse d'une fonction pure pour cela (ce qui n'est pas en soi une condition suffisante) - et généralement, la question de savoir si une fonction est compilable ou non trouve ici une réponse identique à celle de la fonction explicite Compile. En particulier, les fonctions définies par des motifs seront pas bénéficier de l'auto-compilation, ce qui est quelque chose à garder à l'esprit.

Voici un exemple un peu artificiel mais simple pour illustrer le propos :

sumThousandNumbers[n_] := 
   Module[{sum = 0}, Do[sum += i, {i, n, n + 1000}]; sum]

sumThousandNumbersPF = 
   Module[{sum = 0}, Do[sum += i, {i, #, # + 1000}]; sum] &

Maintenant, on essaie :

Map[sumThousandNumbers, Range[3000]]//Short//Timing
Map[sumThousandNumbersPF, Range[3000]]//Short//Timing

(*
  ==> {3.797,{501501,502502,503503,504504,505505,<<2990>>,3499496,
               3500497,3501498,3502499,3503500}}

      {0.094,{501501,502502,503503,504504,505505,<<2990>>,3499496,
               3500497,3501498,3502499,3503500}}
*)

qui montre une accélération de 40 fois dans ce cas particulier, grâce à l'auto-compilation.

Il y a en fait plusieurs cas où cela est important, et tous ne sont pas aussi évidents que l'exemple ci-dessus. Un tel cas a été considéré dans une réponse récente à la question de l'extraction de nombres d'une liste triée appartenant à une certaine fenêtre. La solution est courte et je la reproduis ici :

window[list_, {xmin_, xmax_}] := 
    Pick[list, Boole[xmin <= # <= xmax] & /@ list, 1]

Ce qui peut sembler une solution pas particulièrement efficace, est en fait... tout à fait rapide en raison de l'auto-compilation du prédicat. Boole[...] à l'intérieur de Map, plus Pick étant optimisé sur les tableaux emballés. Voir la question susmentionnée pour plus de contexte et de discussion.

Cela nous montre un autre avantage de la compilation automatique : non seulement elle fait souvent fonctionner le code... beaucoup plus plus rapide, mais elle ne déballe pas non plus, ce qui permet aux fonctions environnantes de bénéficier également des tableaux emballés quand elles le peuvent.

Quelles fonctions peuvent s'auto-compiler ? Une façon de le savoir est d'inspecter SystemOptions["CompileOptions"]:

Cases["CompileOptions"/.SystemOptions["CompileOptions"],
      opt:(s_String->_)/;StringMatchQ[s,__~~"Length"]]

{"ApplyCompileLength" -> [Infinity], "ArrayCompileLength" -> 250, 
 "FoldCompileLength" -> 100, "ListableFunctionCompileLength" -> 250, 
 "MapCompileLength" -> 100, "NestCompileLength" -> 100, 
 "ProductCompileLength" -> 250, "SumCompileLength" -> 250, 
 "TableCompileLength" -> 250}

Cela vous indique également les longueurs seuils de la liste au-delà desquelles l'auto-compilation est activée. Vous pouvez également modifier ces valeurs. En fixant la valeur de ...CompileLength à Infinity désactive effectivement l'autocompilation. Vous pouvez voir que "ApplyCompileLength" a cette valeur. C'est parce qu'il ne peut compiler que 3 têtes : Times, Pluset List. Si vous avez l'un d'eux dans votre code, cependant, vous pouvez réinitialiser cette valeur, pour bénéficier de la compilation automatique. En général, les valeurs par défaut sont assez significatives, il est donc rarement nécessaire de modifier ces valeurs par défaut.

Quelques techniques supplémentaires

Il existe un certain nombre de techniques impliquant Compilequi sont peut-être un peu plus avancées, mais qui permettent parfois de résoudre des problèmes pour lesquels les simples Compile n'est pas assez flexible. En voici quelques-unes que je connais :

  • Parfois, on peut échanger de la mémoire contre de la vitesse, et, ayant une liste imbriquée, la remplir de zéros pour former un tenseur, et le passer à Compile.

  • Parfois, votre liste est générale et vous ne pouvez pas la traiter directement dans Compile . Compilepour faire ce que vous voulez, cependant, vous pouvez reformuler un problème tel que vous pouvez plutôt traiter une liste d'éléments positions qui sont des entiers. J'appelle cela la "dualité élément-position". Un exemple de cette technique en action est ici, pour une application plus large de cette idée voir mon dernier message dans ce fil (j'ai hésité à inclure cette référence parce que mes premiers plusieurs messages là sont des solutions incorrectes. Notez que pour ce problème particulier, une solution beaucoup plus élégante et courte, mais un peu moins efficace, a été donnée à la fin de ce fil).

  • Parfois, vous pouvez avoir besoin de certaines opérations structurelles pour préparer les données d'entrée pour les opérations de Compile, et les données contiennent des listes (ou, généralement, des tenseurs), de différents types (disons, des positions entières et des valeurs réelles). Pour garder la liste emballée, il peut être judicieux de convertir les entiers en réels (dans cet exemple), les reconvertissant en entiers avec IntegerPart à l'intérieur de Compile . Voici un exemple de ce type

  • Génération en temps réel de fonctions compilées, où certains paramètres d'exécution sont intégrés. Ceci peut être combiné avec la mémorisation. Un exemple est ici, un autre très bon exemple est ici.

  • On peut émuler le pass-by-reference et avoir un moyen de composer de plus grandes fonctions compilées à partir de plus petites fonctions. avec des paramètres (enfin, en quelque sorte), sans perte d'efficacité. Cette technique est présentée par exemple ici

  • Une sagesse commune est que, puisque ni les linked-lists, ni les Sow-Reap ne sont compilables, on doit préallouer de grands tableaux la plupart du temps, pour stocker les résultats intermédiaires. Il y a au moins deux autres options :

    • Utiliser Internal`Bag, qui est compilable (le problème cependant est qu'il ne peut pas être retourné comme résultat de Compileà partir de maintenant, AFAIK).
    • Il est assez facile d'implémenter un analogue d'un tableau dynamique à l'intérieur de votre code compilé, en mettant en place une variable qui donne la limite de taille actuelle, et copier votre tableau vers un nouveau tableau plus grand une fois que plus d'espace est nécessaire. De cette façon, vous n'allouez (à la fin) que l'espace réellement nécessaire, pour un prix de quelques overhead, qui est souvent négligeable.
  • On peut souvent être en mesure d'utiliser des opérations vectorielles telles que UnitStep, Clip, Unitize etc, pour remplacer le flux de contrôle if-else dans les boucles internes, également à l'intérieur des boucles internes. Compile . Ceci peut donner une énorme accélération, en particulier lors de la compilation vers la cible MVM. Quelques exemples sont dans mes commentaires dans cet article et cet article de blog, et un autre exemple assez illustratif d'une recherche binaire vectorisée dans ma réponse dans ce fil de discussion.

  • Utiliser une liste supplémentaire d'entiers comme "pointeurs" vers certaines listes que vous pouvez avoir. Ici, je vais faire une exception pour ce post, et donner un exemple explicite, illustrant le propos. Ce qui suit est une fonction assez efficace pour trouver une sous-séquence croissante la plus longue d'une liste de nombres. Elle a été développée conjointement par DrMajorBob, Fred Simons et moi-même, dans une discussion en ligne et hors ligne du MathGroup (donc cette forme finale n'est pas disponible publiquement AFAIK, d'où son inclusion ici).

Voici le code

Clear[btComp];
btComp = 
Compile[{{lst, _Integer, 1}}, 
   Module[{refs, result, endrefs = {1}, ends = {[email protected]}, 
      len = [email protected], n0 = 1, n1 = 1, i = 1, n, e}, 
     refs = result = 0 lst;
     For[i = 2, i <= len, i++, 
        Which[
          lst[[i]] < [email protected], 
             (ends[[1]] = lst[[i]]; endrefs[[1]] = i; refs[[i]] = 0),
          lst[[i]] > [email protected], 
             (refs[[i]] = [email protected];AppendTo[ends, lst[[i]]]; AppendTo[endrefs, i]), 
          [email protected] < lst[[i]] < [email protected], 
             (n0 = 1; n1 = [email protected];  
              While[n1 - n0 > 1, 
                n = Floor[(n0 + n1)/2];
                If[ends[[n]] < lst[[i]], n0 = n, n1 = n]];
                ends[[n1]] = lst[[i]];
                endrefs[[n1]] = i;
                refs[[i]] = endrefs[[n1 - 1]])
        ]];
        For[i = 1; e = [email protected], e != 0, (i++; e = refs[[e]]), 
            result[[i]] = lst[[e]]];
        [email protected][result, i - 1]], CompilationTarget -> "C"];

Voici un exemple d'utilisation (la liste ne doit pas contenir de doublons) :

test = RandomSample[#, Length[#]] &@ [email protected][{1, 1000000}, 1000000];

btComp[test] // Length // Timing

La solution la plus rapide basée sur les built-ins, qui est en effet... très rapide, est encore environ 6 fois plus lente pour cette taille de liste :

LongestCommonSequence[test, [email protected]] // Short // Timing

Quoi qu'il en soit, le point ici est que cela a été possible grâce à des variables supplémentaires... refs et endrefs, dont l'utilisation permettait de ne manipuler que des entiers simples (représentant des positions de sous-listes dans une liste plus grande) au lieu de grandes listes d'entiers.

Quelques remarques assorties

  • Les choses à surveiller: voir cette discussion pour quelques conseils à ce sujet. En gros, il faut éviter

    • Les callbacks vers l'évaluateur principal
    • Les copies excessives de tenseurs (CopyTensor instruction)
    • Déballage accidentel se produisant dans les fonctions de niveau supérieur préparant l'entrée de Compile ou traitant sa sortie. Ceci n'est pas lié à Compile proprement dit, mais il arrive que Compile n'aide pas du tout, car le goulot d'étranglement se trouve dans le code de haut niveau.
  • Conversion de type Je ne m'inquiéterais pas de l'impact sur les performances, mais parfois des types erronés peuvent conduire à des erreurs d'exécution, ou à des rappels non anticipés de MainEvaluate dans le code compilé.

  • Certaines fonctions (par exemple Sort avec la fonction de comparaison par défaut, mais pas seulement), ne bénéficient pas beaucoup de la compilation ou... du tout.

  • Il n'est pas clair comment Compile traite Hold- dans le code compilé, mais il y a des indications qu'il ne préserve pas entièrement la sémantique standard à laquelle nous sommes habitués dans le niveau supérieur.

  • Comment voir si l'on peut utiliser efficacement ou non Compile pour un problème donné.. Mon expérience est qu'avec Compiledans Mathematica, vous devez être "proactif" (avec toute mon aversion pour ce mot, je ne connais rien de mieux ici). Ce que je veux dire, c'est que pour l'utiliser efficacement, vous devez rechercher dans la structure de votre problème / programme les endroits où vous pourriez transformer les (parties de) données en une forme utilisable dans Compile . Dans la plupart des cas (du moins dans mon expérience), sauf ceux évidents où vous avez déjà un algorithme procédural en pseudo-code, vous devez reformuler le problème, donc vous devez demander activement : que dois-je faire pour utiliser Compile ici.

Réglage de SetSystemOptions[ "CompileOptions" -> "CompileReportExternal"->True] permettra d'émettre un message lorsque des parties de votre fonction ne sont pas compilées. Après la compilation, Needs["CompiledFunctionTools`"] suivi de CompilePrint[cF] (avec cF la fonction que vous avez compilée affichera un peu de bytecode; à la recherche de CopyTensor ou MainEvaluate qui aide à localiser les inefficacités (MainEvaluate appelle le noyau principal de mma, donc cela signifie effectivement ne pas compiler).

D'autres options utiles peuvent être trouvées en évaluant SystemOptions["CompileOptions"]

EDIT : Comme suggéré par Oleksandr dans un commentaire, On[Compile::noinfo] est également utile.

Leonid Shifrin a déjà donné une excellente réponse à la question, mais elle est si... longue et peut être frustrante pour quelqu'un qui commence à apprendre l'usage de... Compiledonc j'ai décidé de poster ceci comme une réponse.

Récemment (OK... en fait, c'est il y a plus d'un an), j'ai trouvé que Ted Ersek's Mathematica trucs(.nb version peut être trouvée ici) contient un résumé bref mais magistral pour la limitation de... Compile. Bien qu'il soit écrit sur la base de Version 4, les 6 règles énumérées par lui sont toujours valables et si vous les comprenez parfaitement, vous pouvez gérer la plupart des problèmes de compilation. Pour plus de commodité, je vais coller la partie pertinente ci-dessous (avec des modifications pour une partie des commentaires et des exemples).

BTW, basé sur les 6 règles, j'ai écrit un tutoriel chinois pour la compilation ici.


(1) Seul un sous-ensemble de fonctions du noyau peut être utilisé dans l'évaluation compilée.

Nous avons déjà ce post pour cette question.


(2) L'évaluation compilée ne peut pas utiliser les variables globales.

Dans le morceau de code suivant, une fonction compilée utilise une variable globale. width. Plus tard, nous voyons que la fonction fonctionne, mais les tests de synchronisation montreront qu'elle est plus lente que si la fonction était définie sans utiliser... Compile.

width=2.5;
g1 = Function[{x}, x + width];
g2 = Compile[{x}, x + width];

Do[g1[0.3], {10^6}]; // AbsoluteTiming
Do[g2[0.3], {10^6}]; // AbsoluteTiming
{0.786027, Null}
{1.303057, Null}

Le morceau de code suivant définit la même fonction que l'exemple précédent et prend la variable globale comme argument. C'est la façon dont les variables globales devraient être utilisées dans une fonction compilée.

g3 = Compile[{x, y}, x + y];

Do[g3[0.3, width], {10^6}]; // AbsoluteTiming
{0.415907, Null}

Le morceau de code suivant montre ce qui se passe si nous essayons de définir la valeur d'une variable globale à l'intérieur d'une fonction compilée. Ici encore, la fonction fonctionne, mais les tests de synchronisation montrent que la fonction est plus lente que la même fonction définie sans utiliser... Compile.

h1 = Function[{x}, (temp = 2.5; x + temp)];
h2 = Compile[{x}, (temp = 2.5; x + temp)];

Do[h1[0.3], {10^6}]; // AbsoluteTiming
Do[h2[0.3], {10^6}]; // AbsoluteTiming
{1.471703, Null}
{2.391085, Null}

Dans le dernier exemple, la valeur de temp a été définie à l'intérieur de Compile. Lorsque quelque chose comme ceci est nécessaire, Block, Moduleou With devraient être utilisés pour faire temp une variable locale. Des fonctions compilées sont définies ci-dessous qui font la même chose que le dernier exemple en utilisant Block, Module, With et elles utilisent toutes une évaluation compilée qui s'exécute beaucoup plus rapidement. Outre l'avantage en termes de vitesse, l'approche ci-dessous garantit que l'évaluation de say ha[0.3], hb[0.3], hc[0.3] ne changera pas la valeur temp pourrait avoir en dehors de la fonction compilée.

ha = Compile[{x}, Block[{temp = 2.5}, x + temp]];
hb = Compile[{x}, Module[{temp = 2.5}, x + temp]];
hc = Compile[{x}, With[{temp = 2.5}, x + temp]];

Do[ha[0.3], {10^6}]; // AbsoluteTiming
Do[hb[0.3], {10^6}]; // AbsoluteTiming
Do[hc[0.3], {10^6}]; // AbsoluteTiming
{0.364617, Null}
{0.352145, Null}
{0.347725, Null}

(3) L'évaluation compilée ne peut pas fonctionner avec des motifs.

Cette règle peut être considérée comme une déduction de la règle (1).

La fonction dans le morceau de code suivant prend une liste de nombres réels, et détermine si... 0.5 est dans la liste. Cette fonction est entièrement compilée.

f4=Compile[{{lst,_Real,1}},FreeQ[lst,0.5]];
<[email protected]
…………
1 B0 = FreeQ[ T(R1)0, T(R0)0, R1]]
2 Return

La fonction ci-dessous prend également une liste de nombres réels, et détermine si la liste est exempte de nombres négatifs. Cette fonction fonctionne, mais des tests de chronométrage montreront qu'elle n'est pas plus rapide que la même fonction définie sans utiliser... Compile . Le problème dans ce cas est que la fonction compilée utilise le motif _?Negative. L'évaluation compilée n'est pas utilisée si le deuxième argument de la fonction Compilecomprend des motifs de toute sorte. Lorsque nous sommes confrontés à un tel problème, nous devrions utiliser un algorithme qui ne nécessite pas de motifs, ou définir une fonction sans utiliser de Compile.

f5=Compile[{{lst,_Real,1}},FreeQ[lst,_?Negative]];
[email protected]
…………
1 R0 = MainEvaluate[ Function[{lst}, _?Negative][ T(R1)0]]
2 B0 = FreeQ[ T(R1)0, T(R0)0, R1]]
3 Return

Le morceau de code suivant montre une façon d'écrire la fonction du dernier exemple pour éviter l'utilisation de patterns. Cette version utilisera l'évaluation compilée.

f5fixed=Compile[{{lst,_Real,1}}, FreeQ[Sign[lst],-1]  ];
[email protected]
…………
1 T(I1)1 = Sign[ T(R1)0]
2 B0 = FreeQ[ T(I1)1, T(I0)0]]
3 Return

Remarquez que la définition d'une fonction comme f[x_] = … ou f[x_] := … utilise aussi le motif, donc elles ne peuvent pas être compilées !


(4) Les variables locales dans l'évaluation compilée doivent toujours avoir le même type.

Le morceau de code suivant définit une fonction qui utilise une variable locale. temp où temp commence par une valeur entière. Plus loin dans la fonction temp est transformée en un nombre réel. Dans ce cas, la fonction fonctionne, mais les tests de timing montreront qu'elle n'est pas plus rapide que la même fonction définie sans utiliser Compile. Les variables locales utilisées dans une fonction compilée doivent toujours avoir le même type (Real, Integer, Complex, True|False) pour garantir l'utilisation de l'évaluation compilée.

f6 = Compile[{x}, Module[{temp = 5}, (temp = temp + x; Round[temp])]];
[email protected]
…………
1 I1 = I0
2 R1 = I1
3 R1 = R1 + R0
4 V17I1 = MainEvaluate[ Function[{x, tempCompile$2}, 
    Block[{temp = tempCompile$2}, {temp 
= temp + x, temp}]][ R0, I1]]
5 Return

On peut donc corriger f6 de la manière suivante :

f6fixed1 = Compile[{x}, Module[{temp = 5}, temp = Round[temp + x]]];
[email protected]
…………
1 I1 = I0
2 R1 = I1
3 R1 = R1 + R0
4 I2 = Round[ R1]
5 I1 = I2
6 Return
f6fixed2 = Compile[{x}, Module[{temp = 5.}, temp = temp + x; Round[temp]]];
[email protected]
…………
1 R2 = R1
2 R3 = R2 + R0
3 R2 = R3
4 I1 = Round[ R2]
5 Return

(5) Une fonction compilée ne peut pas changer la valeur de son argument.

Le morceau de code suivant montre une tentative de définition d'une fonction. Si vous essayez d'utiliser cette fonction, vous verrez qu'elle ne fonctionne pas. Le problème ici est que la fonction essaie de changer la valeur de son argument, mais une fonction compilée ne peut pas changer la valeur de son argument pour la même raison que nous ne pouvons pas évaluer.... 3=5.

f7=Compile[{x},
Do[x=Cos[x],{4}];
x];

Parfois, nous voulons utiliser un algorithme qui commence avec une certaine valeur et la remplace par une autre. Si nous voulons faire cela dans une fonction compilée, nous devons initialiser une variable locale à la valeur de l'argument de la fonction, puis changer la valeur de la variable locale. La fonction dans la cellule suivante fait cela et utilise l'évaluation compilée.

f7fixed=Compile[{x},
Module[{temp=x},
Do[temp=Cos[temp],{4}];
temp
]];

(6) L'évaluation compilée ne peut pas fonctionner avec toutes les structures de liste possibles.

Les seules structures de liste avec lesquelles l'évaluation compilée peut fonctionner sont les vecteurs, les matrices et autres tenseurs. La fonction définie dans le morceau de code suivant retourne une structure de liste qui ne répond pas à ces restrictions, donc l'évaluation compilée ne peut pas être utilisée. Dans un cas comme celui-ci, il n'y a aucun avantage à utiliser Compile pour définir la fonction.

f8=Compile[{x},{x,{2x,3x}}];
[email protected]
…………
11    T(R1)2 = MainEvaluate[ Hold[List][ R0, T(R1)1]]
12    Return

Vous pouvez ajouter de la valeur à nos informations en taxant votre expérience dans les notes.



Utilisez notre moteur de recherche

Ricerca
Generic filters

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.