2 votes

Différence inexplicable dans les résultats de qsort() sous macOS et Linux

J'ai eu récemment un étudiant qui a écrit à l'intérieur de son comparateur :

int comparator (const void *p, const void *q) {
    const data_t *indexp = *(data_t **)p;
    const data_t *indexq = *(data_t **)q;
    return indexp->index > indexq->index;
}

Cela a fonctionné sans problème sous Ubuntu 16.04 LTS, mais n'a pas réussi à trier correctement lorsqu'il a été exécuté sous macOS. Il suffit de modifier le paramètre > à un - était suffisant pour le faire fonctionner sur les deux plateformes.

Alors, qu'est-ce qui se passe ici ? La seule chose à laquelle je peux penser est que l'inégalité signifie une perte de la -1 mais apparemment l'implémentation Linux (ou GCC) de la valeur qsort() n'est pas affectée ?

Comment est-ce possible ?

EDITAR:

Je voulais vérifier si c'était spécifique au compilateur, j'ai donc compilé avec clang sous Ubuntu 16.04 et il n'y a pas de changement, ça fonctionne très bien là. Donc, je pense qu'il y a quelque chose à propos de la version actuelle du programme. qsort() dans libc par rapport à macOS (BSD libc ?) qui provoque ce phénomène. Je suis vraiment curieux de savoir ce que c'est !

3voto

rici Points 45980

Il ne devrait pas être nécessaire de souligner que la fonction de comparaison dans le PO est incorrecte, et que son utilisation est un comportement non défini. Néanmoins, il est intéressant de se demander comment cela pourrait fonctionner.


Bien que l'interface avec qsort exige une fonction de comparaison qui renvoie un entier, en prévoyant trois valeurs de retour possibles, rien n'oblige l'utilisateur de l qsort la mise en œuvre pour utiliser toutes ces informations. Elle pourrait toujours effectuer la même branche simple à deux voies. Cela serait similaire à la méthode Bibliothèque standard C++ sort fonction dont le comparateur renvoie un booléen, vrai si le premier argument est strictement inférieur au second.

Bien entendu, la fonction comparateur de votre élève n'indiquera jamais que le premier argument est strictement inférieur au second, ce qui poserait des problèmes pour une implémentation dans laquelle les tests ressemblent à quelque chose du genre :

if (*cmp(p, q) < 0) ...

Cependant, si le comparateur est toujours appelé comme :

if (*cmp(p, q) > 0) ...

(ou, de manière équivalente, <= ), alors le comparateur de votre élève fonctionnera sans problème.

Comme la glibc est open source, il est facile de vérifier, une fois que nous avons trouvé la fonction de tri correcte. Mais tout n'est pas tout à fait comme il semble. Il y a un fichier source appelé qsort.c (dans le stdlib ), qui implémente l'algorithme quicksort ( pas l'algorithme d'introsort utilisé par les plus modernes std::sort ), et dans ce fichier nous pouvons voir une utilisation cohérente de strict less than. Par exemple, dans le fichier boucle interne principale :

while ((*cmp) ((void *) left_ptr, (void *) mid, arg) < 0)
  left_ptr += size;
while ((*cmp) ((void *) mid, (void *) right_ptr, arg) < 0)
  right_ptr -= size;

(Il existe de nombreuses autres comparaisons dans la fonction, mais elles ont toutes la même forme).

Cela ne devrait donc pas fonctionner du tout avec le comparateur de votre élève. Cependant, il s'avère que ce n'est pas la fonction habituellement appelée par qsort . qsort est en fait défini dans le fichier msort.c qui implémente un tri par fusion. Et la fonction de tri par fusion effectue des tests de type "moins que ou égal à". Par exemple :

 if ((*cmp) (b1, b2, arg) <= 0)

L'implémentation du tri par fusion n'est pas in-place, ce qui serait plus lent ; elle nécessite une mémoire temporaire. Et cela pourrait être un problème, car qsort devrait fonctionner même si aucune mémoire temporaire n'est disponible. Ainsi, l'implémentation réelle de qsort commence par essayer de savoir si la mémoire physique disponible est suffisante. (Il veut de la mémoire physique, pas de l'espace de swap, afin d'éviter le swapping. Rappelez-vous également que Linux procède à une allocation optimiste, de sorte que le fait que malloc réussisse n'implique pas nécessairement que les adresses de mémoire virtuelle allouées soient réellement utilisables). Ici, il a calculé l'espace temporaire nécessaire (en milliers d'euros). size ) et utilisé sysconf pour demander combien de mémoire physique possède l'hôte. (Il divise cette valeur par quatre afin de ne pas monopoliser toute la mémoire physique).

/* If the memory requirements are too high don't allocate memory.  */
if (size / pagesize > (size_t) phys_pages)
  {
    _quicksort (b, n, s, cmp, arg);
    return;
  }

Donc, si le tri par fusion nécessite trop de mémoire, il s'en remet à _quicksort qui est précisément la fonction que j'ai citée précédemment et qui se trouve dans qsort.c .

En résumé :

  • La fonction de comparateur défectueux fonctionnera avec la glibc qsort à condition que le tableau à trier ne soit pas trop grand.

  • Si le tableau est trop grand, le tri échouera.

En revanche, le système BSD qsort utilise les trois valeurs de retour des comparateurs. Dans son boucle intérieure nous voyons :

    while (pb <= pc && (cmp_result = CMP(thunk, pb, a)) <= 0) {
        if (cmp_result == 0) {
            swap_cnt = 1;
            swap(pa, pb);
            pa += es;
        }
        pb += es;
    }
    while (pb <= pc && (cmp_result = CMP(thunk, pc, a)) >= 0) {
        if (cmp_result == 0) {
            swap_cnt = 1;
            swap(pc, pd);
            pd -= es;
        }
        pc -= es;
    }

Il y a également des appels au comparateur pour trouver le pivot ; ce sont tous les < ils produiront donc également une mauvaise réponse. Cependant, tout n'est pas perdu ; pour les très petits vecteurs (moins de sept éléments), il utilise le tri par insertion à la place, et la boucle de tri par insertion utilise une comparaison stricte de type plus grand que.

2voto

dbush Points 8590

L'utilisation de > est en effet un bug. Cette opération ne donne qu'un résultat de 0 ou 1. Utilisation de - est la méthode correcte.

La raison pour laquelle cela a fonctionné sur l'un mais pas sur l'autre est très probablement due à la façon dont la liste à trier était initialement ordonnée. En cas de doute, exécutez le code à travers Valgrind sur Linux.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X