IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Développement 2D, 3D et Jeux Discussion :

Le langage Java est-il adapté pour les jeux vidéo ? [Débat]


Sujet :

Développement 2D, 3D et Jeux

  1. #621
    Expert confirmé Avatar de DonQuiche
    Inscrit en
    Septembre 2010
    Messages
    2 741
    Détails du profil
    Informations forums :
    Inscription : Septembre 2010
    Messages : 2 741
    Points : 5 485
    Points
    5 485
    Par défaut
    Citation Envoyé par LapinGarou Voir le message
    Non mais bloodridge... Regardez un peu les jeux faits avec XNA....
    Oui je sais le graphisme ne fait pas tout mais vous vous tapez dessus pour une comparaison de jeux ... pas AAA, très très loin même... On dirait des jeux datant de l'époque de Quake 2 en moins beau...
    Bastion est pourtant conçu avec XNA et Monogame.
    Ce n'est pas du AAA mais c'est très joli.

    En fait le C++ n'a pas un si grand avantage car le C++ moderne use et abuse de mécanismes qui reproduisent les fonctionnalités d'un code managé (smart pointer et compagnie) et pas forcément de façon plus efficace. Car, oui, la stabilité et la productivité comptent aussi dans l'univers du jeu vidéo. Si bien qu'on se retrouve avec des codes C++ qui ne sont pas plus efficaces que leurs équivalents C# mais sont plus lourds et moins élégants.


    Néanmoins, par rapport à un programme normal où C# est presque toujours un meilleur choix que le C++ (si, promis), un domaine temps réel à hautes performances comme le JV a tout de même quelques spécificités qui donnent l'avantage au C++ :

    * Les dévs C#/Java vont faire des pieds et des mains pour éviter les instanciations, ce qui ruine leurs avantages naturels. Car même si en général un GC mark'n sweep générationnel est techniquement supérieur au comptage de références des shared_ptr C++, ce dernier mécanisme est déterministe et c'est plutôt un avantage dans le JV.

    * De nos jours l'optimisation se fait avant tout sur les algorithmes et la parallélisation, pas en disputant des poils de derrière de mouches à coups d'assembleur et autres bidouilles. Néanmoins ce genre de choses a encore sa place dans le JV, notamment avec les instructions vectorielles (SIMD), ce qui permet à un dév C++ de creuser un peu l'écart.

    * La qualité des compilateurs C++ est incomparable avec celle des JIT C#/Java qui, disons-le, laissent beaucoup à désirer. C'est là que se situe le gros des écarts de performances et il faut se tourner vers des solutions techniques tierces pour compiler ces langages en C++ afin d'obtenir de bonnes perfs ! Un comble puisqu'on perd au passage des informations qui pourraient être utiles à l'optimisation.

  2. #622
    Inactif  


    Homme Profil pro
    Doctorant sécurité informatique — Diplômé master Droit/Économie/Gestion
    Inscrit en
    Décembre 2011
    Messages
    9 012
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 31
    Localisation : France, Loire (Rhône Alpes)

    Informations professionnelles :
    Activité : Doctorant sécurité informatique — Diplômé master Droit/Économie/Gestion
    Secteur : Enseignement

    Informations forums :
    Inscription : Décembre 2011
    Messages : 9 012
    Points : 23 145
    Points
    23 145
    Par défaut
    Bonjour,

    Les dévs C#/Java vont faire des pieds et des mains pour éviter les instanciations, ce qui ruine leurs avantages naturels. Car même si en général un GC mark'n sweep générationnel est techniquement supérieur au comptage de références des shared_ptr C++
    Pourquoi donc ?
    Comme ça, j'aurais plutôt l'impression que les shared_ptr sont plus "légers" et permettent une libération de la mémoire plus "rapide" que les GCs.

  3. #623
    Inactif  

    Homme Profil pro
    Ingénieur test de performance
    Inscrit en
    Décembre 2003
    Messages
    1 986
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 49
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Ingénieur test de performance
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Décembre 2003
    Messages : 1 986
    Points : 2 605
    Points
    2 605
    Par défaut
    Bonjour.

    Le problème des JVM et autre Garbage Collector a déjà été soulevé par quelqu'un précédemment.

    Ce ne sont pas les performances intrinsèques des langages qui posent vraiment problème. D'ailleurs on peut faire des jeux vidéos avec presque tous les langages.

    Le problème, c'est avec des jeux vidéos qui poussent les machines dans leur dernier retranchement (comme Crysis par exemple). Pour ce type de jeu, mais même sans prendre un exemple aussi extrême, si la Jvm ou le GC pose des problèmes de performance, tu l'as souvent dans l'os. La société ne va pas recoder la Jvm ou le GC pour palier au problème. Et elle n'a pas trop envie de jeter un projet à la poubelle à cause de cela.

    En cas de problème de performance, le C++ permettra de la jouer finement, alors qu'une Jvm ou un Gc peuvent être totalement bloquant et sans issue.

    Je pense que c'est pour cette raison qu'à l'heure actuelle, le C++ est majoritaire sur ce type de projet.

  4. #624
    Expert confirmé Avatar de DonQuiche
    Inscrit en
    Septembre 2010
    Messages
    2 741
    Détails du profil
    Informations forums :
    Inscription : Septembre 2010
    Messages : 2 741
    Points : 5 485
    Points
    5 485
    Par défaut
    Citation Envoyé par Neckara Voir le message
    Comme ça, j'aurais plutôt l'impression que les shared_ptr sont plus "légers" et permettent une libération de la mémoire plus "rapide" que les GCs.
    • Le comptage de références est incapable de gérer les références circulaires.
    • Les deux sont en O(n). Mais les "n" sont un peu différents, à l'avantage du GC, et le coût unitaire également à l'avantage du GC.
    • En effet les compteurs doivent être mis à jour à chaque assignation via une opération atomique (une centaine de cycles grosso modo), ce qui affecte sensiblement les performances. Un GC, lui, n'a pas besoin de modifier les assignations puisqu'à partir de l'adresse de l'instruction courante et de la pile des appels il est capable d'obtenir la liste de tous les objets racines sur la pile ou dans les registres.
    • Le travail du GC est déféré aux moments de repos de l'application et en général il ne visite que les objets récemment créés. Les instructions à exécuter sont simples et peu nombreuses et si un large morceau de la mémoire doit être parcouru les accès produits sont souvent séquentiels.
    • Un GC générationnel peut naturellement énumérer et distinguer les objets permanents des objets temporaires, ce qui lui permet de réaliser efficacement des optimisations mémoire.


    Pour toutes ces raisons le GC est, en général, de très loin préférable et offre de meilleures performances. Sauf quand il n'y a aucun moment de repos, une exigence de flux constant ou une forte contrainte sur la mémoire nécessitant un nettoyage dès que possible.

  5. #625
    Inactif  


    Homme Profil pro
    Doctorant sécurité informatique — Diplômé master Droit/Économie/Gestion
    Inscrit en
    Décembre 2011
    Messages
    9 012
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 31
    Localisation : France, Loire (Rhône Alpes)

    Informations professionnelles :
    Activité : Doctorant sécurité informatique — Diplômé master Droit/Économie/Gestion
    Secteur : Enseignement

    Informations forums :
    Inscription : Décembre 2011
    Messages : 9 012
    Points : 23 145
    Points
    23 145
    Par défaut
    Citation Envoyé par DonQuiche Voir le message
    [*] Le comptage de références est incapable de gérer les références circulaires.
    Il me semble que std::weak_ptr a justement été conçu pour cela.

    En effet les compteurs doivent être mis à jour à chaque assignation via une opération atomique (une centaine de cycles grosso modo), ce qui affecte sensiblement les performances.
    Une centaine de cycle me semble un peu "gros" juste pour un déférencement et une incrémentation (1 cycle il me semble) .

    Un GC, lui, n'a pas besoin de modifier les assignations puisqu'à partir de l'adresse de l'instruction courante et de la pile des appels il est capable d'obtenir la liste de tous les objets racines sur la pile ou dans les registres.
    Mais lui en revanche va vérifier régulièrement si les objets sont encore "référencés" ou non. Donc au final n'use-t-il pas plus de cycles que le shared_ptr ?

    Le travail du GC est déféré aux moments de repos de l'application et en général il ne visite que les objets récemment créés. Les instructions à exécuter sont simples et peu nombreuses et si un large morceau de la mémoire doit être parcouru les accès produits sont souvent séquentiels.
    Ce qui peut d'ailleurs poser problème pour l'appel du destructeur (on ne sait pas vraiment exactement quand il sera appelé).
    Ensuite, certes les shared_ptr vont "consommer" des cycles et pas que pendant les "moments de repos", mais c'est le coût ajouté est tellement négligeable. De plus si on recherche vraiment les performances, rien n'empêche d'utiliser des pointeurs nus.
    Un avantage serait donc le choix, soit on prend un shared_ptr, soit un weak_ptr, soit un unique_ptr soit un pointeur nu (voir même d'utiliser sa propre méthode d'allocation) alors que les GC au final ne nous laissent pas vraiment de choix .

    J'ai donc du mal à comprendre qu'on puisse utiliser un GC.

  6. #626
    Expert confirmé Avatar de DonQuiche
    Inscrit en
    Septembre 2010
    Messages
    2 741
    Détails du profil
    Informations forums :
    Inscription : Septembre 2010
    Messages : 2 741
    Points : 5 485
    Points
    5 485
    Par défaut
    Citation Envoyé par Neckara Voir le message
    Il me semble que std::weak_ptr a justement été conçu pour cela.
    Oui c'est une gestion manuelle qui rompt avec le comptage de références. Et tous les graphes d'objets ne sont pas aussi simples que des arbres bidirectionnels, si bien qu'utiliser correctement ces weak_ptr peut être un casse-tête et une source de bogues. En tout cas c'est très loin d'être transparent, rien à voir avec le GC.

    Une centaine de cycle me semble un peu "gros" juste pour un déférencement et une incrémentation (1 cycle il me semble) .
    Justement non car c'est une incrémentation atomique. Le problème est que lorsque deux threads tentent simultanément d'incrémenter une variable, celle-ci n'est parfois incrémentée qu'une seul fois au total, si les instructions se sont produites dans l'ordre suivant :
    Thread1 : charger variable de la mémoire vers le registre
    Thread2 : charger variable de la mémoire vers le registre
    Thread2: incrémenter registre puis mettre le résultat en mémoire
    Thread1: incrémenter registre puis mettre le résultat en mémoire

    Donc ce problème de concurrence est prévenu via des "opérations atomiques" (std::atomic<int> en C++ 11 et InterlockedIncrement sous win32), lesquelles vont nécessiter des barrières mémoires et autres joyeusetés. Donc une centaine de cycles grosso modo, davantage s'il y a compétition entre les coeurs.

    Mais lui en revanche va vérifier régulièrement si les objets sont encore "référencés" ou non. Donc au final n'use-t-il pas plus de cycles que le shared_ptr ?
    D'un côté ton coût est proportionnel au nombre d'assignations, de l'autre il est proportionnel au nombre de nouveaux objets depuis la dernière passe du GC (c'est l'intérêt d'un algo générationnel). Le second est plus faible que le premier.

    Ce qui peut d'ailleurs poser problème pour l'appel du destructeur (on ne sait pas vraiment exactement quand il sera appelé).
    C'est vrai sauf qu'en pratique ce n'est pas un problème : les seules fois où on a besoin d'un destructeur en code managé c'est pour libérer des ressources non-managées. Or dans un tel cas de figure, assez rare, on préfère utiliser un idiome qui enferme le traitement consommant ces ressources dans un bloc qui libère explicitement les ressources à la fin et désenregistre le destructeur. L'exemple typique c'est celui du fichier :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    using (var file = new FileStream("c:\\truc.txt"))
    {
        ReadFile(file);
    }
    En pratique je dois écrire trois destructeurs par an et ils ne sont jamais exécutés sauf exception.


    Ensuite, certes les shared_ptr vont "consommer" des cycles et pas que pendant les "moments de repos", mais c'est le coût ajouté est tellement négligeable.
    En réalité le nombre total de cycles est très supérieur pour le comptage de références. D'ailleurs l'équipe ayant créé C# avait initialement préféré le comptage de références et c'est après avoir codé et comparé les deux solutions et vu les mesures qu'ils ont choisi le GC générationnel, qui battait à plate couture le comptage de références.

    Il n'y a guère que dans les scénarios que j'ai énoncé (mémoire contrainte, temps réel, utilisation cpu 100% permanente) que le comptage de références présente des avantages. Le reste du temps c'est plus lent et moins pratique.

    De plus si on recherche vraiment les performances, rien n'empêche d'utiliser des pointeurs nus.
    Oui c'est l'avantage du C++ : pouvoir tout faire. Sauf que ton temps de développement n'est pas infini, que tu as déjà perdu suffisamment de temps à gérer tes références cycliques et à écrire des destructeurs, et que de toute façon ce genre d'optimisation est peu rentable par rapport à un choix judicieux d'algorithme et de parallélisation. Et à ce dernier titre pouvoir écrire rapidement un code fiable, simple et lisible est un gros avantage.

  7. #627
    Inactif  

    Homme Profil pro
    Ingénieur test de performance
    Inscrit en
    Décembre 2003
    Messages
    1 986
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 49
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Ingénieur test de performance
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Décembre 2003
    Messages : 1 986
    Points : 2 605
    Points
    2 605
    Par défaut
    Bonjour.

    Citation Envoyé par DonQuiche Voir le message


    Donc ce problème de concurrence est prévenu via des "opérations atomiques" (std::atomic<int> en C++ 11 et InterlockedIncrement sous win32), lesquelles vont nécessiter des barrières mémoires et autres joyeusetés. Donc une centaine de cycles grosso modo, davantage s'il y a compétition entre les coeurs.
    J'utilise InterlockedIncrement dans tous mes projets MediaFoundation et anciennement DirectShow. Je n'y ai jamais rencontré de problème de performance.

    Première question : où peut-on avoir la documentation qui explique que les opérations atomiques telles que InterlockedIncrement peuvent prendre 100 cycles processeur lors d'un accès concurrent.

    Deuxième question : tu dis que le Garbage Collector gère mieux la chose. Une explication/démonstration serait le bienvenue. Je reste sur ma faim là.

    Citation Envoyé par DonQuiche Voir le message
    Justement non car c'est une incrémentation atomique. Le problème est que lorsque deux threads tentent simultanément d'incrémenter une variable, celle-ci n'est parfois incrémentée qu'une seul fois au total, si les instructions se sont produites dans l'ordre suivant :
    Thread1 : charger variable de la mémoire vers le registre
    Thread2 : charger variable de la mémoire vers le registre
    Thread2: incrémenter registre puis mettre le résultat en mémoire
    Thread1: incrémenter registre puis mettre le résultat en mémoire
    InterlockedIncrement permet d'éviter ce genre de situation et le garantit. Mais on a pas dû lire la même documentation.

  8. #628
    Expert confirmé Avatar de DonQuiche
    Inscrit en
    Septembre 2010
    Messages
    2 741
    Détails du profil
    Informations forums :
    Inscription : Septembre 2010
    Messages : 2 741
    Points : 5 485
    Points
    5 485
    Par défaut
    Bonjour à toi.

    Citation Envoyé par moldavi Voir le message
    J'utilise InterlockedIncrement dans tous mes projets MediaFoundation et anciennement DirectShow. Je n'y ai jamais rencontré de problème de performance.
    100 cycles ce n'est pas énorme. Mais quand tu dois payer ce prix à chaque assignation c'est un autre problème : à 60 ips, entre deux images, tu passes de 10M à 100k assignations possibles.

    Première question : où peut-on avoir la documentation qui explique que les opérations atomiques telles que InterlockedIncrement peuvent prendre 100 cycles processeur.
    C'est un ordre de grandeur communément admis, rien de plus. Mais tu peux facilement le vérifier par toi-même.

    Deuxième question : tu dis que le Garbage Collector gère mieux la chose. Une explication/démonstration serait le bienvenue.
    Je l'ai déjà expliqué : le GC n'a pas besoin de modifier les assignations. Tout ce dont il a besoin c'est de la pile des appels avec l'adresse de l'instruction courante pour chacune.

    L'ago est en gros le suivant :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    fonction RamasseMiettes()
       valeurAccessible = !valeurAccessible // bascule à chaque passe
     
       Marquer(valeurAccessible)
     
       PromouvoirGeneration(1, valeurAccessible)
       NettoyerGeneration(1)
     
       PromouvoirGeneration(0, valeurAccessible)
       NettoyerGeneration(0)
    fin
     
    fonction Marquer(valeurAccessible)
       pour chaque instruction dans pile_des_appels
          var racinesAccessibles = dictionaireRacinesAccessibles[instruction]
          pour chaque référence dans racinesAccessibles
             MarquerRacine(référence, valeurAccessible)
          fin
       fin
    fin
     
    fonction MarquerRacine(référence, valeurAccessible)
       si reference.generation > 1 alors quitter
       si reference.accessible == valeurAccessible alors quitter
       reference.accessible = valeurAccessible
     
       var type = référence.type
       var offsetsMembres = type.listeRéférencesMembres
       pour chaque offset dans offsetsMembres
         var membre = référence + offset
         MarquerRacine(membre, valeurAccessible)
      fin
    fin
     
    fonction PromouvoirGeneration(generation, valeurAccessible)
       var listeActuelle = listAllocations[generation]
       var listeSuivante = listAllocations[generation + 1]
     
       pour chaque référence dans listeActuelle
          si référence .accessible alors 
             Ajouter référence à listeSuivante
             référece.generation++
         fin
       fin
    fin

    Simple, rapide et efficace, en O(profondeur pile appels + nombre nouvelles allocations). Le cas de la génération 2 est un peu plus compliqué mais en général elle n'est pas visitée et l'algo se résume alors à ce que j'ai donné. A mon avis c'est bandwidth-bound donc on pourrait calculer son efficacité par rapport à la bande passante mémoire.


    InterlockedIncrement permet d'éviter ce genre de situation et le garantit. Mais on a pas dû lire la même documentation.
    Bien sûr qu'il évite ce genre de situation, c'est même sa raison d'être. Tu m'as mal compris : Neckara pensait qu'une incrémentation simple suffisait, j'expliquais pourquoi il fallait une incrémentation atomique.

  9. #629
    Inactif  


    Homme Profil pro
    Doctorant sécurité informatique — Diplômé master Droit/Économie/Gestion
    Inscrit en
    Décembre 2011
    Messages
    9 012
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 31
    Localisation : France, Loire (Rhône Alpes)

    Informations professionnelles :
    Activité : Doctorant sécurité informatique — Diplômé master Droit/Économie/Gestion
    Secteur : Enseignement

    Informations forums :
    Inscription : Décembre 2011
    Messages : 9 012
    Points : 23 145
    Points
    23 145
    Par défaut
    Citation Envoyé par DonQuiche Voir le message
    Oui c'est une gestion manuelle qui rompt avec le comptage de références. Et tous les graphes d'objets ne sont pas aussi simples que des arbres bidirectionnels, si bien qu'utiliser correctement ces weak_ptr peut être un casse-tête et une source de bogues. En tout cas c'est très loin d'être transparent, rien à voir avec le GC.
    Il faudrait que j'ai un exemple de graphes d'objets "compliqué pour voir.


    Justement non car c'est une incrémentation atomique. Le problème est que lorsque deux threads tentent simultanément d'incrémenter une variable, celle-ci n'est parfois incrémentée qu'une seul fois au total, si les instructions se sont produites dans l'ordre suivant :
    Thread1 : charger variable de la mémoire vers le registre
    Thread2 : charger variable de la mémoire vers le registre
    Thread2: incrémenter registre puis mettre le résultat en mémoire
    Thread1: incrémenter registre puis mettre le résultat en mémoire
    Autant pour moi .

    D'un côté ton coût est proportionnel au nombre d'assignations, de l'autre il est proportionnel au nombre de nouveaux objets depuis la dernière passe du GC (c'est l'intérêt d'un algo générationnel). Le second est plus faible que le premier.
    D'où tiens-tu cela ?


    C'est vrai sauf qu'en pratique ce n'est pas un problème : les seules fois où on a besoin d'un destructeur en code managé c'est pour libérer des ressources non-managées. Or dans un tel cas de figure, assez rare, on préfère utiliser un idiome qui enferme le traitement consommant ces ressources dans un bloc qui libère explicitement les ressources à la fin et désenregistre le destructeur. L'exemple typique c'est celui du fichier :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    using (var file = new FileStream("c:\\truc.txt"))
    {
        ReadFile(file);
    }
    En pratique je dois écrire trois destructeurs par an et ils ne sont jamais exécutés sauf exception.
    Mais si on a besoin d'utiliser une ressource qui doit durer plus d'un bloc ?
    Exemple pour les BDD ou des fichiers de logs, qu'on ne sait pas forcément quand ils seront fermés.


    Oui c'est l'avantage du C++ : pouvoir tout faire. Sauf que ton temps de développement n'est pas infini, que tu as déjà perdu suffisamment de temps à gérer tes références cycliques et à écrire des destructeurs, et que de toute façon ce genre d'optimisation est peu rentable par rapport à un choix judicieux d'algorithme et de parallélisation. Et à ce dernier titre pouvoir écrire rapidement un code fiable, simple et lisible est un gros avantage.
    En quoi écrire des constructeur est une "perte de temps" ?
    Avec le principe RAII et les shared_ptr/unique_ptr, on a pas grand chose à faire au niveau de la mémoire et on peut alors se charger de faire des actions plus spécifiques qu'on retrouvera de toute façon avec un GC.
    Par rapport au C#, je ne vois pas en quoi le code serait plus "rapide" que le C++ par exemple pour peu qu'on utilise les bonnes bibliothèques...

    Citation Envoyé par DonQuiche Voir le message
    100 cycles ce n'est pas énorme. Mais quand tu dois payer ce prix à chaque assignation c'est un autre problème : à 60 ips, entre deux images, tu passes de 10M à 100k assignations possibles.

    En C++11 (en C++14 on a std::make_ptr ou un truc du genre), il te suffit de faire cela :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    std::shared_ptr ptr(new Pixels[BEAUCOUP]);
    Donc une assignation d'une image ne coûte absolument rien.
    Par contre, si le GC doit tester chaque élément du tableau... Même s'il ne le fait pas souvent c'est quand même assez .

  10. #630
    Expert confirmé Avatar de DonQuiche
    Inscrit en
    Septembre 2010
    Messages
    2 741
    Détails du profil
    Informations forums :
    Inscription : Septembre 2010
    Messages : 2 741
    Points : 5 485
    Points
    5 485
    Par défaut
    Citation Envoyé par Neckara Voir le message
    Il faudrait que j'ai un exemple de graphes d'objets "compliqué pour voir.
    Des graphes avec des cycles ce n'est pas ce qui manque en info : topologies réseaux, DFA, buffers circulaires, maillages de surface, etc.

    D'où tiens-tu cela ?
    De laquelle des trois assertions parlais-tu ? Si c'est la troisième il est très rare qu'une instanciation ne soit pas suivie d'une assignation alors que la réciproque n'est pas vraie, si bien qu'en général le nombre d'assignations est très supérieur au nombre d'instanciations.

    Mais si on a besoin d'utiliser une ressource qui doit durer plus d'un bloc ?
    Quand ça arrive tu peux soit laisser le GC appeler le finaliseur plus tard, soit, comme dans un langage non-managé, mettre en place un suivi de la durée de vie de l'objet afin d'ordonner manuellement son nettoyage à la fin s'il faut nettoyer dès que possible.

    Mais peu importe à vrai dire : tu abordais le non-déterminisme des finaliseurs et je voulais simplement insister sur le fait qu'ils sont très rares en C#, typiquement associés aux ressources non-managées et qu'il est toujours possible d'ordonner un nettoyage manuel anticipé et déterministe.

    En quoi écrire des constructeur est une "perte de temps" ?
    Je parlais des destructeurs et écrire n'importe quoi est plus lent que de ne rien écrire.

    Avec le principe RAII et les shared_ptr/unique_ptr, on a pas grand chose à faire au niveau de la mémoire
    C'est quand même plus lourd parce qu'il faut toujours spécifier weak_ptr ou shared_ptr ou unique_ptr ou... Le code reste encombré par ces annotations sur la gestion de la mémoire. Qui plus est il faut continuer en permanence à vérifier mentalement si la gestion mise en place convient et il y a toujours des risques de création de références circulaires inattendues.

    La gestion de la mémoire est toujours là, il faut toujours s'en préoccuper. C'est simplement facilité.

    Donc une assignation d'une image ne coûte absolument rien.
    Par contre, si le GC doit tester chaque élément du tableau... Même s'il ne le fait pas souvent c'est quand même assez .
    Le GC ne visiterait ce tableau que si son élément est un type référence, il ne visitera pas un tableau d'entiers. Maintenant, oui, un large tableau de références devrait être visité entièrement. Sauf que :
    * Les grands tableaux ont tendance à persister dans la mémoire et se retrouvent donc en génération 2 où ils ne sont plus visités sauf cas exceptionnel.
    * On ne fait pas de grands tableaux de référence. Dans ton exemple, en C#, Pixel aurait été un type valeur. Et en C++ on l'aurait utilisé comme tel.


    Enfin je n'ai pas prétendu que le GC était universellement meilleur, j'ai le premier souligné qu'il était un inconvénient pour le JV. En revanche il est presque toujours meilleur et offre presque toujours de meilleures performances. Dans la très grande majorité des cas la gestion manuelle de la mémoire du C++ n'est plus pertinente aujourd'hui, même facilitée : le rapport coût/bénéfices n'est pas intéressant.

  11. #631
    Inactif  


    Homme Profil pro
    Doctorant sécurité informatique — Diplômé master Droit/Économie/Gestion
    Inscrit en
    Décembre 2011
    Messages
    9 012
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 31
    Localisation : France, Loire (Rhône Alpes)

    Informations professionnelles :
    Activité : Doctorant sécurité informatique — Diplômé master Droit/Économie/Gestion
    Secteur : Enseignement

    Informations forums :
    Inscription : Décembre 2011
    Messages : 9 012
    Points : 23 145
    Points
    23 145
    Par défaut
    Citation Envoyé par DonQuiche Voir le message
    Des graphes avec des cycles ce n'est pas ce qui manque en info : topologies réseaux, DFA, buffers circulaires, maillages de surface, etc.
    Si c'est simplement un problème de cycles, on a des solutions assez simples avec les std::weak_ptr ou en utilisant un parcours de l'arbre (qui sera presque toujours implémenté pour un arbre) pour libérer les ressources. Je ne vois pas où serait la complexité.


    De laquelle des trois assertions parlais-tu ? Si c'est la troisième il est très rare qu'une instanciation ne soit pas suivie d'une assignation alors que la réciproque n'est pas vraie, si bien qu'en général le nombre d'assignations est très supérieur au nombre d'instanciations.
    Je veux bien reconnaître qu'on puisse faire trois fois plus d'affectations que d’instanciations, mais comme le GC vérifie tout plus ou moins régulièrement, soit :
    - il tarde à libérer la mémoire/les ressources (ce qui est un peu dommage) ;
    - il fait pas mal de vérifications, donc au final ça devrait coûter plus que 3 malheureuses affectations/std::shared_ptr créé.


    Quand ça arrive tu peux soit laisser le GC appeler le finaliseur plus tard, soit, comme dans un langage non-managé, mettre en place un suivi de la durée de vie de l'objet afin d'ordonner manuellement son nettoyage à la fin s'il faut nettoyer dès que possible.
    Donc grosso-modo implémenter une sorte de shared_ptr ?

    Je parlais des destructeurs et écrire n'importe quoi est plus lent que de ne rien écrire.
    Ton raisonnement ne tient pas.
    On a aucune obligation d'écrire un destructeur. Si on en écrit un, c'est qu'en C# il te faudra un finaliseur ou un "suivi de la durée de vie de l'objet".


    C'est quand même plus lourd parce qu'il faut toujours spécifier weak_ptr ou shared_ptr ou unique_ptr ou...
    Je ne dirais pas plus lourd, mais plus précis.
    std::unique_ptr ou std::weak_ptr par exemple n'ont pas d'équivalent dans le GC.
    Rien qu'avec le type du pointeur on peut savoir :
    - si on va avoir l'unique instance ou non de l'objet (pratique pour du multi-thread) ;
    - si l'objet vivra au moins jusqu'à ce qu'on ai fini de l'utiliser ;
    - si l'objet peut être détruit pendant qu'on l'utilise (ce qui peut être cohérent et voulu).

    Informations qu'on a pas directement avec un GC.

    Le code reste encombré par ces annotations sur la gestion de la mémoire.
    Qui plus est il faut continuer en permanence à vérifier mentalement si la gestion mise en place convient et il y a toujours des risques de création de références circulaires inattendues.
    Certes, gérer la mémoire demande plus de connaissances, plus d'habitude et de bonnes pratiques, mais ce n'est pas pour autant que cela "encombre" le code.
    Et puis bon, le risque des références circulaires est quand même assez faible.

    La gestion de la mémoire est toujours là, il faut toujours s'en préoccuper. C'est simplement facilité.
    Je ne pense pas cela car on nous "impose" une unique gestion de la mémoire. Alors que sans GC, on peut utiliser la méthode la plus appropriée pour avoir quelque chose de bien plus clair.
    Un GC n'empêche pas de se préoccuper de la mémoire, surtout dans des boucles .


    En revanche il est presque toujours meilleur et offre presque toujours de meilleures performances. Dans la très grande majorité des cas la gestion manuelle de la mémoire du C++ n'est plus pertinente aujourd'hui, même facilitée : le rapport coût/bénéfices n'est pas intéressant.
    Je ne suis pas vraiment convaincu

  12. #632
    Expert confirmé Avatar de DonQuiche
    Inscrit en
    Septembre 2010
    Messages
    2 741
    Détails du profil
    Informations forums :
    Inscription : Septembre 2010
    Messages : 2 741
    Points : 5 485
    Points
    5 485
    Par défaut
    Citation Envoyé par Neckara Voir le message
    Si c'est simplement un problème de cycles
    Soient deux sommets A et B et un connecteur A -> B, comment décides-tu quand utiliser un weak_ptr au lieu d'un shared_ptr ? Pour une structure comme un arbre bidirectionnel toujours référencé par le noeud plus haut le problème est trivial mais pour un graphe quelconque ou un graphe simple référencé d'une façon quelconque il est impossible de décider à l'écriture et tu dois alors maintenir une structure de données pour le décider à l'exécution. Et ça peut être très lourd et complexe, et encombrer ton code.

    Je veux bien reconnaître qu'on puisse faire trois fois plus d'affectations que d’instanciations, mais comme le GC vérifie tout plus ou moins régulièrement
    Le GC ne vérifie les objets à longue durée de vie que lorsque le système réclame de la mémoire, ce qui ne pose aucun problème : cette mémoire ne manque à personne.

    Donc grosso-modo implémenter une sorte de shared_ptr ?
    Par exemple, oui. Nous parlons des cas où tu veux mettre en oeuvre une finalisation précoce sans pouvoir déterminer exactement quand elle surviendra, ce qui a dû m'arriver une fois il y a deux ou trois ans. Encore une fois je n'ai pas prétendu que le GC était toujours meilleur, seulement presque toujours.



    Nous en sommes arrivés au point où nous n'avons plus grand chose à dire : la gestion manuelle de la mémoire du C++ est toujours explicite et donc plus lourde, et presque toujours moins performante. Pour autant tu soutiens qu'elle est meilleure pour les 0,1% de cas où elle sera plus avantageuse et pour la liberté qu'elle te donne de passer des heures à tout gérer manuellement pour gagner des poils de mouches de performances en massacrant ton code au cas plus que très improbable où ce serait un jour la meilleure chose à faire pour optimiser ton code.

    Je ne souscris pas à cette appréciation et la comptage de référence est à mon sens une mauvaise solution qui se trouve toutefois avoir des avantages lorsque la mémoire est contrainte ou qu'il existe une contrainte de temps réel. C'est bien pour le jeu vidéo, c'est mauvais en général.

  13. #633
    Membre émérite Avatar de Djakisback
    Profil pro
    Inscrit en
    Février 2005
    Messages
    2 021
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Février 2005
    Messages : 2 021
    Points : 2 278
    Points
    2 278
    Par défaut
    Il y a un problème fréquent avec le GC de la JVM standard, le thread du GC prend la main sur les threads de l'appli principale car la RAM allouée est saturée ou va l'être. Il y a peut-être moyen de régler un peu plus finement le comportement du GC mais je n'ai jamais trouvé de solutions concluantes, à part augmenter la RAM allouée à la JVM. On peut donc parfois se retrouver avec des quasi-freezes de l'appli pendant plusieurs secondes. On pourrait se dire que c'est la faute du dev qui charge trop de trucs en mémoire ou qui fait nimp mais le problème est que cela peut se produire même si la mémoire n'est pas encore saturée. Ex. :
    - lancement d'une première méthode qui sature pratiquement la RAM. La durée de vie des instances est le bloc de la méthode
    - lancement d'une 2e méthode qui sature pratiquement la RAM. Il arrive parfois que le GC soit encore en train d'essayer de libérer les ressources du premier appel, du coup le thread principal passe en priorité low, saccade, etc. mais s'il lance encore une autre méthode lourde, il y a ensuite un décalage du GC, le GC semble ne plus pouvoir se rattraper et prend complètement la main sur l'appli pour pouvoir libérer les ressources, une espèce d'effet boule de neige.

    C'est un exemple ramené au plus simple d'un phénomène que j'ai observé plusieurs fois, que j'avoue n'avoir pas compris complètement mais qui en tout cas me semble problématique pour du temps réel.
    Si quelqu'un a plus d'info sur la modification du comportement du GC de la JVM ça m'intéresse

    La question du topic est assez vague mais en tout cas dans le cadre du jeu temps réel, on tombe exactement dans le cas de figure que tu décris DonQuiche :
    - flux constant et peu de temps de repos (tracé selon xx fps)
    - commodité (obligation ?) de pouvoir avoir la RAM à full à un instant t puis de libérer tout ou partie à un instant t + 1 (chargement/déchargement de ressources muti-threadé ou non, modèles, maps, sprites, etc.)

    et où à mon sens le GC de la JVM n'est pas adapté (en tout cas avec son comportement standard).

    Sinon dire que la gestion de mémoire manuelle en C++ (je ne parle pas des shared_ptr) est presque toujours moins performante qu'un GC m'étonne pas mal, surtout dans le cadre du temps réel.
    Vive les roues en pierre

  14. #634
    Expert confirmé Avatar de DonQuiche
    Inscrit en
    Septembre 2010
    Messages
    2 741
    Détails du profil
    Informations forums :
    Inscription : Septembre 2010
    Messages : 2 741
    Points : 5 485
    Points
    5 485
    Par défaut
    Citation Envoyé par Djakisback Voir le message
    Sinon dire que la gestion de mémoire manuelle en C++ (je ne parle pas des shared_ptr) est presque toujours moins performante qu'un GC m'étonne pas mal, surtout dans le cadre du temps réel.
    Non, non, ce n'est pas rapport à la gestion manuelle, je comparais l'usage du comptage de références avec le GC. Si tu utilises des pointeurs nus tu peux souvent faire quelque chose de bien plus rapide que les deux mécanismes pré-cités, à moins que tu sois dans une situation concurrente où tu te retrouves à devoir réinventer les smart pointers. Simplement une gestion manuelle n'est pas très intéressante : la gestion mémoire ne pèse pas très lourd et retravailler les algorithmes ou mieux paralléliser est presque toujours plus rentable que d'affiner le code avec des micro-optimisations.

    Pour le reste je ne connais pas assez la JVM mais ce ne serait pas un problème de fragmentation mémoire ? Sur la plateforme dotnet les objets de grande taille sont alloués sur un tas séparé (large object heap) qui n'est jamais compacté (ces grands objets sont en général permanents - le jeu est encore un contre-exemple), d'où une fragmentation dans certains cas si on n'intervient pas. Le problème existe aussi en C/C++, simplement les conséquences sont moins nombreuses (saturation mémoire et accès moins performants).

    Pour cette raison le chargement de ces grands objets fait souvent l'objet d'une attention particulière, que ce soit en C++ ou en Java/C#, notamment pas la mise en oeuvre de recyclage de tableau en code managé et d'allocateurs personnalisés en C++ (qui revient à peu près au même).

  15. #635
    Membre émérite Avatar de Djakisback
    Profil pro
    Inscrit en
    Février 2005
    Messages
    2 021
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Février 2005
    Messages : 2 021
    Points : 2 278
    Points
    2 278
    Par défaut
    Citation Envoyé par DonQuiche Voir le message
    Non, non, ce n'est pas rapport à la gestion manuelle, je comparais l'usage du comptage de références avec le GC. Si tu utilises des pointeurs nus tu peux souvent faire quelque chose de bien plus rapide que les deux mécanismes pré-cités, à moins que tu sois dans une situation concurrente où tu te retrouves à devoir réinventer les smart pointers. [...]
    Ah ok, c'est ce que j'avais cru comprendre mais j'en étais pas sûr

    Citation Envoyé par DonQuiche Voir le message
    Pour le reste je ne connais pas assez la JVM mais ce ne serait pas un problème de fragmentation mémoire ? Sur la plateforme dotnet les objets de grande taille sont alloués sur un tas séparé (large object heap) qui n'est jamais compacté (ces grands objets sont en général permanents - le jeu est encore un contre-exemple), d'où une fragmentation dans certains cas si on n'intervient pas. Le problème existe aussi en C/C++, simplement les conséquences sont moins nombreuses (saturation mémoire et accès moins performants).

    Pour cette raison le chargement de ces grands objets fait souvent l'objet d'une attention particulière, que ce soit en C++ ou en Java/C#, notamment pas la mise en oeuvre de recyclage de tableau en code managé et d'allocateurs personnalisés en C++ (qui revient à peu près au même).
    Merci pour ces précisions. Effectivement il faudrait que je refasse des recherches et que je reteste certains truc, c'est peut-être une piste.
    Vive les roues en pierre

  16. #636
    Inactif  

    Homme Profil pro
    Ingénieur test de performance
    Inscrit en
    Décembre 2003
    Messages
    1 986
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 49
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Ingénieur test de performance
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Décembre 2003
    Messages : 1 986
    Points : 2 605
    Points
    2 605
    Par défaut
    Bonjour DonQuiche.

    Citation Envoyé par DonQuiche Voir le message
    Tu m'as mal compris : Neckara pensait qu'une incrémentation simple suffisait, j'expliquais pourquoi il fallait une incrémentation atomique.
    En effet, je comprends mieux maintenant.

    InterlockedIncrement permet d'incrémenter un variable de façon "safe" en environnement multithreadé. Donc effectivement, c'est normal que cela demande un certain nombre de cycle processeur afin d'assurer le côté "safe".

    Citation Envoyé par DonQuiche Voir le message
    Simple, rapide et efficace, en O(profondeur pile appels + nombre nouvelles allocations). Le cas de la génération 2 est un peu plus compliqué mais en général elle n'est pas visitée et l'algo se résume alors à ce que j'ai donné. A mon avis c'est bandwidth-bound donc on pourrait calculer son efficacité par rapport à la bande passante mémoire.
    Désolé mais je ne comprends rien à cette démonstration.

    Les Jvm et Gc s'éxécutent dans un autre processus. Ils doivent donc utiliser les mêmes fonctionnalités de "memory barrier", tel que InterlockedIncrement. A ce niveau là je ne vois pas en quoi ils seraient plus performants.

    Citation Envoyé par DonQuiche Voir le message
    En fait le C++ n'a pas un si grand avantage car le C++ moderne use et abuse de mécanismes qui reproduisent les fonctionnalités d'un code managé (smart pointer et compagnie) et pas forcément de façon plus efficace. Car, oui, la stabilité et la productivité comptent aussi dans l'univers du jeu vidéo.
    C'est certain qu'une Jvm ou Gc gèrent mieux les pointeurs qu'une application C++ native, c'est leur rôle.

    Je peut affirmer, comme tu le fais, que les jeux vidéos n'utilisent pas les "smart pointer". Je n'ai aucune preuve, et j'attends les tiennes... Mais si je développais des jeux vidéos, les "smart pointer" seraient interdits.

    PS: Je n'utilise plus les "smart pointer" et assimilés depuis deux ans. Le C++ permet de s'abstraire de ce genre de chose, grâce aux destructeurs et à d'autres techniques. Et ce n'est pas une question d'optimisation, c'est juste que je trouve cela inutile avec une bonne architecture.

    PS2: En C++, les "smart pointer", c'est pour les gros fainéants. Autant qu'ils fassent du Java ou du C#. Bon après je comprends, un bon développeur est un développeur fainéant.

  17. #637
    Expert confirmé Avatar de DonQuiche
    Inscrit en
    Septembre 2010
    Messages
    2 741
    Détails du profil
    Informations forums :
    Inscription : Septembre 2010
    Messages : 2 741
    Points : 5 485
    Points
    5 485
    Par défaut
    Citation Envoyé par moldavi Voir le message
    Les Jvm et Gc s'éxécutent dans un autre processus. Ils doivent donc utiliser les mêmes fonctionnalités de "memory barrier", tel que InterlockedIncrement. A ce niveau là je ne vois pas en quoi ils seraient plus performants.
    En réalité le GC dotnet met les threads en pause durant son traitement, c'est ce qui le rend rapide mais peu adéquat pour le jeu vidéo.

    Il existe aussi une version concurrente du GC : je n'en connais pas l'implémentation mais je gage qu'elle place une barrière de synchronisation avant chaque appel de méthode pour que le GC puisse traiter de façon concurrente tout le bas de la pile et ne geler le thread qu'une fois arrivé au sommet. Ou peut-être fait-elle simplement une mise en file atomique, ce qui serait équivalent en termes de performances au comptage de référence.


    Je peut affirmer, comme tu le fais, que les jeux vidéos n'utilisent pas les "smart pointer".
    Je n'ai pas affirmé ça et je pense même qu'ils sont utilisés dans le jeu vidéo. Je sais d'ailleurs pour sûr que Crytek, au moins, les utilise. Par contre à mon avis ils utilisent en général des smart-pointers qui ne sont pas thread-safe.

    Ou peut-être ne s'embêtent-ils même pas et utilisent-ils une implémentation standard puisque de toute façon le coût des smart pointers reste acceptable et qu'avoir un code stable et lisible est important, et que mieux vaut dépenser du temps de développement ailleurs que sur la gestion manuelle de la mémoire.

    Bon après je comprends, un bon développeur est un développeur fainéant.
    En effet. ^^ C'est cette façon de faire qui génère le moins de bogues et laisse tout le loisir de se concentrer sur les algorithmes et les structures de données qui sont les véritables gisements d'optimisation.

  18. #638
    Inactif  
    Homme Profil pro
    Développeur multimédia
    Inscrit en
    Février 2021
    Messages
    16
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Développeur multimédia
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Février 2021
    Messages : 16
    Points : 0
    Points
    0
    Par défaut
    Evidemment qu'on peut faire des jeux en java... après je peux pas donner mon avis en tant que développeur parce que j'ai jamais essayé de faire des jeux avec java. En tant que joueur je peux pas dire grand chose non plus parce que des jeux en java y'en a très peu...

Discussions similaires

  1. Réponses: 39
    Dernier message: 13/07/2018, 04h48
  2. L’interview technique est-il adapté pour les recrutements ?
    Par Cedric Chevalier dans le forum Actualités
    Réponses: 103
    Dernier message: 08/07/2013, 09h38
  3. [Autre] HTML5 est-il adapté pour les jeux sur le Web ?
    Par Hinault Romaric dans le forum Publications (X)HTML et CSS
    Réponses: 42
    Dernier message: 22/01/2012, 12h17
  4. HTML5 est-il adapté pour les jeux sur le Web ?
    Par Hinault Romaric dans le forum Balisage (X)HTML et validation W3C
    Réponses: 42
    Dernier message: 22/01/2012, 12h17

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo