Pourquoi une simple instruction get est-elle si lente?

Il y a quelques années, j’ai été affecté à l’école où je devais paralléliser un Raytracer.
C’était une tâche facile, et j’ai vraiment aimé travailler dessus.

Aujourd’hui, j’avais envie de profiler le traceur de rayons pour voir si je pouvais le faire fonctionner plus rapidement (sans refondre complètement le code). Lors du profilage, j’ai remarqué quelque chose d’intéressant:

// Sphere.Intersect public bool Intersect(Ray ray, Intersection hit) { double a = ray.Dir.x * ray.Dir.x + ray.Dir.y * ray.Dir.y + ray.Dir.z * ray.Dir.z; double b = 2 * (ray.Dir.x * (ray.Pos.x - Center.x) + ray.Dir.y * (ray.Pos.y - Center.y) + ray.Dir.z * (ray.Pos.z - Center.z)); double c = (ray.Pos.x - Center.x) * (ray.Pos.x - Center.x) + (ray.Pos.y - Center.y) * (ray.Pos.y - Center.y) + (ray.Pos.z - Center.z) * (ray.Pos.z - Center.z) - Radius * Radius; // more stuff here } 

Selon le profileur, 25% du temps processeur a été consacré à get_Dir et à get_Pos . C’est pourquoi j’ai décidé d’optimiser le code de la manière suivante:

  // Sphere.Intersect public bool Intersect(Ray ray, Intersection hit) { Vector3d dir = ray.Dir, pos = ray.Pos; double xDir = dir.x, yDir = dir.y, zDir = dir.z, xPos = pos.x, yPos = pos.y, zPos = pos.z, xCen = Center.x, yCen = Center.y, zCen = Center.z; double a = xDir * xDir + yDir * yDir + zDir * zDir; double b = 2 * (xDir * (xPos - xCen) + yDir * (yPos - yCen) + zDir * (zPos - zCen)); double c = (xPos - xCen) * (xPos - xCen) + (yPos - yCen) * (yPos - yCen) + (zPos - zCen) * (zPos - zCen) - Radius * Radius; // more stuff here } 

Avec des résultats étonnants.

Dans le code d’origine, exécuter Raytracer avec ses arguments par défaut (créer une image 1024×1024 avec seulement l’éclairage direct et sans AA) prendrait environ 88 secondes .
Dans le code modifié, la même chose prendrait un peu moins de 60 secondes .
J’ai réalisé une accélération de ~ 1,5 avec seulement cette petite modification du code.

Au début, je pensais que le getter pour Ray.Dir et Ray.Pos faisait des choses en arrière-plan, qui ralentiraient le programme.

Voici les getters pour les deux:

  public Vector3d Pos { get { return _pos; } } public Vector3d Dir { get { return _dir; } } 

Donc, les deux renvoient un Vector3D, et c’est tout.

Je me demande vraiment comment il serait plus long d’appeler le getter que d’accéder directement à la variable.

Est-ce à cause des variables de mise en cache de la CPU? Ou peut-être que la surcharge d’appeler ces méthodes à plusieurs resockets s’est additionnée? Ou peut-être le JIT traitant le dernier cas mieux que le premier? Ou peut-être qu’il y a quelque chose d’autre que je ne vois pas?

Toute idée serait grandement appréciée.

Modifier:

Comme @MatthewWatson l’a suggéré, j’ai utilisé un StopWatch pour libérer les StopWatch de libération en dehors du débogueur. Afin de supprimer le bruit, j’ai exécuté les tests plusieurs fois. En conséquence, le premier code prend environ 21 secondes (entre 20,7 et 20,9), tandis que le dernier ne prend que 19 secondes (entre 19 et 19,2).
La différence est devenue négligeable, mais elle existe toujours.

introduction

Je serais prêt à parier que le code original est tellement plus lent en raison d’une bizarrerie dans C # impliquant des propriétés de type structs. Ce n’est pas exactement intuitif, mais ce type de propriété est insortingnsèquement lent. Pourquoi? Parce que les structs ne sont pas passés par référence. Donc, pour accéder à ray.Dir.x , vous devez

  1. Charge le ray variable local.
  2. Appelez get_Dir et stockez le résultat dans une variable temporaire. Cela implique la copie de la structure entière, même si seul le champ ‘x’ est utilisé.
  3. Accédez au champ x partir de la copie temporaire.

En regardant le code d’origine, les accesseurs get sont appelés 18 fois. C’est un énorme gaspillage, car cela signifie que la structure entière est copiée 18 fois en tout. Dans votre code optimisé, il n’y a que deux copies. Dir et Pos sont appelés qu’une seule fois. L’access ultérieur aux valeurs consiste uniquement en la troisième étape d’en haut:

  1. Accédez au champ x partir de la copie temporaire.

Pour résumer, struct et propriétés ne vont pas ensemble.

Pourquoi C # se comporte-t-il de la sorte avec les propriétés de structure?

Cela a quelque chose à voir avec le fait qu’en C #, les structures sont des types valeur. Vous transmettez la valeur elle-même plutôt qu’un pointeur sur la valeur.

Pourquoi le compilateur ne reconnaît-il pas que l’accesseur get renvoie simplement un champ et ignore la propriété dans son ensemble?

En mode débogage, les optimisations de ce type sont ignorées pour offrir une meilleure expérience de débogage. Même en mode de lancement, vous constaterez que la plupart des tremblements ne le font pas souvent. Je ne sais pas exactement pourquoi, mais je pense que c’est parce que le champ n’est pas toujours aligné sur les mots. Les processeurs modernes ont des exigences de performances étranges. 🙂