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

  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])

  7. #7
    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
    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.
    Faîtes une recherche du côté du terme "Bulk", l'intérêt est d'accumuler quelques milliers de lignes en mémoire et de faire un executemany puis un commit.

    Et un fichier texte (pointé par la clé fname dans les différentes "entités")
    Ça semble compliqué à parser, et aussi très consommateur (conséquence).

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    get_weighted_random_row_value
    C'est une requête je suppose ? Si oui, si vous faîtes cette requête (qui à l'air de faire pas mal de choses) sur chaque ligne, la discussion entre SQLite et Python va pas être simple... et dans ce cas pas étonnant les 100 Mo/h

    Et d'autres choses que j'ai pas eu le temps d'analyser encore ...
    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)

  8. #8
    Membre éclairé

    Inscrit en
    Novembre 2008
    Messages
    442
    Détails du profil
    Informations forums :
    Inscription : Novembre 2008
    Messages : 442
    Par défaut
    Bonjour,

    Merci pour ces compléments.
    Effectivement, get_weighted_random_row_value va chercher des données dans une base de données en respectant des probabilités mais il n'y a pas tant de traitement que ça : j'ai des tables de référence avec une colonne de fréquence normalisée cumulée (entre 0 et 1) puis cette fonction prend une valeur pseudo aléatoire (p = random.uniform(0, 1.0))

    et la requête de tirage est f"SELECT * FROM {tname} WHERE {tname}.{weightcol} > {p} ORDER BY {tname}.{weightcol} LIMIT 1;")

    La ligne renvoyée dépend des probabilités définies pour calculer la fréquence normalisée cumulée "weightcol".

    Bien sûr, j'ai un index sur weightcol et j'évite d'avoir de grosses tables de référence. Mais bon, il y a un forcément un coût. D'ailleurs, j'ai été surpris mais le fait de mettre ma base de référence en mémoire ne change pas très significativement les temps de réponse.

    Pour le parser, vu que c'est un outil perso fait en mode assez rapide, il reste assez basique (et assez chatouilleux avec une syntaxe pas toujours bien cohérente en fonction des situations).

    En gros, le principe, c'est qu'au chargement de l'appli, je crée des entités avec les champs paramétrés dans les fichiers textes et je les "branche" sur les fonctions appelées Par exemple, dans model.random_date_before([auteur.today_hid], 600, 100), model est une clé correspondant à un objet et random_date_before, une de ses méthodes que j'appelle avec un attr(...).

    Donc cette partie là n'est parsée qu'une fois et c'est vraiment complètement négligeable dans le processus global.

    Par contre, les paramètres sont effectivement parsés lors de l'exécution de la fonction. Donc oui, le parsing a un coût mais à moins d'écrire mes entités directement en python, avec tout le code et les éléments de syntaxe supplémentaires que cela implique (imports, définition de classe...), je ne vois pas comment l'éviter.

    De même, je sais que l'insertion ligne à ligne est un problème c'est dans la liste des points qu'il faut améliorer. Mais pour le moment, je ne vois pas trop comment intégrer ça dans le processus pour que ça reste simple sans pénaliser la souplesse et les possibilités de l'outil... Quoique depuis qu'on en discute, j'ai peut-être une piste que je vais bientôt explorer puisque mes tentatives de parallélisation du processus ne fonctionneront pas sans revoir l'appli en profondeur et je n'ai vraiment pas le temps en ce moment (je vais peut-être tenter de jouer au bourrin en lançant l'appli plusieurs fois en même temps... On verra ce que ça donne).

    Ce qu'il faut bien avoir en tête, c'est que l'axe principal de réflexion qui a amené à cet outil, c'est la possibilité de générer des jeux de données les plus réalistes possible en écrivant le moins possible. Pour l'avoir fait plus d'une fois à partir de rien, je sais que la création de tels jeux peut vite devenir un projet à part entière dès qu'on veut un peu de volumétrie qui respecte des règles complexes avec des relations entre entités, des rollups, des chronologies de dates...
    Les techniques utilisées dans les framework comme symfony pour les tests unitaires (les fixtures) sont juste des blagues très largement insuffisantes dans beaucoup de cas d'utilisations (dès qu'on veut regarder comment une chaîne réagit avec de la volumétrie par exemple tout en implémentant des règles de gestions contrôlées dans cette chaîne mais sans utiliser de données de prod).
    Ainsi, dans cette appli, je me suis vraiment concentré sur les mécanismes mis à dispo pour implémenter des règles de gestion éventuellement relativement complexes.
    A ce niveau, je pense avoir atteint mon objectif. Je n'ai pas encore rencontré de cas insoluble dans ce cadre. Mais effectivement, cela a un coût assez important en terme de performance mais je préfère ça que perdre en possibilités.
    Un deuxième élément, c'est que je ne veux pas tout générer en mémoire pour éviter de risquer de l'exploser lors de la création de jeux volumineux (en même temps, vu les perfs actuelles, cette situation n'est pas souvent rencontrée).

    En tout cas merci pour le temps consacré. D'en discuter m'a permis d'identifier quelques pistes prometteuses. Je n'ai plus qu'à m'y mettre.

  9. #9
    Rédacteur/Modérateur

    Homme Profil pro
    Ingénieur qualité méthodes
    Inscrit en
    Décembre 2013
    Messages
    4 261
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Ingénieur qualité méthodes
    Secteur : Conseil

    Informations forums :
    Inscription : Décembre 2013
    Messages : 4 261
    Par défaut
    Je pratique très peu Python. Pourquoi ? parce que j'ai un cursus qui m'a amené à concevoir des programmes comme des boucles (des boucles imbriquées assez souvent), et Python déteste les boucles.

    Avec d'autres outils, on va faire :
    Pour i = 1 a 1000000
    insere_ligne()
    fin
    En Python, ceci est atrocement long.
    Python sait traiter des tableaux. Il est très performant quand il traite des tableaux, il est atrocement lent quand il traite des enregistrements 1 par 1.
    En caricaturant un peu beaucoup, chaque instruction Python prend un temps fixe. Si cette instruction traite un tableau de 1 Million de lignes, ou si elle traite une seule ligne, peu importe, la durée est (quasiment) la même.

    Tu parles aussi de Sqllite.
    Pareil, même problème. Avec les outils de type SQL en général (et je ne vois pas pourquoi SQLLite ferait exception), une boucle dans laquelle tu lances 1 million d'instructions 'INSERT', c'est catastrophique.

    En gros, voici ce qui se passe : tu lances 1 Million d'appels téléphoniques à une personne. A chaque appel, tu commences par allo, tu m'entends, la base de données répond oui, je t'entend .. puis tu lui donne une 'micro'-instruction (insère une ligne), et tu lui demandes ensuite : 'C'est ok , ça s'est bien passé ?'

    La solution pour éviter ce problème SQL, c'est de traiter par paquets. Par exemple, au lieu de faire 1 Million d'appels, tu fais 1000 fois le traitement suivant :
    - Générer un fichier TXT avec 1000 enregistrements.
    - Loader ce fichier dans ta bae SQLLite (par un SQLLoad ou un outil du genre)
    Ca peut te générer d'autres difficultés, mais là , on n'en sait pas assez pour t'aider.
    Toute la partie 'protocole' (établissement de la communication, accusé de réception....) est faite une seule fois au lieu de 1000.

    Selon la configuration (réseau ....) les gains sont variables, mais je ne serais pas surpris si tu reviens en disant : c'est devenu 50 fois plus rapide en passant par un LOADER au lieu d'une succession d'insert individuels.
    N'oubliez pas le bouton Résolu si vous avez obtenu une réponse à votre question.

  10. #10
    Membre éclairé

    Inscrit en
    Novembre 2008
    Messages
    442
    Détails du profil
    Informations forums :
    Inscription : Novembre 2008
    Messages : 442
    Par défaut
    Oui, je sais que vous avez raison. Mon traitement enregistrement par enregistrement n'est pas bon.

    Mais le remplacement par une construction en mémoire (ou dans un fichier texte) n'est pas évident car cela répartit les données en deux endroits entre lesquels il faudra que je jongle pour récupérer mes petits ou alors il faudra gérer finement les insertions en base pour que les données dont j'ai besoin y soient. J'avais commencé dans cette idée et, de renoncement en renoncement, j'en était arrivé à écrire au fur et à mesure pour m'autoriser davantage de cas d'usages.

    D'un autre côté, j'ai amélioré d'autres aspects si bien que je ne suis plus complètement sûr que ces renoncements soient toujours nécessaires.

    Du coup, je vais essayer de travailler ce point directement dans l'objet qui s'occupe des écritures. Un facteur 50, ce serait vraiment top. 10, déjà, ça m'irait bien. Je tâcherai de me souvenir de venir poster ici les résultats s'ils sont à ce niveau :-)

    Sans doute pas de fichiers / loader (du moins pas en première intention : ça générerait pas mal de complications) mais plutôt une bufferisation dans de simples listes et un insertmany toutes les x000 lignes. Ca donnera déjà une idée.

    Il restera toujours le fait que j'interroge les bases de référence très fréquemment mais là aussi, une amélioration imposerait beaucoup de code pour gérer ce qu'un simple select peut faire (par exemple le cas précédent)

    Quant aux perfs de python... C'est toujours un sujet. Mais en ce qui me concerne, la plupart du temps, ce qui compte le plus, c'est la capacité de sortir des trucs efficaces rapidement. Je n'écris que rarement des applis embarquant plus d'une ou deux fonctionnalités en batch (par contre, j'en écris un peu tout le temps). Beaucoup de parsing et de traitement de fichiers, de génération de requêtes et autres mais pas d'application réellement touffues, ce qui fait que ce langage me semble bien adapté. Très peu de lignes de code pour un résultat suffisant.

    En fait, cette moulinette, partie comme les autres de besoins ponctuels a fini par grossir et maintenant, les perfs deviennent une limitation.

    Idem pour sqlite... C'est très pratique pour gérer la volumétrie (sans compter que sql est ma première langue en informatique).
    J'avais pensé à des trucs genre pandas et autre polars mais tout ça travaille en mémoire et ça, il est certain que ça bloquera à un moment donné.

    Enfin, je travaille en local. Pas de réseau. Pas de complications. Ce qu'on me demande, ce sont des données, pas un programme. Mon outil me sert en local sur une machine relativement standard et je souhaite que ça reste comme ça, ne serait-ce que pour des raisons de sécurités. Entre mon parsing à l'arrache, les execs et autres, les failles sont partout. Hors de question de rendre cet outil accessible de l'extérieur en l'état. Sécuriser serait peut-être une bonne idée mais je n'ai pas de temps pour ça.

    Merci en tout cas. Je vais étudier la question de sqlite et des insertions. Ca me semble effectivement un bon axe pour de gros gains à moindre frais.

  11. #11
    Membre éclairé

    Inscrit en
    Novembre 2008
    Messages
    442
    Détails du profil
    Informations forums :
    Inscription : Novembre 2008
    Messages : 442
    Par défaut
    Déjà, juste pour info, si je lance 4 fois le programme simultanément (avec, évidemment, une écriture dans des bases différentes pour ne pas être gêné), j'ai 4 x plus de lignes générées dans le même temps (à peu près).
    Bon, j'imagine que si je le lance 150 fois, je vais avoir des problèmes mais bon. Si avec l'optimisation des insertions j'ai un facteur 20 ou 30 en plus, ça va commencer à devenir intéressant :-)

  12. #12
    Rédacteur/Modérateur

    Homme Profil pro
    Ingénieur qualité méthodes
    Inscrit en
    Décembre 2013
    Messages
    4 261
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Ingénieur qualité méthodes
    Secteur : Conseil

    Informations forums :
    Inscription : Décembre 2013
    Messages : 4 261
    Par défaut
    Je retire ma casquette d'informaticien pour mettre ma vraie casquette, celle de statisticien.

    On ne connaît pas précisément ton besoin, mais a priori, tu veux générer un fichier qui ressemble à un fichier aléatoire, mais avec des contraintes de type quotas.

    Si tu veux par exemple 1 Million d'enregistrements, tu dois pouvoir commencer par insérer 500 000 enregistrements, totalement aléatoires. A cette étape, tu regardes quelles catégories sont sur-représentées ou sous-représentées, et tu insères 150 000 lignes 'totalement aléatoires', mais avec des quotas un peu différents, pour rattraper les retards.
    Si tu as besoin de lire la base pour chaque nouvel insert, il y a quelque chose de pas très sain dans ton process
    N'oubliez pas le bouton Résolu si vous avez obtenu une réponse à votre question.

  13. #13
    Membre éclairé

    Inscrit en
    Novembre 2008
    Messages
    442
    Détails du profil
    Informations forums :
    Inscription : Novembre 2008
    Messages : 442
    Par défaut
    Effectivement, je n'ai pas décrit mon besoin de façon claire et nette. Difficile d'identifier tout (mais uniquement) ce qui pourrait être utile.
    J'ai bien conscience que pour une aide précise et détaillée, c'est insuffisant mais ce n'est pas ce que je demande. Déjà avec les pistes évoquées, j'ai déjà bien avancé.

    Pour répondre à ta remarque : les données créées ne sont pas totalement aléatoires... Elles utilisent de l'aléatoire pour varier mais doivent respecter des règles de gestion (éventuellement nombreuses et complexes) correspondant à l'application testée.

    Typiquement, chez nous, il n'est pas possible d'utiliser les données de production pour les tests. Cette situation pose beaucoup de difficultés pour les applications qui peuvent se retrouver en prod sans avoir été suffisamment testées.

    Donc il faut parfois créer des jeux de données qui ressemblent à ce qu'on peut trouver en production (ou qu'on trouvera si l'application n'existe pas encore) en terme de lien entre les données, de probabilités d'apparitions des valeurs, de chronologies... Et de volumétrie).

    L'idée de données totalement aléatoires... Je ne vois pas comment mettre en oeuvre ce genre d'approche. Sur une même ligne, je peux avoir des données dépendantes, de même entre différentes tables voire entre différentes lignes de tables...

    Par exemple, si on considère le monde de l'édition, un éditeur propose des collections... Les éditeurs se partagent le marché selon certaines probabilités, les collections se répartissent les volumes d'éditions de chaque éditeur selon certaines probabilités. Ces éditeurs et collections peuvent être choisies dans des listes de valeurs existantes pour ajouter un peu de réalisme ou créées de toutes pièces si les données sont confidentielles, le nombre d'auteurs par collection dépend également de différentes probabilités (un romancier écrit souvent plus fréquemment des livres qu'un chef cuisinier...) et ainsi de suite. Les dates d'écriture des romans ne peuvent se situer avant la date de naissance (+ quelques années) ou après la date de décès, on n'écrit pas deux romans la même semaine... Certaines cardinalités doivent respecter des répartitions gaussiennes, croissantes ou décroissantes selon des fonctions de répartition

    De même, générer des familles de personnes avec des règles de respects de dates entre parents et enfants, nombre d'enfants, probabilités d'avoir des jumeaux...

    Mon outil est là pour permettre de déclarer ces situations sans avoir à écrire des masses de code. Ça rentre dans le paramétrage standard.

    J'entends bien que certains choix ne sont pas au top mais certaines règles (pas toutes, bien heureusement) me semblent tout de même beaucoup plus simples à gérer en sql (simplicité vs perf...)

    Le prix que je paie pour le moment, c'est que je ne génère que 100 Mo/heure.

    Mais en intégrant vos remarques, mes premières estimations me permettent d'espérer un gain d'un facteur 8-10 (à la grosse louche. Ce ne sont que des estimations initiales). J'ai aussi ajouté une technique de bourrin pour faire de parallélisme en lançant plusieurs instances de l'appli en même temps (qui écrivent dans des bases différentes) et ici aussi, je multiplie les gains par le nombre d'instances lancées (avec certainement des limites, j'ai testé jusqu'à 4)
    Du coup, je pense pouvoir monter aux alentours de 4 ou 5 Go / heures et là, ça me convient. Si en une nuit, je génère 40Go, ça donne de la marge.

  14. #14
    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,

    Je suis pas sur mon PC, je vais être bref, Désolé !

    Parsing -> catastrophe pour l'efficacité de l'application, surtout si c'est fait n fois
    Bulk -> on peut pas faire mieux, on utilise la mémoire vive et on insère une fois au lieu de n fois

    Je sais ce que c'est péter du système, pour le rendre plus efficace, et si vous savez ce que vous faites, l'IA peut être utile pour la partie gain de temps.

    À mon sens plutôt que de faire du parsing pour rendre dynamique vos requêtes, créer des fonctions lambda prêtes à être appelées en mémoire, vous exploserez les temps...

    Bref, revoyez la conception, ça se fait rapidement de nos jours.
    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)

  15. #15
    Membre éclairé

    Inscrit en
    Novembre 2008
    Messages
    442
    Détails du profil
    Informations forums :
    Inscription : Novembre 2008
    Messages : 442
    Par défaut
    Merci.

    Bulk : c'est en cours. J'ai trouvé le moyen d'intégrer ça à peu près proprement. Ça introduit quelques difficultés mais ça devrait passer. On est sur des facteurs de gains de temps de l'ordre de 6-8. C'est déjà pas mal.

    Le parsing n'est fait qu'une fois en début de procédure. Totalement pour les appels de fonctions et partiellement pour les paramètres (ils sont bien séparés mais il reste parfois de petits éléments à traiter. Par exemple remplacer [table.champ] par la valeur prise par ce champ dans l'environnement courant). Ainsi, pendant le déroulement du programme, il n'y a plus vraiment de parsing autre que des split('.') et ce genre de choses basiques.
    Cependant, il est probable que ça pourrait être encore amélioré un peu.

    Mais à part en écrivant mes fichiers de paramétrage directement en python, je ne vois pas comment éliminer complètement ce problème (je ne comprends pas du tout comment je pourrais remplacer le parsing par des lambdas).
    Ça aurait l'inconvénient de compliquer le paramétrage qui devrait inclure tout un tas de code sous forme d'imports, de définitions de classes... et nécessiterait de vraiment bien connaître la structure interne du programme pour le paramétrer (or, je voudrais plutôt simplifier cette étape).

    Peut-être que je vais finir par mettre une étape intermédiaire où le parser génèrerait du code python qui serait ensuite importé dans le corps principal pour être exécuté. De cette façon, tout serait sous forme de python à l'exécution (et accessoirement, ça renforcerait très probablement la cohérence de la syntaxe de mon paramétrage).

    Dans les temps d'exécutions, il ne faut pas non plus oublier les calculs eux mêmes. Certaines règles de gestion peuvent s'avérer relativement complexes. Si je reprends l'exemple de la bibliothèque, on peut imaginer que la date d'édition du premier bouquin d'un auteur quelconque se situe au moins 17 ans après la naissance dans un intervalle de 30 ans sous réserve que cela reste inférieur à la date de décès (si l'auteur est décédé) le tout respectant éventuellement (pour l'âge des auteurs à la parution de leur premier livre) une courbe de distribution façon gauss tronquée ou poisson. Cela peut également être compliqué si la borne inférieure est conditionnée par la catégorie du livre (il est difficile d'écrire un livre scientifique de niveau thèse avant 25 ou 26 ans...)

    Ces règles de gestion dépendent du cas d'utilisation et l'outil doit permettre d'exprimer cela de la façon la plus synthétique possible. Il ne s'agit pas de développer une application entière pour chaque base à générer.
    En l'occurrence, si je passe 2 jours à paramétrer et une nuit à générer, c'est plus intéressant que 3 ou 4 jours (ou plus) à préparer la génération et 15 s pour la génération elle même.

    En tout état de cause, je vous remercie pour tous vos conseils. Cela ouvre un certain nombre d'axes de réflexion pour la suite.
    Les deux points qui ressortent franchement suspects dans ma première version semblent clairement être les interactions avec la base et le parsing. Ce dernier probablement dans une moindre mesure puisqu'il est surtout fait au chargement, mais il faudra que je regarde cela de plus près pour en être sûr.

    L'interaction avec la base est en cours de réécriture et promet déjà des gains significatifs.
    Le parsing, il faut que je réfléchisse un peu. Ça me semble plus difficile sans compromis avec la simplicité de l'expression des règles de gestion.

+ 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