Il y a 3 semaines, j’avais écrit un solveur de cube-serpent en Python.

Un commentaire, en apparence anodin, m’a mis dans la tête une question que je ne pouvais pas laisser sans réponse : combien de fois plus rapidement s’exécuterait le même algorithme implémenté en C que celui en Python (interprêté) ? 2× ? 10× ? 50× ?

Pour y répondre, il fallait donc implémenter le même algorithme en C. En plus, c’était l’occasion de rendre hommage à Dennis Ritchie. Après avoir découvert Python, j’ai donc (ré)appris le C (et ça fait drôle de rejouer avec les pointeurs après plusieurs années !).

Implémentation

Je ne vais pas m’attarder sur l’algorithme, c’est exactement le même principe que sur mon billet précédent, et j’ai essayé de garder les mêmes noms de fonctions.

La structure du cube et ses dimensions (à modifier selon votre cube-serpent) sont définies par macro (les fameux #define). Par rapport au programme Python, il faut en plus préciser la taille des tableaux.

La seule partie que j’ai réimplémentée complètement différemment est la fonction get_useful_points de la version Python (souvenez-vous, avec des yields dans une fonction récursive). La fonction équivalente dans la version C s’appelle symmetry_helper_inc_cursor(int cursor[]) : au lieu de retourner au fur et à mesure chacun des points à traiter, elle donne le point « suivant » de celui passé en paramètre.

De même, les solutions trouvées sont données dans un callback (la fonction solution), toujours dans l’objectif de supprimer simplement les yields.

J’ai tout mis dans un seul fichier csnakesolver.c (par simplicité, même si dans les règles de l’art, plusieurs fichiers .c avec leurs .h auraient été préférables).

Performances

Passons maintenant à ce qui nous intéresse : les performances.

Exemples de référence

Je fais mes tests sur 3 exemples : un rapide, un moyen et un long.

Le rapide est un 3×3×3, les deux autres sont des 4×4×4. Voici leurs structures respectives :

// (R)apide
{2, 1, 1, 2, 1, 2, 1, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2}
// (M)oyen
{2, 1, 2, 1, 1, 3, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 2, 3, 1, 1, 1, 3, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1}
// (L)ong
{1, 1, 2, 1, 1, 3, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1 ,1, 2, 2, 1, 1, 1, 1, 1, 2, 3, 1, 1, 1, 3, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3}

Protocole

Je vais comparer, grâce à la commande time, le temps nécessaire pour trouver une solution :

time ./csnakesolver

Le programme doit s’arrêter après avoir trouvé la première. Les programmes Python et C trouveront forcément les mêmes et dans le même ordre, vu qu’ils implémentent le même algorithme.

Compilation

Il est intéressant de tester les performances en compilant sans optimisations :

gcc csnakesolver.c -o csnakesolver

et avec :

gcc -O3 csnakesolver.c -o csnakesolver

Résultats

Python C (gcc) C (gcc -O3)
Exemple R instantané instantané instantané
Exemple M 5m6.903s 0m3.715s (×83) 0m0.826s (×372)
Exemple L 3h53m17.012s 5m4.533s (×46) 0m50.681s (×276)

Le gain est loin d’être négligeable, un gain de ×276 dans un cas et ×372 dans l’autre ! Honnêtement, je ne m’y attendais pas. Tout au plus, j’espérais ×40~×50, sans trop y croire.

Origines des gains de performances

Deux différences expliquent ces gains :

Il serait intéressant de savoir dans quelle mesure les gains proviennent de la compilation et dans quelle mesure ils proviennent de l’abstraction (nous savons déjà que le facteur de gain entre les programmes compilés avec et sans -O3 provient uniquement de la compilation).

Une approche intéressante pour répondre à cette question serait de compiler le programme Python en code natif (je n’ai encore jamais fait).

Débogueur

Travailler sur un mini-projet personnel permet toujours d’apprendre des choses. En dehors du langage lui-même, j’ai découvert le débogueur gdb.

N’ayant toujours utilisé des débogueurs qu’en mode graphique (pour d’autres langages), je m’attendais à ce qu’il soit un peu long à prendre en main. Mais en fait, pas du tout, j’ai été agréablement surpris par sa simplicité d’utilisation.

Avec certains langages, on peut se passer de débogueur pour de petits programmes. C ne fait pas partie de ceux-là, par exemple dans ce cas précis :

$ ./csnakesolver
Erreur de segmentation

Il faut d’abord compiler le programme avec l’option de debug :

gcc -g csnakesolver.c -o csnakesolver

Puis lancer le programme avec le débogueur :

gdb csnakesolver

Un prompt permet d’entrer des commandes :

(gdb) 

Pour placer un point d’arrêt à la ligne 215 :

(gdb) break 215

Pour démarrer le programme :

(gdb) run

Le programme s’arrête sur le point d’arrêt :

Breakpoint 1, volume_helper_can_move (vector=...) at csnakesolver.c:215
215	    int cursor_position_value = volume_helper.cursor[vector.position];

Pour afficher le bout de code source concerné :

(gdb) l
210	void volume_helper_set_flag(int cursor[], bool value) {
211	    * volume_helper_get_flag_pointer(cursor) = value;
212	}
213
214	bool volume_helper_can_move(vector_s vector) {
215	    int cursor_position_value = volume_helper.cursor[vector.position];
216	    int new_value = cursor_position_value + vector.value;
217	    int future_cursor[DIMENSIONS_COUNT];
218	    int sign, i, abs_value;
219	    if (new_value < 0 || new_value >= dimensions[vector.position]) {

Il est possible de consulter les valeurs des variables grâce à p (print) :

(gdb) p vector.position
$1 = 0
(gdb) p vector
$2 = {position = 0, value = 1}

Pour afficher des tableaux, il faut indiquer le pointeur et la longueur du tableau à afficher (ici 3) :

(gdb) p * volume_helper.cursor @ 3
$3 = {0, 0, 0}

Pour obtenir la pile d’exécution :

(gdb) bt
#0  volume_helper_can_move (vector=...) at csnakesolver.c:215
#1  0x0000000000401294 in solve_rec (init_cursor=0x7fffffffe210, step=0)
    at csnakesolver.c:438
#2  0x0000000000401191 in solve () at csnakesolver.c:407
#3  0x00000000004013de in main () at csnakesolver.c:475

Pour avancer dans le programme, 3 commandes sont indispensables :

  • c (continue) pour dérouler le programme jusqu’au prochain point d’arrêt ;
  • n (next) pour exécuter la ligne suivante complètement ;
  • s (step) pour rentrer dans la fonction sur la ligne suivante et l’exécuter ligne à ligne.

Ces commandes essentielles permettent déjà de se sortir de beaucoup de situations.

Conclusion

Un programme écrit en C est plus rapide qu’un programme écrit en Python (ah bon ?), dans une proportion plus importante que je ne l’imaginais. Ce n’est qu’un test sur un exemple particulier, mais il donne déjà une petite idée.

La morale de l’histoire est qu’il faut bien choisir son langage suivant le programme à réaliser. Et pour du calcul brut, évidemment un langage bas niveau est préférable (même si le développement est plus laborieux). Dans la majorité des cas cependant, où les performances brutes ne sont pas cruciales, Python sera préféré à C.

Code source

Le programme est disponible au même endroit que la version Python : csnakesolver-0.1.c.

Par contre, désolé, cette version est beaucoup moins commentée que la version Python.

/*
 * csnakesolver v0.1, 19th october 2011
 *
 * changelog:
 *   0.1
 *     - initial version
 *
 * Solver for generalized snake-cube:
 * http://en.wikipedia.org/wiki/Snake_cube
 * http://fr.wikipedia.org/wiki/Cube_serpent
 *
 * By Romain Vimont (®om)
 *   rom@rom1v.com
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>

#define EXEMPLE_L // change with EXEMPLE_M or EXEMPLE_L

#ifdef EXEMPLE_R
#define SNAKE_STRUCTURE {2, 1, 1, 2, 1, 2, 1, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2}
#define STRUCTURE_VECTOR_COUNT 17
#define VOLUME_DIMENSIONS {3, 3, 3}
#define DIMENSIONS_COUNT 3
#endif

#ifdef EXEMPLE_M
#define SNAKE_STRUCTURE {2, 1, 2, 1, 1, 3, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 2, 3, 1, 1, 1, 3, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1}
#define STRUCTURE_VECTOR_COUNT 46
#define VOLUME_DIMENSIONS {4, 4, 4}
#define DIMENSIONS_COUNT 3
#endif

#ifdef EXEMPLE_L
#define SNAKE_STRUCTURE {1, 1, 2, 1, 1, 3, 1, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1 ,1, 2, 2, 1, 1, 1, 1, 1, 2, 3, 1, 1, 1, 3, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3}
#define STRUCTURE_VECTOR_COUNT 47
#define VOLUME_DIMENSIONS {4, 4, 4}
#define DIMENSIONS_COUNT 3
#endif

#define VARIABLES {'x', 'y', 'z', 't'}
#define VARIABLES_COUNT 4

int structure[] = SNAKE_STRUCTURE;
int dimensions[] = VOLUME_DIMENSIONS;
int variables[] = VARIABLES;

int structure_length; // sum of SNAKE_STRUCTURE
int volume_size; // product of dimensions

typedef struct vector {
    int position;
    int value;
} vector_s;

typedef struct volume_helper {
    bool * flags; // length = product of all VOLUME_DIMENSIONS items
    vector_s path[STRUCTURE_VECTOR_COUNT];
    int path_length;
    int init_cursor[DIMENSIONS_COUNT];
    int cursor[DIMENSIONS_COUNT];
} volume_helper_s;

typedef struct symmetry_helper {
    int eq_classes_path[DIMENSIONS_COUNT * STRUCTURE_VECTOR_COUNT + 2];
    int * eq_classes;
} symmetry_helper_s;

vector_s new_vector(char position, char value);
void symmetry_helper_init(symmetry_helper_s * symmetry_helper);
void volume_helper_init(volume_helper_s * volume_helper);
void init();

char * vector_to_string(vector_s vector);
char * get_variable(char position);
char * get_canonical_number(char number);

char * cursor_to_string(int cursor[]);

void volume_helper_set_cursor(int cursor[]);
bool * volume_helper_get_flag_pointer(int cursor[]);
bool volume_helper_get_flag(int cursor[]);
void volume_helper_set_flag(int cursor[], bool value);
bool volume_helper_can_move(vector_s vector);
void volume_helper_move(vector_s vector);
void volume_helper_back();
void volume_helper_append_vector_in_path(vector_s vector) ;
vector_s volume_helper_pop_vector_in_path();

void symmetry_helper_init_eq_classes_from_dimensions();
bool symmetry_helper_inc_cursor(int cursor[]);
bool symmetry_helper_inc_cursor_rec(int cursor[], int index);
void symmetry_helper_set_cursor(int cursor[]);
void symmetry_helper_move(vector_s vector);
void symmetry_helper_back();
bool symmetry_helper_must_explore(int i);
bool eq_cmp(int p1, int p2, int dim);

bool solve();
bool solve_rec(int init_cursor[], int step);
bool solution(int * init_cursor, vector_s * path);

vector_s new_vector(char position, char value) {
    vector_s vector;
    vector.position = position;
    vector.value = value;
    return vector;
};

symmetry_helper_s symmetry_helper;
volume_helper_s volume_helper;

void symmetry_helper_init(symmetry_helper_s * symmetry_helper) {
    symmetry_helper->eq_classes = symmetry_helper->eq_classes_path;
}

void volume_helper_init(volume_helper_s * volume_helper) {
    volume_helper->flags = (bool *) malloc(volume_size * sizeof(bool));
    volume_helper->path_length = 0;
}

void init() {
    int i;
    volume_size = 1;
    for (i = 0; i < DIMENSIONS_COUNT; i++) {
        volume_size *= dimensions[i];
    }
    structure_length = 0;
    for (i = 0; i < STRUCTURE_VECTOR_COUNT; i++) {
        structure_length += structure[i];
    }
    symmetry_helper_init(&symmetry_helper);
    volume_helper_init(&volume_helper);
}

char * vector_to_string(vector_s vector) {
    char * string = (char *) malloc(5 * sizeof(char));
    char * variable = get_variable(vector.position);
    char * canonical_number = get_canonical_number(vector.value);
    sprintf(string, "%s%s", canonical_number, variable);
    free(variable);
    free(canonical_number);
    return string;
}

char * get_variable(char position) {
    char * variable = (char *) malloc(3 * sizeof(char));
    if (position < VARIABLES_COUNT) {
        sprintf(variable, "%c", variables[position]);
    } else {
        sprintf(variable, "k%i", position - VARIABLES_COUNT);
    }
    return variable;
}

char * get_canonical_number(char number) {
    char * canonical_number = (char *) malloc(3 * sizeof(char));
    if (number == (char) 1) {
        sprintf(canonical_number, "");
    } else if (number == (char) -1) {
        sprintf(canonical_number, "-");
    } else {
        sprintf(canonical_number, "%i", number);
    }
    return canonical_number;
}

char * cursor_to_string(int cursor[]) {
    char * result = (char *) malloc(255 * sizeof(char));
    char * s = result;
    int i;
    s += sprintf(s, "(");
    for (i = 0; i < DIMENSIONS_COUNT; i++) {
        if (i != 0) {
            s += sprintf(s, ", ");
        }
        s += sprintf(s, "%i", cursor[i]);
    }
    s += sprintf(s, ")");
    return result;
}

void volume_helper_set_cursor(int cursor[]) {
    volume_helper_set_flag(volume_helper.init_cursor, false);
    memcpy(volume_helper.init_cursor, cursor, DIMENSIONS_COUNT * sizeof(int));
    memcpy(volume_helper.cursor, cursor, DIMENSIONS_COUNT * sizeof(int));
    volume_helper_set_flag(cursor, true);
}

bool * volume_helper_get_flag_pointer(int cursor[]) {
    bool * p_flag = volume_helper.flags;
    int product = 1;
    int i;
    for (i = DIMENSIONS_COUNT - 1; i >= 0; i--) {
        p_flag += cursor[i] * product;
        product *= dimensions[i];
    }
    return p_flag;
}

bool volume_helper_get_flag(int cursor[]) {
    return * volume_helper_get_flag_pointer(cursor);
}

void volume_helper_set_flag(int cursor[], bool value) {
    * volume_helper_get_flag_pointer(cursor) = value;
}

bool volume_helper_can_move(vector_s vector) {
    int cursor_position_value = volume_helper.cursor[vector.position];
    int new_value = cursor_position_value + vector.value;
    int future_cursor[DIMENSIONS_COUNT];
    int sign, i, abs_value;
    if (new_value < 0 || new_value >= dimensions[vector.position]) {
        return false;
    }
    memcpy(future_cursor, volume_helper.cursor, DIMENSIONS_COUNT * sizeof(int));
    if (vector.value < 0) {
        sign = -1;
    } else {
        sign = 1;
    }
    abs_value = sign * vector.value;
    for (i = 0; i < abs_value; i++) {
        future_cursor[vector.position] += sign;
        if (volume_helper_get_flag(future_cursor)) {
            return false;
        }
    }
    return true;
}

void volume_helper_move(vector_s vector) {
    int sign, i, abs_value;
    volume_helper_append_vector_in_path(vector);
    if (vector.value < 0) {
        sign = -1;
    } else {
        sign = 1;
    }
    abs_value = sign * vector.value;
    for (i = 0; i < abs_value; i++) {
        volume_helper.cursor[vector.position] += sign;
        volume_helper_set_flag(volume_helper.cursor, true);
    }
}

void volume_helper_back() {
    int sign, i, abs_value;
    vector_s vector = volume_helper_pop_vector_in_path();
    if (vector.value < 0) {
        sign = -1;
    } else {
        sign = 1;
    }
    abs_value = sign * vector.value;
    for (i = 0; i < abs_value; i++) {
        volume_helper_set_flag(volume_helper.cursor, false);
        volume_helper.cursor[vector.position] += -sign;
    }
}

void volume_helper_append_vector_in_path(vector_s vector) {
    vector_s * current_vector = volume_helper.path + volume_helper.path_length;
    memcpy(current_vector, &vector, sizeof(vector_s));
    volume_helper.path_length ++;
}

vector_s volume_helper_pop_vector_in_path() {
    volume_helper.path_length --;
    vector_s * current_vector = volume_helper.path + volume_helper.path_length;
    vector_s vector;
    memcpy(&vector, current_vector, sizeof(vector));
    return vector;
}

void symmetry_helper_init_eq_classes_from_dimensions() {
    // eq_classes from dimensions is the first item of eq_classes_path
    int * eq_classes = symmetry_helper.eq_classes_path;
    int i, j;
    int value;
    bool found;
    for (i = 1; i < DIMENSIONS_COUNT; i++) {
        value = dimensions[i];
        j = 0;
        found = false;
        while (j < i && !found) {
            if (dimensions[j] == value) {
                eq_classes[i] = j;
                found = true;
            } else {
                j++;
            }
        }
        if (!found) {
            eq_classes[j] = j;
        }
    }
    symmetry_helper.eq_classes = eq_classes;
}

bool symmetry_helper_inc_cursor(int cursor[]) {
    return symmetry_helper_inc_cursor_rec(cursor, DIMENSIONS_COUNT - 1);
}

bool symmetry_helper_inc_cursor_rec(int cursor[], int index) {
    int * eq_classes = symmetry_helper.eq_classes_path;
    int value = cursor[index];
    int i;
    if (value < (dimensions[index] - 1) / 2) {
        // the last coordinate can be incremented
        cursor[index] ++;
        return true;
    }
    // we must increment recursively the previous coordinate
    if (index == 0) {
        // there is no more coordinate to increment
        return false;
    }
    i = index - 1;
    if (!symmetry_helper_inc_cursor_rec(cursor, i)) {
        return false;
    }
    while (i >= 0 && eq_classes[i] != eq_classes[index]) {
        i--;
    }
    if (i >= 0) {
        // coordinate value must at least equals the previous coordinates
        // in the same equivalence class
        cursor[index] = cursor[i];
    } else {
        cursor[index] = 0;
    }
    return true;
}

void symmetry_helper_set_cursor(int cursor[]) {
    int * eq_classes_path = symmetry_helper.eq_classes_path;
    int * cursor_eq_classes = symmetry_helper.eq_classes_path + DIMENSIONS_COUNT;
    int i, j, old_class;

    symmetry_helper.eq_classes = cursor_eq_classes;
    // copy the eq_classes computed from the dimensions into the next segment
    memcpy(cursor_eq_classes, eq_classes_path, DIMENSIONS_COUNT * sizeof(int));
    for (i = 0; i < DIMENSIONS_COUNT; i++) {
        if (cursor_eq_classes[i] != i && !eq_cmp(cursor_eq_classes[i], cursor[i], dimensions[i])) {
            old_class = cursor_eq_classes[i];
            cursor_eq_classes[i] = i;
            for (j = i + 1; j < DIMENSIONS_COUNT; j++) {
                if (cursor_eq_classes[j] == old_class) {
                    cursor_eq_classes[j] = i;
                }
            }
        }
    }
}

void symmetry_helper_move(vector_s vector) {
    int position = vector.position;
    int * previous_eq_classes = symmetry_helper.eq_classes;
    int * new_eq_classes = previous_eq_classes + DIMENSIONS_COUNT;

    int new_eq_class = -1;
    int i;
    memcpy(new_eq_classes, previous_eq_classes, DIMENSIONS_COUNT * sizeof(int));
    for (i = position + 1; i < DIMENSIONS_COUNT; i++) {
        if (new_eq_classes[i] == position) {
            if (new_eq_class == -1) {
                new_eq_class = i;
            }
            new_eq_classes[i] = new_eq_class;
        }
    }
    symmetry_helper.eq_classes = new_eq_classes;
}

void symmetry_helper_back() {
    symmetry_helper.eq_classes -= DIMENSIONS_COUNT;
}

bool symmetry_helper_must_explore(int i) {
    return symmetry_helper.eq_classes[i] == i;
}

bool eq_cmp(int p1, int p2, int dim) {
    return p1 == p2 || p1 + p2 + 1 == dim;
}

bool solve() {
    int cursor[DIMENSIONS_COUNT] = {}; // init with zeros
    int i;
    if (structure_length + 1 != volume_size) {
        fprintf(stderr,
                "Structure has not the right length (%i instead of %i)\n",
                structure_length, volume_size);
        return false;
    }

    do {
        volume_helper_set_cursor(cursor);
        symmetry_helper_set_cursor(cursor);
        if (!solve_rec(cursor, 0)) {
            return false;
        }
    } while (symmetry_helper_inc_cursor(cursor));

    // explored all possible solutions
    return true;
}

bool solve_rec(int init_cursor[], int step) {
    int previous_position;
    int norm = structure[step];
    int vector_value;
    int i, k;
    vector_s possible_vector;
    if (step == STRUCTURE_VECTOR_COUNT) {
        if (!solution(volume_helper.init_cursor, volume_helper.path)) {
            // stop searching for new solutions
            return false;
        }
    } else {
        if (volume_helper.path_length == 0) {
            previous_position = -1;
        } else {
            previous_position = volume_helper.path[volume_helper.path_length -1].position;
        }
        for (i = 0; i < DIMENSIONS_COUNT; i++) {
            if (i != previous_position && symmetry_helper_must_explore(i)) {
                for (k = 0; k < 2; k++) {
                    vector_value = k == 0 ? norm : -norm;
                    possible_vector = new_vector(i, vector_value);
                    if (volume_helper_can_move(possible_vector)) {
                        volume_helper_move(possible_vector);
                        symmetry_helper_move(possible_vector);
                        if (!solve_rec(init_cursor, step + 1)) {
                            return false;
                        }
                        volume_helper_back();
                        symmetry_helper_back();
                    }
                }
            }
        }
    }
    return true;
}

bool solution(int * init_cursor, vector_s * path) {
    int i;
    char * vector_string;
    vector_s vector;
    printf("(%s, [", cursor_to_string(init_cursor));
    for (i = 0; i < STRUCTURE_VECTOR_COUNT; i++) {
        if (i != 0) {
            printf(", ");
        }
        vector = path[i];
        vector_string = vector_to_string(vector);
        printf("%s", vector_string);
        free(vector_string);
    }
    printf("])\n");
    // stop after the first solution
    return false;
}

int main(void) {
    init();
    solve();
    exit(0);
}