Les membres ayant 30 points peuvent parler sur les canaux annonces, projets et hs du chat.
La shoutbox n'est pas chargée par défaut pour des raisons de performances. Cliquez pour charger.

Forum Casio - Autres questions


Index du Forum » Autres questions » La quête de l'optimisation (non fiscale)
Tituya Hors ligne Administrateur Points: 2163 Défis: 26 Message

La quête de l'optimisation (non fiscale)

Posté le 08/01/2025 20:17

Hey hey hey !

En Erasmus j'ai un cours d'optimisation où l'objectif est... d'optimiser un programme. Le sujet de cette année repose sur une jointure entre 4 fichiers différents, je suis donc à la recherche de la moindre petite optimisation pour pouvoir économiser des cycles. Et à la grande surprise de tous : Je ne suis absolument pas bon dedans

Avant de pouvoir faire la jointure il faut d'abord lire et stocker les fichiers en mémoire, et c'est là l'intérêt de ce topic. Voici le code C++ permettant de réaliser la lecture, le parsage de la ligne et finalement le stockage dans la structure de donnée :

#include <unordered_map>
#include <iostream>
#include <ostream>
#include <string>
#include <cstddef>
#include <vector>
#include <x86intrin.h>
#include <fstream>

using XxHashMap = std::unordered_map<std::string, std::vector<std::string>>;

XxHashMap read_file_to_map(const std::string& file) {
    XxHashMap map;
    map.reserve(5000000);
    std::ifstream infile(file);
    std::string line;
    while (std::getline(infile, line)) {
        size_t pos = line.find(',');
        map[line.substr(0, pos)].emplace_back(line.substr(pos + 1));
    }
    return map;
}

int main(int argc, char* argv[]) {
    const XxHashMap f1 = read_file_to_map(argv[1]);
    return 0;
}


Vous l'aurez compris, sur un fichier de ce style :
a,b
a,c
d,e
d,f

La structure de donnée est similaire à cela : {a:[b,c], d:[e,f]}

L'intérêt est donc d'optimiser au maximum cette fonction, quitte à modifier la structure de donnée ce n'est pas important. L'ordre du stockage n'est pas important non plus, il me faut juste la possibilité d'obtenir les éléments associés à une clé.

Avec ce code j'obtiens entre 8.8 et 9.1B de cycles. Un nombre ne signifiant pas grand chose mais multiplié 4 fois on arrive à plus de 50% de l'exécution totale de la jointure. Ce qui n'est pas correct quand même.

Donc si vous avez une quelconque idée, n'hésitez pas à m'aider sur ce coup là

Le programme complet de la jointure se trouve ci dessous et un fichier d'exemple se trouve pour une durée limitée à cet emplacement : https://t.breizh.pm/nP7JItk4XQ/f2.csv

Cliquez pour découvrir
Cliquez pour recouvrir

using XxHashMap = std::unordered_map<std::string, std::vector<std::string>>;

XxHashMap read_file_to_map(const std::string& file) {
    XxHashMap map;
    map.reserve(5000000);
    std::ifstream infile(file);
    std::string line;
    size_t start = __rdtsc();
    while (std::getline(infile, line)) {
        size_t pos = line.find(',');
        map[line.substr(0, pos)].emplace_back(line.substr(pos + 1));
    }
    size_t end = __rdtsc();
    std::cout << "Read cycles: " << end - start << '\n';
    return map;
}

void print_map(const XxHashMap& map) {
    for (const auto& [key, vec] : map) {
        std::cout << key << ':';
        for (const auto& val : vec) {
            std::cout << val << ',';
        }
        std::cout << '\n';
    }
}

void join(const XxHashMap& f1, const XxHashMap& f2, const XxHashMap& f3, const XxHashMap& f4) {
    std::ostream& buffer = std::cout;
    std::string line;
    for (const auto& [key, vec1] : f1) {
        auto it2 = f2.find(key);
        auto it3 = f3.find(key);
        if (it2 != f2.end() && it3 != f3.end()) {
            for (const auto& x1 : vec1) {
                for (const auto& x2 : it2->second) {
                    for (const auto& x3 : it3->second) {
                        auto it4 = f4.find(x3);
                        if (it4 != f4.end()) {
                            for (const auto& x4 : it4->second) {
                                line.clear();
                                line.append(x3).append(",").append(key).append(",").append(x1).append(",").append(x2).append(",").append(x4).append("\n");
                                buffer.write(line.c_str(), line.size());
                            }
                        }
                    }
                }
            }
        }
    }
}

int main(int argc, char* argv[]) {
    size_t startAll = __rdtsc();

    if (argc < 5) {
        std::cerr << "Usage: " << argv[0] << " <file1> <file2> <file3> <file4>\n";
        return 1;
    }

    // measure the time
    size_t startRead = __rdtsc();
    const XxHashMap f1 = read_file_to_map(argv[1]);
    const XxHashMap f2 = read_file_to_map(argv[2]);
    const XxHashMap f3 = read_file_to_map(argv[3]);
    const XxHashMap f4 = read_file_to_map(argv[4]);
    size_t endRead = __rdtsc();
    std::cout << "Read cycles : " << endRead - startRead << '\n';

    // print_map(f1);
    size_t startJoin = __rdtsc();
    // join(f1, f2, f3, f4);
    size_t endJoin = __rdtsc();
    std::cout << "Join cycles: " << endJoin - startJoin << '\n';
    size_t endAll = __rdtsc();
    std::cout << "All cycles: " << endAll - startAll << '\n';
    std::cout << "Join time: " << (endJoin - startJoin) * 100.0 / (endAll - startAll) << "%\n";
    std::cout << "Read time: " << (endRead - startRead) * 100.0 / (endAll - startAll) << "%\n";
    return 0;
}



Lephenixnoir En ligne Administrateur Points: 24769 Défis: 170 Message

Citer : Posté le 08/01/2025 20:35 | #


Je fais des essais mais globalement sur ma machine ça fait 17-18 milliards de cycles. Une partie est sur la boucle, le reste est sur la table de hachage, peu sur les vecteurs apparemment.

Ma première réaction est de charger le fichier en un seul bloc. Un malloc() si gros va mmap(), ce qui est parfait. Ensuite pas de lecture de ligne et split(), il suffit littéralement de chercher la prochaine virgule, puis le prochain \n, etc. Comme le fichier est chargé, string_view au lieu de string.

Je reviens plus tard...
Mon graphe (11 Avril): ((Rogue Life || HH2) ; PythonExtra ; serial gint ; Boson X ; passe gint 3 ; ...) || (shoutbox v5 ; v5)
Tituya Hors ligne Administrateur Points: 2163 Défis: 26 Message

Citer : Posté le 08/01/2025 20:56 | #


J'ai essayé les mmap, sans amélioration particulière. Dans la plupart des cas c'était même pire, peut être parce que le plus gros fichier ne pèse "que" 350Mo ?
Je n'aime pas le principe de lire ligne par ligne sur ces fichiers, mais la lecture par bloc ne semblait pas améliorer les performances non plus.

J'ai aussi essayé d'utiliser xxHash comme fonction de hash, pas d'amélioration particulière non plus

Dans tous les cas il va falloir que je pense à une implémentation en C, à titre informatif le programme de référence tourne en 155B de cycles, celui ci dans les alentours de 116 dont 44-46B sur la fonction en question.
Bretagne > Reste du globe
(Et de toute façon, vous pouvez pas dire le contraire)
Projet en cours : Adoranda

Mes programmes
Hésite pas à faire un test !


Lephenixnoir En ligne Administrateur Points: 24769 Défis: 170 Message

Citer : Posté le 08/01/2025 20:58 | #


J'ai oublié de préciser qu'avec les changements ci-dessus je tourne à 13 milliards environ donc les suggestions n'étaient pas dans le vide.
Mon graphe (11 Avril): ((Rogue Life || HH2) ; PythonExtra ; serial gint ; Boson X ; passe gint 3 ; ...) || (shoutbox v5 ; v5)
Lephenixnoir En ligne Administrateur Points: 24769 Défis: 170 Message

Citer : Posté le 08/01/2025 21:08 | #


J'allais oublier mais -m32 gagne un peu.

Accessoirement, tu peux remplacer la fonction de hash par un truc plus simple. T'as 350 Mo de texte et que 8 millions d'éléments donc tu peux remplacer la hashmap par un tableau brut. Les vecteurs c'est cher (3 pointeurs) je suis sûr qu'on peut faire moins. J'ai pas testé toutes ces idées donc pas sûr que ça aide, là dans l'immédiat je suis à 12 milliards chez moi, je testerai plus tard sans essayer de rester proche de ton code.
Mon graphe (11 Avril): ((Rogue Life || HH2) ; PythonExtra ; serial gint ; Boson X ; passe gint 3 ; ...) || (shoutbox v5 ; v5)
Fcalva Hors ligne Membre Points: 614 Défis: 10 Message

Citer : Posté le 08/01/2025 22:52 | #


Les include a rajouter sont les suivants :
#include <unordered_map>
#include <iostream>
#include <ostream>
#include <string>
#include <cstddef>
#include <vector>
#include <x86intrin.h>
#include <fstream>

Vous faites tourner le code avec 4x f2 si j'ai bien compris ? Sur mon portable j'ai 18,2-18,5B avec ça, je vais tenter des trucs pour voir
Pc master race - Apréciateur de Noctua moyen
Caltos : G35+EII, G90+E (briquée )
Tituya Hors ligne Administrateur Points: 2163 Défis: 26 Message

Citer : Posté le 08/01/2025 22:55 | #


Nop, un seul f2 de mon côté. En fait le join est sur 4 fichiers différents mais inutile de polluer le serveur de Breizh avec 1.2gio de données inutiles
Si quelqu'un les veux pour lancer le programme complet n'hésitez pas (mais j'en demandais pas autant)

Donc un f2 avec -O3 -march=native -mtune=native chez moi tourne entre 8.8 et 9.1B de cycles
Bretagne > Reste du globe
(Et de toute façon, vous pouvez pas dire le contraire)
Projet en cours : Adoranda

Mes programmes
Hésite pas à faire un test !


Fcalva Hors ligne Membre Points: 614 Défis: 10 Message

Citer : Posté le 08/01/2025 23:06 | #


Ok oui en plus si je mets pas les options qui vont ça ne va pas
J'ai 10-11B maintenant.
J'avais fait tourner avec 4x f2 vu que j'avais pris le programme entier
Pc master race - Apréciateur de Noctua moyen
Caltos : G35+EII, G90+E (briquée )
Tituya Hors ligne Administrateur Points: 2163 Défis: 26 Message

Citer : Posté le 11/01/2025 16:07 | #


Hey, pour la petite histoire j'ai cette présentation lundi après-midi. Des membres du groupe ont optimisé de leur côté en C donc mon implémentation C++ est un peu inutile à présent.
Par curiosité j'aimerai bien savoir ce que vous avez obtenu et comment, histoire d'avoir une "fin" à ce topic un peu inutile

(L'organisation du groupe a été catastrophique, aucune communication et chacun faisait les choses de son côté sans en parler aux autres, je ne connais même pas leur visage c'est pour dire )

Merci d'avoir cherché un peu et de m'avoir donné quelques pistes !
Bretagne > Reste du globe
(Et de toute façon, vous pouvez pas dire le contraire)
Projet en cours : Adoranda

Mes programmes
Hésite pas à faire un test !


Fcalva Hors ligne Membre Points: 614 Défis: 10 Message

Citer : Posté le 11/01/2025 16:15 | #


J'avais ré-écrit la partie séparation de valeurs avec stdio (fseek,getc,gets) mais c'était plus lent (vu que ça doit faire des syscalls je pense, en théorie ça fait beaucoup moins d'opérations).
J'avais aussi chronométré l'ajout dans la hashmap, qui représente ~10B cycles sur 10-11B total pour moi
Donc si il y a une piste d'opti c'est de réimplémenter une hashmap plus rapide que celle de la lib standard. Et puis éventuellement refaire la séparation avec des blocs de gets() au lieu de mes fseek() + gets() par valeur, mais encore ça va pas être si utile je pense.
Pc master race - Apréciateur de Noctua moyen
Caltos : G35+EII, G90+E (briquée )
Lephenixnoir En ligne Administrateur Points: 24769 Défis: 170 Message

Citer : Posté le 11/01/2025 18:06 | #


J'ai très envie de le faire proprement mais il faut plus d'infos. Il y a un gros compromis sur la structure de données entre le temps de construction et de requêtes. On peut avoir un ordre d'idée du nombre de requêtes à faire ?
Mon graphe (11 Avril): ((Rogue Life || HH2) ; PythonExtra ; serial gint ; Boson X ; passe gint 3 ; ...) || (shoutbox v5 ; v5)
Tituya Hors ligne Administrateur Points: 2163 Défis: 26 Message

Citer : Posté le 11/01/2025 18:29 | #


Avec mon implémentation C++ voici les stats, tu as besoin d'autre chose ? :

Nombre d'insertion dans la hashmap : 40 000 000 (dans 4 hashmap, respectivement 12M, 8M, 9M et 11M)
Nombre d'accès à un élément de la structure (hashmap.find()) : 29 597 674

Ces données sont tirées du Join complet entre f1 à f4, vu de cette manière il faut d'abord prioriser l'insertion plutôt que la recherche

Le code du join est le suivant :

void join(const FileMap& f1, const FileMap& f2, const FileMap& f3, const FileMap& f4) {
    std::ostream& buffer = std::cout;
    std::string line;

    for (const auto& [key, vec1] : f1) {
        auto it2 = f2.find(key);
        auto it3 = f3.find(key);
        if (it2 != f2.end() && it3 != f3.end()) {
            for (const auto& x1 : vec1) {
                for (const auto& x2 : it2->second) {
                    for (const auto& x3 : it3->second) {
                        auto it4 = f4.find(x3);
                        if (it4 != f4.end()) {
                            for (const auto& x4 : it4->second) {
                                line.clear();
                                line.append(x3)
                                  .append(",").append(key)
                                  .append(",").append(x1)
                                  .append(",").append(x2)
                                  .append(",").append(x4).append("\n");
                                buffer.write(line.c_str(), line.size());
                            }
                        }
                    }
                }
            }
        }
    }
}

Bretagne > Reste du globe
(Et de toute façon, vous pouvez pas dire le contraire)
Projet en cours : Adoranda

Mes programmes
Hésite pas à faire un test !


Lephenixnoir En ligne Administrateur Points: 24769 Défis: 170 Message

Citer : Posté le 11/01/2025 22:18 | #


Plus je le regarde moins je suis convaincu qu'on peut optimiser ce bout en isolation. L'algo du join est impliqué aussi. Par exemple dans ton code ci-dessus tu peux hoist le calcul de it4 et le test associé d'un niveau en intervertissant les boucles sur x2 et x3. Et comme il y a moins de requêtes que d'insertion, c'est pas immédiatement clair que ça vaut le coup de matérialiser toute la hashmap produit. Pendant que j'y suis, t'as 12, 8, 9 et 11 millions de lignes et le produit cartésien fait que 40 millions ?
Mon graphe (11 Avril): ((Rogue Life || HH2) ; PythonExtra ; serial gint ; Boson X ; passe gint 3 ; ...) || (shoutbox v5 ; v5)

LienAjouter une imageAjouter une vidéoAjouter un lien vers un profilAjouter du codeCiterAjouter un spoiler(texte affichable/masquable par un clic)Ajouter une barre de progressionItaliqueGrasSoulignéAfficher du texte barréCentréJustifiéPlus petitPlus grandPlus de smileys !
Cliquez pour épingler Cliquez pour détacher Cliquez pour fermer
Alignement de l'image: Redimensionnement de l'image (en pixel):
Afficher la liste des membres
:bow: :cool: :good: :love: ^^
:omg: :fusil: :aie: :argh: :mdr:
:boulet2: :thx: :champ: :whistle: :bounce:
valider
 :)  ;)  :D  :p
 :lol:  8)  :(  :@
 0_0  :oops:  :grr:  :E
 :O  :sry:  :mmm:  :waza:
 :'(  :here:  ^^  >:)

Σ π θ ± α β γ δ Δ σ λ
Veuillez donner la réponse en chiffre
Vous devez activer le Javascript dans votre navigateur pour pouvoir valider ce formulaire.

Si vous n'avez pas volontairement désactivé cette fonctionnalité de votre navigateur, il s'agit probablement d'un bug : contactez l'équipe de Planète Casio.

Planète Casio v4.3 © créé par Neuronix et Muelsaco 2004 - 2025 | Il y a 86 connectés | Nous contacter | Qui sommes-nous ? | Licences et remerciements

Planète Casio est un site communautaire non affilié à Casio. Toute reproduction de Planète Casio, même partielle, est interdite.
Les programmes et autres publications présentes sur Planète Casio restent la propriété de leurs auteurs et peuvent être soumis à des licences ou copyrights.
CASIO est une marque déposée par CASIO Computer Co., Ltd