Concurso de código oculto: ordenamiento no tan rápido [cerrado]

28

La tarea

Escriba un programa, en el idioma que elija, que lea líneas de entrada desde la entrada estándar hasta EOF, y luego las escriba en la salida estándar en orden ASCIIbetical, similar al sortprograma de línea de comandos. Un ejemplo corto y sin pretensiones en Python es:

import sys

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

La parte oculta

Al igual que The OS War , su objetivo es demostrar que su plataforma favorita es "mejor", haciendo que su programa se ejecute deliberadamente mucho más lentamente en una plataforma competidora. En aras de este concurso, una "plataforma" consiste en cualquier combinación de:

  • Procesador
    • Arquitectura (x86, Alpha, ARM, MIPS, PowerPC, etc.)
    • Bitness (64 bits frente a 32 bits frente a 16 bits)
    • Big-versus little-endian
  • Sistema operativo
    • Windows vs. Linux vs. Mac OS, etc.
    • Diferentes versiones del mismo sistema operativo.
  • Implementación del lenguaje
    • Diferentes proveedores de compiladores / intérpretes (por ejemplo, MSVC ++ vs. GCC)
    • Diferentes versiones del mismo compilador / intérprete

Aunque podría cumplir los requisitos escribiendo código como:

#ifndef _WIN32
    Sleep(1000);
#endif

Tal respuesta no debe ser votada. El objetivo es ser sutil. Idealmente, su código debería verse como si no dependiera de la plataforma en absoluto. Si no tiene ningún #ifdefdeclaraciones (o condiciones sobre la base de os.nameo System.Environment.OSVersiono lo que sea), deben tener una justificación plausible (basado en una mentira, por supuesto).

Incluir en tu respuesta

  • El código
  • Sus plataformas "favoritas" y "desfavorables".
  • Una entrada con la que probar su programa.
  • El tiempo de ejecución en cada plataforma, para la misma entrada.
  • Una descripción de por qué el programa se ejecuta tan lentamente en la plataforma desfavorable.
dan04
fuente
44
Esto es más difícil de lo que pensaba. Las únicas respuestas que se me ocurren son muy largas y un poco obvias, o muy cortas y extremadamente obvias :-(
aprensivo ossifrage
2
Estoy votando para cerrar esta pregunta como fuera de tema porque los desafíos poco claros ya no están en el tema en este sitio. meta.codegolf.stackexchange.com/a/8326/20469
gato

Respuestas:

29

do

CleverSort

CleverSort es un algoritmo de clasificación de cadenas de dos pasos de última generación (es decir, sobredimensionado y subóptimo).

En el paso 1, comienza ordenando previamente las líneas de entrada usando la clasificación de radix y los primeros dos bytes de cada línea. La clasificación de radix no es comparativa y funciona muy bien para cadenas.

En el paso 2, utiliza el orden de inserción en la lista de cadenas ordenada previamente. Dado que la lista está casi ordenada después del paso 1, la ordenación por inserción es bastante eficiente para esta tarea.

Código

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

Plataformas

Todos sabemos que las máquinas big endian son mucho más eficientes que sus contrapartes little endian. Para la evaluación comparativa, compilaremos CleverSort con las optimizaciones activadas y crearemos aleatoriamente una lista enorme (un poco más de 100,000 cadenas) de líneas de 4 bytes:

$ 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 de referencia big endian

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

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

No está nada mal.

Bechmark little endian

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

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

Boo, pequeño Endian! ¡Abucheo!

Descripción

La ordenación por inserción es realmente bastante eficiente para listas casi ordenadas, pero es terriblemente ineficiente para las ordenadas aleatoriamente.

La parte oculta de CleverSort es la macro FIRSTSHORT :

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

En máquinas big-endian, ordenar una cadena de dos enteros de 8 bits lexicográficamente o convertirlos en enteros de 16 bits y ordenarlos luego produce los mismos resultados.

Naturalmente, esto también es posible en máquinas little endian, pero la macro debería haber sido

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

que funciona como se esperaba en todas las plataformas.

El "punto de referencia big-endian" anterior es en realidad el resultado del uso de la macro adecuada.

Con la macro incorrecta y una máquina little-endian, la lista se ordena previamente por el segundo carácter de cada línea, lo que resulta en un orden aleatorio desde el punto de vista lexicográfico. La ordenación por inserción se comporta muy mal en este caso.

Dennis
fuente
16

Python 2 contra Python 3

Obviamente, Python 3 es varios órdenes de magnitud más rápido que Python 2. Tomemos como ejemplo esta implementación del algoritmo Shellsort :

Código

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

Punto de referencia

Prepare una entrada de prueba. Esto está tomado de la respuesta de Dennis pero con menos palabras: Python 2 es muuuy 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

¿Dónde está el código oculto?

Supongo que algunos lectores pueden querer cazar al embaucador ellos mismos, así que oculto la respuesta con una etiqueta de spoiler.

El truco es la división entera en el cálculo de max_sequence_element. En Python 2 se 1/2evalúa a cero y, por lo tanto, la expresión siempre es cero. Sin embargo, el comportamiento del operador cambió a la división de coma flotante en Python 3. Como esta variable controla la longitud de la secuencia de separación, que es un parámetro crítico de Shellsort, Python 2 termina usando una secuencia que contiene solo el número uno, mientras que Python 3 usa la secuencia correcta. Esto da como resultado un tiempo de ejecución cuadrático para Python 2.

Bonus 1:

Puede corregir el código simplemente agregando un punto después de 1o 2en el cálculo.

Bonus 2:

Al menos en mi máquina, Python 2 es un poco más rápido que Python 3 cuando ejecuta el código fijo ...

René
fuente
¡Bien jugado! Tiempo Nitpix: flagparece de solo escritura, ¿no podrías eliminarlo? Además, rparece superfluo si lo haces if lst[i+h] < lst[i]: .... Por otro lado, si sigues r¿por qué hacer el intercambio? ¿No podrías simplemente hacerlo lst[i+h] = lst[i]? ¿Es todo esto una distracción intencional?
Jonas Kölker