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;
}
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...
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.
(Et de toute façon, vous pouvez pas dire le contraire)
MultipliCasio
RDM Calculs
Back Mirror
A Switch To The Top C
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.
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.
Citer : Posté le 08/01/2025 22:52 | #
Les include a rajouter sont les suivants :
#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
Caltos : G35+EII, G90+E (briquée )
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
(Et de toute façon, vous pouvez pas dire le contraire)
MultipliCasio
RDM Calculs
Back Mirror
A Switch To The Top C
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
Caltos : G35+EII, G90+E (briquée )
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 !
(Et de toute façon, vous pouvez pas dire le contraire)
MultipliCasio
RDM Calculs
Back Mirror
A Switch To The Top C
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.
Caltos : G35+EII, G90+E (briquée )
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 ?
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 :
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());
}
}
}
}
}
}
}
}
(Et de toute façon, vous pouvez pas dire le contraire)
MultipliCasio
RDM Calculs
Back Mirror
A Switch To The Top C
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 ?