Concours de code sournois: Tri pas si rapide [fermé]


28

La tâche

Écrivez un programme, dans la langue de votre choix, qui lit les lignes d'entrée de l'entrée standard jusqu'à EOF, puis les écrit sur la sortie standard dans un ordre ASCIIbétique, similaire au sortprogramme de ligne de commande. Un exemple court et non sournois en Python est:

import sys

for line in sorted(sys.stdin):
    print(line.rstrip('\n'))

La partie sournoise

Semblable à The OS War , votre objectif est de prouver que votre plate-forme préférée est «meilleure», en faisant délibérément fonctionner votre programme beaucoup plus lentement sur une plate-forme concurrente. Pour les besoins de ce concours, une «plate-forme» comprend toute combinaison de:

  • Processeur
    • Architecture (x86, Alpha, ARM, MIPS, PowerPC, etc.)
    • Témoin (64 bits contre 32 bits contre 16 bits)
    • Gros ou petit endian
  • Système opérateur
    • Windows vs Linux vs Mac OS, etc.
    • Différentes versions du même système d'exploitation
  • Implémentation du langage
    • Différents fournisseurs de compilateurs / interprètes (par exemple, MSVC ++ vs GCC)
    • Différentes versions du même compilateur / interprète

Bien que vous puissiez répondre aux exigences en écrivant du code comme:

#ifndef _WIN32
    Sleep(1000);
#endif

Une telle réponse ne devrait pas être votée positivement. Le but est d'être subtil. Idéalement, votre code devrait regarder comme ce n'est pas dépendant de la plate - forme du tout. Si vous n'avez des déclarations (ou des conditions basées sur ou ou autre), ils devraient avoir une justification plausible (basée sur un mensonge, bien sûr).#ifdefos.nameSystem.Environment.OSVersion

Inclure dans votre réponse

  • Le code
  • Vos plateformes «favorites» et «défavorisées».
  • Une entrée avec laquelle tester votre programme.
  • Le temps d'exécution sur chaque plate-forme, pour la même entrée.
  • Une description des raisons pour lesquelles le programme s'exécute si lentement sur la plate-forme défavorable.

4
C'est plus difficile que je ne le pensais. Les seules réponses que je peux trouver sont soit très longues et un peu évidentes, soit très courtes et extrêmement évidentes :-(
squeamish ossifrage

2
Je vote pour fermer cette question comme hors sujet car les défis sournois ne sont plus sur le sujet sur ce site. meta.codegolf.stackexchange.com/a/8326/20469
cat

Réponses:


29

C

CleverSort

CleverSort est un algorithme de tri de chaîne en deux étapes de pointe (c'est-à-dire sur-conçu et sous-optimal).

À l'étape 1, il commence par pré-trier les lignes d'entrée à l'aide du tri radix et les deux premiers octets de chaque ligne. Le tri Radix n'est pas comparatif et fonctionne très bien pour les chaînes.

À l'étape 2, il utilise le tri par insertion sur la liste de chaînes pré-triée. Comme la liste est presque triée après l'étape 1, le tri par insertion est assez efficace pour cette tâche.

Code

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Convert first two bytes of Nth line into integer

#define FIRSTSHORT(N) *((uint16_t *) input[N])

int main()
{
    char **input = 0, **output, *ptemp;
    int first_index[65536], i, j, lines = 0, occurrences[65536];
    size_t temp;

    // Read lines from STDIN

    while(1)
    {
        if(lines % 1000 == 0)
            input = realloc(input, 1000 * (lines / 1000 + 1) * sizeof(char*));

        if(getline(&input[lines], &temp, stdin) != -1)
            lines++;
        else
            break;
    }

    output = malloc(lines * sizeof(char*));

    // Radix sort

    memset(occurrences, 0, 65536 * sizeof(int));

    for(i = 0; i < lines; i++) occurrences[FIRSTSHORT(i)]++;

    first_index[0] = 0;

    for(i = 0; i < 65536 - 1; i++)
        first_index[i + 1] = first_index[i] + occurrences[i];

    memset(occurrences, 0, 65536 * sizeof(int));

    for(i = 0; i < lines; i++)
    {
        temp = FIRSTSHORT(i), output[first_index[temp] + occurrences[temp]++] = input[i];
    }

    // Insertion sort

    for(i = 1; i < lines; i++)
    {
        j = i;

        while(j > 0 && strcmp(output[j - 1], output[j]) > 0)
            ptemp = output[j - 1], output[j - 1] = output[j], output[j] = ptemp, j--;
    }

    // Write sorted lines to STDOUT

    for(i = 0; i < lines; i++)
        printf("%s", output[i]);
}

Plateformes

Nous savons tous que les machines big-endian sont beaucoup plus efficaces que leurs homologues little-endian. Pour l'analyse comparative, nous compilerons CleverSort avec les optimisations activées et créerons au hasard une énorme liste (un peu plus de 100 000 chaînes) de lignes de 4 octets:

$ gcc -o cleversort -Ofast cleversort.c
$ head -c 300000 /dev/zero | openssl enc -aes-256-cbc -k '' | base64 -w 4 > input
$ wc -l input
100011 input

Référence Big-endian

$ time ./cleversort < input > /dev/null

real    0m0.185s
user    0m0.181s
sys     0m0.003s

Pas trop mal.

Bechmark de Little-endian

$ time ./cleversort < input > /dev/null

real    0m27.598s
user    0m27.559s
sys     0m0.003s

Boo, petit Endian! Huer!

La description

Le tri par insertion est vraiment assez efficace pour les listes presque triées, mais il est horriblement inefficace pour les listes triées aléatoirement.

La partie sournoise de CleverSort est la macro FIRSTSHORT :

#define FIRSTSHORT(N) *((uint16_t *) input[N])

Sur les machines big-endian, ordonner une chaîne de deux entiers 8 bits lexicographiquement ou les convertir en entiers 16 bits et les ordonner ensuite donne les mêmes résultats.

Naturellement, cela est également possible sur les machines peu endiennes, mais la macro aurait dû être

#define FIRSTSHORT(N) (input[N][0] | (input[N][1] >> 8))

qui fonctionne comme prévu sur toutes les plateformes.

Le "benchmark big-endian" ci-dessus est en fait le résultat de l'utilisation de la macro appropriée.

Avec la mauvaise macro et une petite machine endian, la liste est pré-triée par le deuxième caractère de chaque ligne, résultant en un ordre aléatoire du point de vue lexicographique. Le tri par insertion se comporte très mal dans ce cas.


16

Python 2 contre Python 3

Évidemment, Python 3 est plus rapide de plusieurs ordres de grandeur que Python 2. Prenons l'exemple de cette implémentation de l' algorithme Shellsort :

Code

import sys
from math import log

def shellsort(lst):

    ciura_sequence = [1, 4, 10, 23, 57, 132, 301, 701]  # best known gap sequence (Ciura, 2001)

    # check if we have to extend the sequence using the formula h_k = int(2.25h_k-1)
    max_sequence_element = 1/2*len(lst)
    if ciura_sequence[-1] <= max_sequence_element:
        n_additional_elements = int((log(max_sequence_element)-log(701)) / log(2.25))
        ciura_sequence += [int(701*2.25**k) for k in range(1,n_additional_elements+1)]
    else:
        # shorten the sequence if necessary
        while ciura_sequence[-1] >= max_sequence_element and len(ciura_sequence)>1:
            ciura_sequence.pop()

    # reverse the sequence so we start sorting using the largest gap
    ciura_sequence.reverse()

    # shellsort from http://sortvis.org/algorithms/shellsort.html
    for h in ciura_sequence:
        for j in range(h, len(lst)):
            i = j - h
            r = lst[j]
            flag = 0
            while i > -1:
                if r < lst[i]:
                    flag = 1
                    lst[i+h], lst[i] = lst[i], lst[i+h]
                    i -= h
                else:
                    break
            lst[i+h] = r

    return lst

# read from stdin, sort and print
input_list = [line.strip() for line in sys.stdin]
for line in shellsort(input_list):
    print(line)

assert(input_list==sorted(input_list))

Référence

Préparez une entrée de test. Ceci est tiré de la réponse de Dennis mais avec moins de mots - Python 2 est tellement lent ...

$ head -c 100000 /dev/zero | openssl enc -aes-256-cbc -k '' | base64 -w 4 > input

Python 2

$ time python2 underhanded2.py < input > /dev/null 

real    1m55.267s
user    1m55.020s
sys     0m0.284s

Python 3

$ time python3 underhanded2.py < input > /dev/null 

real    0m0.426s
user    0m0.420s
sys     0m0.006s

Où est le code sournois?

Je suppose que certains lecteurs peuvent vouloir traquer le filou eux-mêmes, alors je cache la réponse avec une balise de spoiler.

L'astuce est la division entière dans le calcul du max_sequence_element. En Python 2, il est 1/2évalué à zéro et, par conséquent, l'expression est toujours nulle. Cependant, le comportement de l'opérateur a changé en division à virgule flottante dans Python 3. Comme cette variable contrôle la longueur de la séquence d'intervalle, qui est un paramètre critique de Shellsort, Python 2 finit par utiliser une séquence qui ne contient que le numéro un tandis que Python 3 utilise la séquence correcte. Il en résulte un temps d'exécution quadratique pour Python 2.

Bonus 1:

Vous pouvez corriger le code en ajoutant simplement un point après le 1ou le 2dans le calcul.

Bonus 2:

Au moins sur ma machine, Python 2 est un peu plus rapide que Python 3 lors de l'exécution du code fixe ...


Bien joué! Temps Nitpix: flagsemble en écriture seule, ne pouvez-vous pas le supprimer? En outre, cela rsemble superflu si vous le faites if lst[i+h] < lst[i]: .... D'un autre côté, si vous continuez rpourquoi le swap? Ne pourriez-vous pas simplement faire lst[i+h] = lst[i]? Tout cela est-il une distraction intentionnelle?
Jonas Kölker
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.