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

Python Discussion :

Optimisation de code


Sujet :

Python

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre éclairé

    Inscrit en
    Novembre 2008
    Messages
    442
    Détails du profil
    Informations forums :
    Inscription : Novembre 2008
    Messages : 442
    Par défaut Optimisation de code
    Bonjour,
    J'ai développé un outil pour générer des jeux de donnés fictives en python, l'objectif étant d'avoir des bases avec des données proches de ce qu'on pourrait trouver en prod en terme de volumétrie et respect de règles fonctionnelles mais avec des données complètement bidon pour ne pas avoir à me préoccuper des questions de confidentialité extrêmement présents dans ma branche. En outre, je n'ai pas besoin d'existant. Je peux créer des bases pour une application complètement nouvelle.

    J'ai construit cet outil au fur et à mesure de mes propres besoins et il commence à devenir suffisamment intéressant pour que j'essaie de l'optimiser un peu.

    L'idée de départ était d'avoir une moulinette très hautement paramétrable pour permettre une adaptation à toutes sortes de situations.

    Présentation rapide du fonctionnement
    Je paramètre la structure d'alimentation de la base sous forme d'une arborescence yaml et, pour chaque table, je crée un fichier texte qui contient (en gros) des fonctions que le moteur appelle. Au démarrage de l'application, le mapping entre les champs et les fonctions est stocké dans des objets/dictionnaires (correspondant aux entités) puis le programme consiste en une imbrication de boucles qui décrivent l'arborescence paramétrée et appels des fonctions d'alimentation des champs.
    Les données de référence (valeurs et probabilités, principalement) sont stockées dans une ou plusieurs bases SQLite et les données de sorties sont également écrites dans une ou plusieurs bases SQLite (il est possible d'avoir des bases de travail pour écrire des données intermédiaires mais non souhaitées dans le résultat).
    Je peux facilement intégrer du hasard et des probabilités d'apparitions de valeurs respectant éventuellement des distributions plus ou moins gaussiennes.
    Tout cela fonctionne pas mal et, au final, pour créer une nouvelle base, c'est relativement rapide. Le gros du travail est dans le paramétrage. Je n'ai pas ou très peu de code à écrire (juste les fonctions qui manquent pour des cas spécifiques non encore rencontrés) et ça tourne bien.

    Sauf qu'en terme de performance, je pense qu'il y a pas mal de marge de progression (je génère environ 100 Mo/h).

    La question
    Je serais intéressé par quelques avis d'orientation pour améliorer tout cela le plus efficacement possible avant de me lancer dans des expérimentations éventuellement très lourdes en terme d'investissement pour un retour nul ou quasi nul.

    Quelques réflexions :

    - Serait-il pertinent d'essayer de passer cette base de code en Cython. J'ai peur que ce soit difficile pour un résultat incertain car l'application est très dynamique : les fonctions appelées pour alimenter les champs des tables sont écrites dans les fichiers de paramétrage et les branchements se font à l'initialisation et à l'exécution, avec parfois un peu d'"exec" (la sécurité n'est pas une priorité : cette moulinette n'est pas censée être utilisée autrement que sur mon poste local). D'un autre côté, certaines fonctions ont des variables qui pourraient sans doute être typées de façon statiques. Je ne connais pas cython et en particulier le niveau de modification de code que cela implique pour un gain intéressant et j'ai donc du mal à mesurer l'intérêt de cette solution qui me semble la plus réaliste en terme de réécriture (j'avais pensé à tout réécrire en Rust mais là, le réalisme m'amène à penser que la marche est vraiment haute)

    - multiprocessing ? multithreading ? Ces options sont certainement prometteuses... La génération est un enchevêtrement de boucles qui décrit une arborescence. Les boucles filles sont, bien sûr, dépendantes de leurs parents mais à la racine, chaque tour est indépendant du précédent. On pourrait tout à fait scinder la génération en autant de morceaux à traiter en parallèle. Les questions sont relatives au fait que la (ou les) base(s) de référence (qui contient les valeurs et les probabilités à respecter), et la base de sortie sont, du coup, à partager entre les processus ou les threads. Est-ce une limite ? Faut-il prévoir une écriture dans autant de bases que de process/thread lancés avec réconciliation à la fin ? Quel est le plus adapté ? Intuitivement, je dirais que le multiprocessing serait bien pour éclater la boucle racine et le multithreading pour les écritures en base mais je ne vois pas trop comment concilier les deux. En fait, je ne vois pas très bien comment utiliser le multithreading dans ce cas...

    Merci pour vos éclairages.

  2. #2
    Expert confirmé
    Avatar de fred1599
    Homme Profil pro
    Lead Dev Python
    Inscrit en
    Juillet 2006
    Messages
    4 899
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Meurthe et Moselle (Lorraine)

    Informations professionnelles :
    Activité : Lead Dev Python
    Secteur : Arts - Culture

    Informations forums :
    Inscription : Juillet 2006
    Messages : 4 899
    Par défaut
    Hello,

    Tout ça c'est du blabla technique, utilisez des outils python adaptés pour vérifier où se trouvent les goulots d'étranglement.
    Ensuite, créez votre fonction de tests et posez là ici et les résultats.

    Nous pourrons discuter ensuite de la bonne démarche à suivre si besoin...
    Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
    La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)

  3. #3
    Membre éclairé

    Inscrit en
    Novembre 2008
    Messages
    442
    Détails du profil
    Informations forums :
    Inscription : Novembre 2008
    Messages : 442
    Par défaut
    Je ne vois pas ce que vous appelez blabla technique. Inutile d'être condescendant.
    Il s'agit juste de conception générale potentiellement à revoir. Je ne cherche pas à grapiller des ms ici ou là, j'essaie de voir si certaines approches que je n'ai jamais tenté jusqu'à présent peuvent permettre des gains significatifs (je pense que oui)

    Dans le déroulement actuel de la génération, les goulots d'étranglements sont assez clairement identifiés et sont en lien très fort avec le nombre, la complexité des règles de gestion à appliquer et l'indexation de la base de référence.
    Sur cela sans doute y aura-t-il encore un peu d'amélioration à apporter mais ce n'est pas vraiment là la question. La souplesse de l'outil me semble valoir le compromis sur la performance dans le cadre actuel. Après tout, je laisse tourner un week end et me voilà avec une base de 5 ou 6 Go.

    La question tourne plutôt autour des gains potentiels liés l'approche de la conception.
    A ce stade, j'explore le multithreading et sans doute après le multiprocessing. Ce sont des modes de fonctionnement que je ne maîtrise pas (la différence de résultat entre les deux ne me semble pas claire) mais qui s'appliquent sans doute très bien au cas de figure puisque chaque instance de l'entité racine devrait pouvoir être générée dans un processus séparé.

    La question est donc plutôt de savoir si l'expérience des uns ou des autres peut me permettre d'aller directement dans une bonne direction et/ou d'éviter des impasses évidentes pour des personnes qui connaissent ce genre de problématiques.

  4. #4
    Membre éclairé

    Inscrit en
    Novembre 2008
    Messages
    442
    Détails du profil
    Informations forums :
    Inscription : Novembre 2008
    Messages : 442
    Par défaut
    Je viens de lire le fil relatif à l'activité du forum.
    Dans le message résultant de la question que Tyrtamos a posé à ChatGPT, on trouve, comme problème sur les forums

    5. Modération parfois trop stricte ou décourageante
    Réponses du type :
    « Question déjà posée »
    « Lis la documentation »
    Cela a découragé beaucoup de débutants.

    J'ai tendance à classer "Tout ça c'est du blabla technique" dans la même catégorie

    Puis en solution

    5. Culture bienveillante et pédagogique
    Règle centrale :
    « On répond comme si la personne était en train d’apprendre. »
    pas de “RTFM”
    pas d’humiliation
    Pas de mépris des débutants
    Les experts sont valorisés pour la clarté, pas pour l’ego.

    Il y a du boulot.

    En l'occurrence si je pose cette même question à une IA, elle donne des pistes et priorise les différentes solutions sans essayer de m'expliquer que cette question est merdique.
    Bonne journée.

  5. #5
    Expert confirmé
    Avatar de fred1599
    Homme Profil pro
    Lead Dev Python
    Inscrit en
    Juillet 2006
    Messages
    4 899
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Meurthe et Moselle (Lorraine)

    Informations professionnelles :
    Activité : Lead Dev Python
    Secteur : Arts - Culture

    Informations forums :
    Inscription : Juillet 2006
    Messages : 4 899
    Par défaut
    Hello,

    L'important est de comprendre qu'il n'y a pas une bonne solution ! Exprimer un choix avec cython ou multiprocessing, serait un total manque de vision de ma part en m'appuyant sur le peu d'informations techniques que vous donnez.

    Bien sûr l'IA va vous donnez une réponse, parce-que vous en voulez une ! Moi je ne là donnerai pas, tant que vous n'avez pas mesuré concrètement l'endroit où se trouve le goulot d'étranglement... la différence, c'est que j'ai du vécu et que selon le contexte, je choisis l'un plus que l'autre, etc...

    Vous ne donnez aucun code ! Rien n'est mesuré, c'est du blabla, j'insiste, désolé, mais quels outils utilisez-vous ? Sur quels codes vous appuyez vous ? On ne sait même pas sur quelle version python vous travaillez, ni même l'OS !

    Avec python 3.13+ ça peut changer le game, par ex.

    100 Mo/s, on parle de quoi ? volume du fichier SQLite ou volume de données brutes traîtées ?
    Que faîtes vous comme process avec ces fichiers texte ?

    Avec vos informations on ne sait même pas si le problème se situe chez SQLite ou Python, et pour savoir, le fait d'indiquer l'un ou l'autre n'est pas une preuve il faut le démontrer...
    Donnez vos métriques et ensuite nous pouvons avoir une discussion sur de potentielles solutions.
    Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
    La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)

  6. #6
    Membre éclairé

    Inscrit en
    Novembre 2008
    Messages
    442
    Détails du profil
    Informations forums :
    Inscription : Novembre 2008
    Messages : 442
    Par défaut
    Merci.
    Votre position est déjà plus claire. Notez que je comprends votre point de vue mais il n'est pas nécessaire d'envoyer les gens paître de façon aussi désagréable. Un simple "je ne peux pas répondre sur la base des informations fournies car..." est moins irritante

    Vous donner le code, je veux bien, mais il commence à y en avoir pas mal (quelques milliers de lignes, rien de monstrueux non plus). Si on commence à entrer dans ce niveau de détail, ça risque d'être une peu chronophage, c'est pourquoi je restais sur des considérations assez générales. La question de l'optimisation du code lui même est bien sûr une question importante mais j'essayais d'explorer des pistes plus structurelles. Dans le cas présent, la mise en parallèle semble assez naturelle et prometteuse compte tenu du fonctionnement général (mais spoil : ça n'est pas aussi simple qu'espéré)
    Pour tout bien présenter, il faudrait bien plus qu'un post sur un forum

    Que l'IA me donne une réponse parce que je lui en demande une, c'est parfaitement évident. Mais l'intérêt, de ces réponses, c'est de me donner des pistes de réflexion qui me permettent d'éliminer des pans entiers de prospection (du moins en première intention)
    Je ne cherche pas une correction complète clé en main, je cherche des angles d'attaque pour améliorer la situation sans avoir à y passer des semaines.
    Il s'agit d'un développement façon side project. J'ai compilé et réarrangé des moulinettes que j'avais développées dans des situations spécifiques mais je n'ai pas de temps alloué là dessus. C'est compliqué de me dire qu'il faut me lancer sur des semaines de boulot pour gagner quelques pourcents de volume. J'ai beaucoup d'autres sujets à traiter.

    Dans le cas présent, les échanges avec l'IA ont rapidement mis en évidence que dans tous les cas, cela nécessiterait un refactoring important.
    - Le multiprocessing parce qu'il faut que tout soit sérialisable et que dans l'état actuel des choses, ce n'est clairement pas le cas, loin s'en faut (du moins dans l'idée que j'avais en tête)
    - Le multithreading, c'est moins violent mais le résultat est le même en raison des partages de mémoire
    - Cython parce que vu l'aspect hautement dynamique du programme, il est probable que les gains soient marginaux en regard de l'investissement.

    Donc au final, pas de solution simple en vue.

    Bref... A ce stade, je crois que j'ai ma réponse, en fait. je ne vais pas vous en demander plus, ça reviendrait à faire de la revue de code.

    La suite est là pour information. Je vais finir de tester une piste ou deux mais ensuite, tant pis, ça attendra encore que j'ai un peu de temps pour avancer..

    100Mo, c'est par heure, pas par seconde, si c'était par seconde, ça m'irait très bien :-), Et il s'agit bien de la taille de la base sqlite produite.
    Mon problème, c'est que les gens avec qui je travaille aimeraient des bases de 10-15Go (actuellement, je n'ai jamais généré aussi gros. Le plus que j'ai fait, c'est 8,5 Go en 3 jours avec quelques tables assez touffues dont une avec 20 millions de lignes). Donc on est sur des temps de génération qui commencent à devenir assez importants. Mais au fond, si ils en veulent plus, il faudrait aussi qu'ils me libèrent du temps sur le sujet.

    Effectivement, je suis en python 3.13 (et à vrai dire, rien ne m'empêche de passer à 3.14 (j'espère qu'il y aura une mineure 15...))
    Les données sont générées à partir de rien, il n'y a pas d'autre volume de données traitées. (ou alors, si on parle des données de référence, c'est variable en fonction des situations mais c'est très peu)

    Responsable : SQLite ou python : les deux, très probablement (par contre, que voulez vous que je mette comme métriques ? j'ai peu de moyens pour faire des comparaisons avec d'autres structures de programmes).

    SQLite parce que j'insère les enregistrements un par un. C'est une des pistes qui tiennent la corde. L'inconvénient, c'est que du coup, il devient beaucoup plus compliqué de travailler sur les données déjà générées (par exemple pour générer des relations (n,n)). De même, je ne travaille que très peu en mémoire pour ne pas risquer de la saturer. Avec la quasi élimination des méthodes initialement envisagée, l'optimisation au niveau de sqlite revient en force. Je vais voir si je peux intercaler efficacement un buffer en mémoire pour stocker les données avec des insertions plus massives, mais ça risque de signifier de sérieuses complications algorithmiques.
    python parce que j'ai pas mal de travail de parsing de strings pour identifier les paramètres, les requêtes à passer sur mes bases de référence, les valeurs des entités déjà générées...

    Pour préciser le fonctionnement (si ça vous intéresse)
    Voici un bout de fichier yaml décrivant l'arborescence d'alimentation (la structure de la base de sortie à quelques nuances près) ainsi qu'un fichier décrivant l'alimentation d'une table.
    Ici, il s'agit d'une base exemple décrivant un stock de bibliothèque

    Tout cela est chargé par le moteur qui résoud les appels de fonctions et les associe aux champs dans les objets python puis les boucles commencent et génèrent des données
    (Ici aussi, il y a peut-être un axe. Je pourrais sans doute m'épargner pas mal de peine si mon paramétrage était fait dans des fichiers python directement. D'un autre côté, comme ça, ça fonctionne pas mal aussi)

    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
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
     
    model:
      name: 'global'
      table_parameters:
        delimiter: ":"
      entities:
        categorie:
          bufferize: "refdb.get_filtered_rows(categorie)"
          card: buffer.length
          fname: '{rootdir}/addons/biblio/tables/categorie.txt' # Fichier décrivant les données attendues
        editeur:
          bufferize: "refdb.get_filtered_rows(editeur)"
          card: buffer.length
          fname: '{rootdir}/addons/biblio/tables/editeur.txt' # Fichier décrivant les données attendues
          sub:
            collection:
              bufferize: "refdb.get_filtered_rows(collection, editeur='[editeur.code]')"
              card: buffer.length
              fname: '{rootdir}/addons/biblio/tables/collection.txt' # Fichier décrivant les données attendues
              sub:
                cardaut: # Calcule du nombre d'auteurs à générer en fonction de la collection et de l'éditeur
                  db: 'work'
                  card: [1, 1]
                  fname: '{rootdir}/addons/biblio/tables/w_cardaut.txt' # Fichier décrivant les données attendues
                auteur:
                  card: 'model.calculate("round([cardaut.card_ed] * [cardaut.card_col] / 5)")'
                  fname: '{rootdir}/addons/biblio/tables/auteur.txt' # Fichier décrivant les données attendues
                  sub:
                    ouvrage:
                      bufferize: "serie.random_dates_between([auteur.date_deb_carriere_hid], [auteur.date_fin_carriere_hid], 280, 1300)"
                      card: buffer.length
                      fname: '{rootdir}/addons/biblio/tables/ouvrage.txt' # Fichier décrivant les données attendues
        usager:
          card: [50,100]  # Nombre d'enregistrements
          fname: '{rootdir}/addons/biblio/tables/usager.txt' # Fichier décrivant les données attendues
          tablename: usager
          supplier:                   # pavé (facultatif) définissant une source de données annexe pour cette table
            name: famille           # Nom utilisé dans les paramétrages pour identifier le data_provider
            module: '{rootdir}/addons/biblio/suppliers/Famille.py'    # Module (dans le répertoire /suppliers)
            parameters: '{rootdir}/addons/biblio/parameters/famille.yaml' # Fichier de paramétrage du supplier
          sub:
            abonnement:
              bufferize: "serie.intervals_between([usager.date_first_abo_hid], [usager.date_last_abo_hid], {{'years':1}})"
              tablename: abonnement
              card: buffer.length
              fname: '{rootdir}/addons/biblio/tables/abonnement.txt' # Fichier décrivant les données attendues
              sub:
                groupe_emprunt:
                  db: work
                  bufferize: "serie.random_dates_between([abonnement.date_deb], [abonnement.date_fin], 21, 70)"
                  card: buffer.length
                  fname: '{rootdir}/addons/biblio/tables/groupe_emprunt.txt' # Fichier décrivant les données attendues
                  sub:
                    emprunt:
                      tablename: emprunt
                      card: [1,5]
                      fname: '{rootdir}/addons/biblio/tables/emprunt.txt' # Fichier décrivant les données attendues
    Et un fichier texte (pointé par la clé fname dans les différentes "entités")

    nom du champ : type : fonction appelée
    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
     
    id:INTEGER:model.get_id(auteur, 20000)
    nationalite:VARCHAR(5):faker.choice_string(('FR', 50), ('US', 25), ('GB', 15), ('IT', 10))
    genre:VARCHAR(1):faker.choice_string(('M', 60), ('F', 40))
    # Par convention personnelle, je suffixe mes champs cachés par _hid. Ce n'est pas obligatoire
    locale_hid:HIDDEN:refdb.get_filtered_row_value(locale.locale, "nationalite='[auteur.nationalite]'")
    setloc_hid:HIDDEN:model.set_faker_locale(auteur.locale_hid)
    nom:VARCHAR(50):model.exec_provider("faker.last_name()")
    prenom:VARCHAR(50):model.if_else("'[auteur.genre]'=='M'",model.exec_provider("faker.first_name_male()"),model.exec_provider("faker.first_name_female()"))
    rstloc_hid:HIDDEN:model.reset_faker_locale()
    # Je stocke l'intervalle de choix des dates en fonction des probabilités de la table de référence
    # date_naissance
    dat_deb_hid:HIDDEN:refdb.get_weighted_random_row_value(date_naissance.DT_NAI_DEB)
    dat_fin_hid:HIDDEN:refdb.get_current_row_value(date_naissance.DAT_NAI_FIN)
    date_nai:VARCHAR(10):model.random_date_between(auteur.dat_deb_hid, auteur.dat_fin_hid)
    age_hid:HIDDEN:refdb.get_weighted_filtered_row_value(age_deces.AGE,NORM_SEX_CNT,age_deces.SEX='[auteur.genre]')
    date_deces_hid:HIDDEN:model.random_date_after(auteur.date_nai, {'years': [auteur.age_hid]}, 250)
    today_hid:HIDDEN:globals.get(today)
    date_deces:varchar(10):model.if_else("'[auteur.date_deces_hid]' < '[auteur.today_hid]'", [auteur.date_deces_hid], '')
    # Date de la période pendant laquelle l'auteur écrit
    date_deb_carriere_min_hid:HIDDEN:model.random_date_before([auteur.today_hid], 600, 100)
    date_deb_carriere_max_hid:HIDDEN:model.random_date_after(auteur.date_nai, {'years': 15}, 1200)
    date_deb_carriere_hid:HIDDEN:model.evaluate("min('[auteur.date_deb_carriere_min_hid]', '[auteur.date_deb_carriere_max_hid]')")
    date_fin_carriere_max_hid:HIDDEN:model.if_else("'[auteur.date_deces_hid]' < '[auteur.today_hid]'", [auteur.date_deces_hid], [auteur.today_hid])
    date_fin_carriere_hid:HIDDEN:model.random_date_after([auteur.date_deb_carriere_hid], 1000, 20000, [auteur.date_fin_carriere_max_hid])

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. optimiser le code d'une fonction
    Par yanis97 dans le forum MS SQL Server
    Réponses: 1
    Dernier message: 15/07/2005, 08h41
  2. Optimiser mon code ASP/HTML
    Par ahage4x4 dans le forum ASP
    Réponses: 7
    Dernier message: 30/05/2005, 10h29
  3. optimiser le code
    Par bibi2607 dans le forum ASP
    Réponses: 3
    Dernier message: 03/02/2005, 14h30
  4. syntaxe et optimisation de codes
    Par elitol dans le forum Langage SQL
    Réponses: 18
    Dernier message: 12/08/2004, 11h54
  5. optimisation du code et var globales
    Par tigrou2405 dans le forum ASP
    Réponses: 2
    Dernier message: 23/01/2004, 10h59

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