Concorso codice subdolo: ordinamento non così rapido [chiuso]


28

L'obiettivo

Scrivi un programma, nella lingua che preferisci, che legge le righe di input dall'input standard fino a EOF, quindi le scrive nell'output standard in ordine alfabetico ASCII, simile al programma della sortriga di comando. Un breve esempio non subdolo in Python è:

import sys

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

La parte subdola

Simile a The OS War , il tuo obiettivo è dimostrare che la tua piattaforma preferita è "migliore", facendo deliberatamente eseguire il tuo programma molto più lentamente su una piattaforma concorrente. Per il bene di questo concorso, una "piattaforma" è costituita da qualsiasi combinazione di:

  • Processore
    • Architettura (x86, Alpha, ARM, MIPS, PowerPC, ecc.)
    • Bitness (64 bit contro 32 bit contro 16 bit)
    • Big-endus little-endian
  • Sistema operativo
    • Windows vs. Linux vs. Mac OS, ecc.
    • Versioni diverse dello stesso sistema operativo
  • Implementazione del linguaggio
    • Diversi produttori di compilatori / interpreti (ad esempio, MSVC ++ vs. GCC)
    • Versioni diverse dello stesso compilatore / interprete

Anche se potresti soddisfare i requisiti scrivendo codice come:

#ifndef _WIN32
    Sleep(1000);
#endif

Tale risposta non dovrebbe essere votata. L'obiettivo è essere sottili. Idealmente, il tuo codice dovrebbe apparire come se non dipendesse affatto dalla piattaforma. Se non avete #ifdefdichiarazioni (o condizioni basate su os.nameo System.Environment.OSVersiono qualsiasi altra cosa), essi dovrebbero avere una giustificazione plausibile (sulla base di una menzogna, naturalmente).

Includi nella tua risposta

  • Il codice
  • Le tue piattaforme "preferite" e "sfavorevoli".
  • Un input con cui testare il tuo programma.
  • Il tempo di esecuzione su ciascuna piattaforma, per lo stesso input.
  • Una descrizione del motivo per cui il programma viene eseguito così lentamente sulla piattaforma sfavorevole.

4
Questo è più difficile di quanto pensassi. Le uniche risposte che posso trovare sono o molto lunghe e un po 'ovvie, o molto brevi ed estremamente ovvie :-(
ossifrage schizzinoso

2
Sto votando per chiudere questa domanda come fuori tema perché le sfide subdole non sono più in argomento su questo sito. meta.codegolf.stackexchange.com/a/8326/20469
cat

Risposte:


29

C

CleverSort

CleverSort è un algoritmo di ordinamento delle stringhe a due fasi all'avanguardia (ovvero sovraingegnerizzato e non ottimale).

Nel passaggio 1, inizia preordinando le righe di input utilizzando l' ordinamento radix e i primi due byte di ciascuna riga. L'ordinamento Radix non è comparativo e funziona molto bene con le stringhe.

Nel passaggio 2 utilizza l'ordinamento per inserzione nell'elenco di stringhe preordinato. Poiché l'elenco è quasi ordinato dopo il passaggio 1, l'ordinamento per inserzione è abbastanza efficiente per questa attività.

Codice

#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]);
}

piattaforme

Sappiamo tutti che le macchine big-endian sono molto più efficienti delle loro controparti little-endian. Per il benchmarking, compileremo CleverSort con le ottimizzazioni attivate e creeremo casualmente un enorme elenco (poco più di 100.000 stringhe) di linee a 4 byte:

$ 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

Punto di riferimento big-endian

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

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

Non troppo malandato.

Bechmark little-endian

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

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

Boo, piccolo Endian! Boo!

Descrizione

L'ordinamento per inserzione è davvero piuttosto efficiente per gli elenchi quasi ordinati, ma è terribilmente inefficiente per quelli ordinati casualmente.

La parte subdola di CleverSort è la macro FIRSTSHORT :

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

Su macchine big-endian, ordinare una stringa di due numeri interi a 8 bit lessicograficamente o convertirli in numeri interi a 16 bit e ordinarli successivamente produce gli stessi risultati.

Naturalmente, questo è possibile anche su macchine little-endian, ma la macro avrebbe dovuto essere

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

che funziona come previsto su tutte le piattaforme.

Il "benchmark big-endian" sopra è in realtà il risultato dell'utilizzo della macro corretta.

Con la macro sbagliata e una macchina little-endian, l'elenco è preordinato dal secondo carattere di ogni riga, risultando in un ordine casuale dal punto di vista lessicografico. L'ordinamento per inserzione si comporta in modo molto scarso in questo caso.


16

Python 2 vs. Python 3

Ovviamente, Python 3 è più veloce di molti ordini di grandezza rispetto a Python 2. Prendiamo ad esempio questa implementazione dell'algoritmo Shellsort :

Codice

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))

segno di riferimento

Preparare un input di prova. Questo è tratto dalla risposta di Dennis ma con meno parole: Python 2 è così lento ...

$ 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

Dov'è il codice subdolo?

Presumo che alcuni lettori potrebbero voler dare la caccia allo stesso imbroglione, quindi nascondo la risposta con un tag spoiler.

Il trucco è la divisione intera nel calcolo del max_sequence_element. In Python 2 1/2restituisce zero e quindi l'espressione è sempre zero. Tuttavia, il comportamento dell'operatore è cambiato in divisione in virgola mobile in Python 3. Poiché questa variabile controlla la lunghezza della sequenza gap, che è un parametro critico di Shellsort, Python 2 finisce per usare una sequenza che contiene solo il numero uno mentre Python 3 utilizza la sequenza corretta. Ciò si traduce in un tempo di esecuzione quadratico per Python 2.

Bonus 1:

È possibile correggere il codice semplicemente aggiungendo un punto dopo il 1o 2nel calcolo.

Bonus 2:

Almeno sulla mia macchina Python 2 è un po 'più veloce di Python 3 quando si esegue il codice fisso ...


Ben fatto! Nitpix time: flagsembra di sola scrittura, non puoi rimuoverlo? Inoltre, rsembra superfluo se lo fai if lst[i+h] < lst[i]: .... D'altra parte, se si mantiene rperché lo scambio? Non potresti farlo lst[i+h] = lst[i]? Tutto ciò è una distrazione intenzionale?
Jonas Kölker,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.