Vidéo très intéressante !
Il y a une citation connue de Donald Knuth (un pionnier de l'algorithmique) :
premature optimization is the root of all evil. Le mot important est
premature. N'importe quel développeur avec un minimum d'expérience te dira qu'il faut toujours mesurer avant d'optimiser. C'est vrai quel que soit le langage, mais surtout pour les langages compilés (comme c'est le cas pour Mario 64). Nous seulement pour optimiser là où c'est pertinent, mais aussi pour vérifier que ça change vraiment quelque chose. C'est encore plus vrai aujourd'hui où l'environnement est énormément complexe avec des optimisations à tous les niveaux (CPU, compilateur, ...).
Difficile de dire d'où viennent ces optimisations sans plus d'informations sur le contexte et la gestion du développement : comment étaient répartis les personnes travaillent sur le moteur et sur le level design, comment se faisait la communication, est-ce qu'il pouvait y avoir des différences entre les types de build ou entre le devkit et la vraie console, etc. Néanmoins, le fait que ça soit non seulement une nouvelle console, mais aussi une toute nouvelle approche (par rapport aux consoles 2D) n'a sûrement pas dû aider...
Toutes les "mauvaises" optimisations qui sont montrées ont du sens intuitivement. Et plusieurs d'entre elles fonctionnaient sur les consoles des générations précédentes. La vidéo le confirme pour l'inlining par exemple. Mais la proximité mémoire était aussi sûrement moins un problème sur NES/SNES.
Il aurait "suffit" de mesurer (cf. citation), mais apparemment c'était compliqué avec les outils qu'ils avaient (d'après la vidéo).
Il est aussi possible que certaines optimisations soient faites par le compilateur. Même si les compilateurs de l'époque étaient sûrement beaucoup plus basiques que ce qu'on a aujourd'hui, certains mécanismes étaient peut-être implémenter. On rappelle que le "code" de SM64 qu'on a vient de la décompilation du binaire, ça n'est pas le code tapé par les dev de Nintendo.
Genre, typiquement c'est quoi cette histoire de loterie de l'optimisation ? On ne peut déterminer à l'avance quelle fonction est optimale pour quel rendu?
C'est la première fois que je vois le terme, donc je vais me limiter à l'exemple donné par la vidéo (l'emplacement en mémoire de certaines fonctions), sans chercher à extrapoler au risque de dire des bêtises.
Le problème n'est pas lié à quelle fonction est appelée, ou à ce qu'elle fait, mais à sa position dans la mémoire. Attention, ça va être un peu technique/long.
Un programme a tendance à utiliser des données/instructions proches de celles accédées/exécutées récemment. C'est le
principe de localité. Les architectures exploitent ce principe pour optimiser les accès. Par exemple, sur n'importe quel CPU pas trop simpliste, la RAM n'est pas accédée directement ; elle est d'abord copiée dans un cache, proche du CPU (et donc bien plus rapide), par petits blocs (quelques ko sur des CPU modernes) : si le CPU a besoin d'un octet dans ce petit bloc, alors il y a de grandes choses qu'il ait besoin d'autres octets de ce petit bloc bientôt, et la donnée sera déjà disponible tout près. Évidemment la taille de ce cache est limitée et y charger un nouveau bloc signifie en sortir un autre, sans forcément savoir lequel serait le meilleur choix. (Pour donner un ordre de grandeur, sur un CPU récent un accès au cache le plus près du CPU est ~100x plus rapide qu'un accès à une barette de RAM.)
Quand le code source est compilé, chaque fonction devient une suite d'instructions qui sera mise de manière contigüe dans la mémoire. Mais les fonctions étant indépendantes, elles peuvent être ordonnées un peu comme on veut dans la mémoire (et en général on s'en fiche). Le compilateur (en vrai plutôt le linker, mais on ne va pas rentrer dans ces détails) va les mettre dans un certain ordre, par exemple dans leur ordre d'apparition dans le code source.
Ce que montre la vidéo à 14:49 c'est que deux fonctions utilisées successivement sont éloignées l'une de l'autre (de plus de 1000 ko) et donc ne sont pas mises en cache ensemble. Je ne sais pas comment fonctionne ce cache, mais on imagine bien qu'il y aura des mouvements inutiles entre le cache et la RAM, qui n'auraient pas lieux si les deux fonctions étaient l'une à côté de l'autre. La vidéo insiste sur le fait que la bande passante mémoire (memory throughput) est le facteur limitant ; on veut donc au maximum éviter d'avoir à lire la RAM directement.
C'est la qu'intervient la "lotterie". Si on n'a pas de chances, parce qu'on a ajouté une fonction entre deux autres, parce qu'on a déplacé du code, ou parce que le compilateur a décidé de les ordonner différemment, des fonctions pourront ou pas se retrouver proches dans la mémoire du programme, et donc affecter les performances.
Pour du matériel bien identifié dont on connaît les forces et les faiblesses, y'a pas de déterminisme?
Il y aurait moyen de forcer deux fonctions à être côte à côte. Encore faut-il avoir identifié le problème et savoir lesquelles avant de penser à le faire (on en revient à l'importance des mesures et de l'outillage).
Il y a des mauvaises optimisations qui laissent penser que l'équipe ne connaissait pas forcément si bien les forces et faiblesses de la machine. Et c'est tout à fait normal : on a l'habitude d'avoir des jeux en fin de vie d'une console bien plus poussés que les premiers, parce que le matériel est mieux compris, et donc on peut développer de manière plus efficace.