Underhanded-Code-Wettbewerb: Nicht ganz so schnelle Sortierung [abgeschlossen]

28

Die Aufgabe

Schreiben Sie ein Programm in der Sprache Ihrer Wahl, das die Eingabezeilen von der Standardeingabe bis zum EOF liest und sie dann in ASCIIbetical-Reihenfolge in die Standardausgabe schreibt, ähnlich dem sortBefehlszeilenprogramm. Ein kurzes, nicht zu unterschätzendes Beispiel in Python ist:

import sys

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

Der hinterhältige Teil

Ähnlich wie bei The OS War ist es Ihr Ziel zu beweisen, dass Ihre bevorzugte Plattform "besser" ist, indem Sie Ihr Programm auf einer konkurrierenden Plattform absichtlich viel langsamer ausführen lassen. Für diesen Wettbewerb besteht eine "Plattform" aus einer beliebigen Kombination von:

  • Prozessor
    • Architektur (x86, Alpha, ARM, MIPS, PowerPC usw.)
    • Bitness (64-Bit vs. 32-Bit vs. 16-Bit)
    • Big versus Little Endian
  • Betriebssystem
    • Windows vs. Linux vs. Mac OS usw.
    • Verschiedene Versionen desselben Betriebssystems
  • Sprachimplementierung
    • Verschiedene Compiler / Interpreter-Anbieter (zB MSVC ++ vs. GCC)
    • Verschiedene Versionen desselben Compilers / Interpreters

Obwohl Sie die Anforderungen erfüllen könnten, indem Sie Code schreiben wie:

#ifndef _WIN32
    Sleep(1000);
#endif

Eine solche Antwort sollte nicht befürwortet werden. Das Ziel ist es, subtil zu sein. Im Idealfall sollte Ihr Code so aussehen, als wäre er überhaupt nicht plattformabhängig. Wenn Sie noch keine haben #ifdefAussagen (oder Bedingungen auf der Basis os.nameoder System.Environment.OSVersionoder was auch immer), sollten sie eine plausible Rechtfertigung haben (basierend auf einer Lüge, natürlich).

Nehmen Sie in Ihre Antwort auf

  • Der Code
  • Ihre "Lieblings" und "ungünstigen" Plattformen.
  • Eine Eingabe, mit der Sie Ihr Programm testen können.
  • Die Laufzeit auf jeder Plattform für dieselbe Eingabe.
  • Eine Beschreibung, warum das Programm auf der ungünstigen Plattform so langsam ausgeführt wird.
dan04
quelle
4
Das ist schwieriger als ich dachte. Die einzigen Antworten, die ich finden kann, sind entweder sehr lang und ein wenig offensichtlich oder sehr kurz und extrem offensichtlich :-(
zimperlicher
2
Ich stimme dafür, diese Frage als "Off-Topic" zu schließen, da hinterhältige Herausforderungen auf dieser Site nicht mehr zum Thema gehören. meta.codegolf.stackexchange.com/a/8326/20469
cat

Antworten:

29

C

CleverSort

CleverSort ist ein hochmoderner (dh überentwickelter und suboptimaler) zweistufiger Algorithmus zur Sortierung von Zeichenfolgen.

In Schritt 1 werden die Eingabezeilen mit der Grundsortierung und den ersten beiden Bytes jeder Zeile vorsortiert . Die Radix-Sortierung ist nicht vergleichbar und eignet sich sehr gut für Streicher.

In Schritt 2 wird die Einfügesortierung für die vorsortierte Liste der Zeichenfolgen verwendet. Da die Liste nach Schritt 1 fast sortiert ist, ist die Einfügesortierung für diese Aufgabe recht effizient.

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

Plattformen

Wir alle wissen, dass Big-Endian-Maschinen viel effizienter sind als ihre Little-Endian-Gegenstücke. Für das Benchmarking kompilieren wir CleverSort mit aktivierten Optimierungen und erstellen zufällig eine riesige Liste (etwas mehr als 100.000 Zeichenfolgen) von 4-Byte-Zeilen:

$ 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

Big-Endian-Benchmark

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

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

Nicht zu schäbig.

Little-Endian Bechmark

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

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

Boo, kleiner Endian! Boo!

Beschreibung

Das Sortieren von Einfügungen ist für fast sortierte Listen sehr effizient, für zufällig sortierte Listen jedoch schrecklich ineffizient.

CleverSorts hinterhältiger Teil ist das FIRSTSHORT- Makro:

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

Auf Big-Endian-Computern führt das lexikografische Ordnen einer Zeichenfolge aus zwei 8-Bit-Ganzzahlen oder das Konvertieren in 16-Bit-Ganzzahlen und das anschließende Ordnen derselben zu denselben Ergebnissen.

Natürlich ist dies auch auf Little-Endian-Maschinen möglich, aber das Makro sollte es gewesen sein

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

was auf allen Plattformen wie erwartet funktioniert.

Der obige "Big-Endian-Benchmark" ist eigentlich das Ergebnis der Verwendung des richtigen Makros.

Mit dem falschen Makro und einer Little-Endian-Maschine wird die Liste nach dem zweiten Zeichen jeder Zeile vorsortiert , was aus lexikografischer Sicht zu einer zufälligen Reihenfolge führt. Die Einfügesortierung verhält sich in diesem Fall sehr schlecht.

Dennis
quelle
16

Python 2 gegen Python 3

Offensichtlich ist Python 3 einige Größenordnungen schneller als Python 2. Nehmen wir als Beispiel die folgende Implementierung des Shellsort- Algorithmus:

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

Benchmark

Bereiten Sie einen Testeingang vor. Dies ist Dennis Antwort entnommen, aber mit weniger Worten - Python 2 ist sooo langsam ...

$ 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

Wo ist der hinterhältige Code?

Ich nehme an, dass einige Leser den Trickster selbst jagen möchten, also verstecke ich die Antwort mit einem Spoiler-Tag.

Der Trick ist die ganzzahlige Division bei der Berechnung der max_sequence_element. In Python 2 wird 1/2null ausgewertet, und daher ist der Ausdruck immer null. Das Verhalten des Operators wurde jedoch in Python 3 in Gleitkommadivision geändert. Da diese Variable die Länge der Lückensequenz steuert, die ein kritischer Parameter von Shellsort ist, verwendet Python 2 letztendlich eine Sequenz, die nur die Nummer eins enthält, während Python aktiv ist 3 verwendet die richtige Reihenfolge. Dies ergibt eine quadratische Laufzeit für Python 2.

Bonus 1:

Sie können den Code korrigieren, indem Sie einfach einen Punkt nach 1oder 2in die Berechnung einfügen .

Bonus 2:

Zumindest auf meinem Computer ist Python 2 etwas schneller als Python 3, wenn der feste Code ausgeführt wird ...

René
quelle
Gut gespielt! Nitpix-Zeit: flagSieht schreibgeschützt aus. Können Sie sie nicht entfernen? Auch rscheint überflüssig , wenn Sie das tun if lst[i+h] < lst[i]: .... Auf der anderen Seite, wenn Sie behalten, rwarum der Swap? Könntest du nicht einfach tun lst[i+h] = lst[i]? Ist das alles eine absichtliche Ablenkung?
Jonas Kölker