Respondiendo a otra pregunta de Stack Overflow ( esta ), me topé con un subproblema interesante. ¿Cuál es la forma más rápida de ordenar una matriz de 6 enteros?
Como la pregunta es de muy bajo nivel:
- no podemos asumir que las bibliotecas están disponibles (y la llamada en sí tiene su costo), solo C simple
- Para evitar el vaciado de la tubería de instrucciones (que tiene un costo muy alto), probablemente deberíamos minimizar las ramas, los saltos y cualquier otro tipo de interrupción del flujo de control (como los ocultos detrás de los puntos de secuencia en
&&
o||
). - el espacio es limitado y la minimización de los registros y el uso de la memoria es un problema, idealmente en el lugar es probablemente el mejor.
Realmente esta pregunta es un tipo de Golf donde el objetivo no es minimizar la longitud de la fuente sino el tiempo de ejecución. Lo llamo código 'Zening' como se usa en el título del libro Zen of Code optimization de Michael Abrash y sus secuelas .
En cuanto a por qué es interesante, hay varias capas:
- el ejemplo es simple y fácil de entender y medir, no involucra mucha habilidad C
- muestra los efectos de elección de un buen algoritmo para el problema, pero también los efectos del compilador y el hardware subyacente.
Aquí está mi implementación de referencia (ingenua, no optimizada) y mi conjunto de pruebas.
#include <stdio.h>
static __inline__ int sort6(int * d){
char j, i, imin;
int tmp;
for (j = 0 ; j < 5 ; j++){
imin = j;
for (i = j + 1; i < 6 ; i++){
if (d[i] < d[imin]){
imin = i;
}
}
tmp = d[j];
d[j] = d[imin];
d[imin] = tmp;
}
}
static __inline__ unsigned long long rdtsc(void)
{
unsigned long long int x;
__asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
return x;
}
int main(int argc, char ** argv){
int i;
int d[6][5] = {
{1, 2, 3, 4, 5, 6},
{6, 5, 4, 3, 2, 1},
{100, 2, 300, 4, 500, 6},
{100, 2, 3, 4, 500, 6},
{1, 200, 3, 4, 5, 600},
{1, 1, 2, 1, 2, 1}
};
unsigned long long cycles = rdtsc();
for (i = 0; i < 6 ; i++){
sort6(d[i]);
/*
* printf("d%d : %d %d %d %d %d %d\n", i,
* d[i][0], d[i][6], d[i][7],
* d[i][8], d[i][9], d[i][10]);
*/
}
cycles = rdtsc() - cycles;
printf("Time is %d\n", (unsigned)cycles);
}
Resultados crudos
A medida que aumenta el número de variantes, las reuní todas en un conjunto de pruebas que se puede encontrar aquí . Las pruebas reales utilizadas son un poco menos ingenuas que las mostradas anteriormente, gracias a Kevin Stock. Puede compilarlo y ejecutarlo en su propio entorno. Estoy bastante interesado por el comportamiento en diferentes compilaciones / arquitectura de destino. (OK chicos, pónganlo en respuestas, haré +1 en cada contribuyente de un nuevo conjunto de resultados).
Le di la respuesta a Daniel Stutzbach (para jugar al golf) hace un año, ya que estaba en la fuente de la solución más rápida en ese momento (redes de clasificación).
Linux 64 bits, gcc 4.6.1 64 bits, Intel Core 2 Duo E8400, -O2
- Llamada directa a la función de biblioteca qsort: 689.38
- Implementación ingenua (tipo de inserción): 285.70
- Tipo de inserción (Daniel Stutzbach): 142.12
- Tipo de inserción desenrollado: 125.47
- Orden de rango: 102.26
- Orden de clasificación con registros: 58.03
- Redes de clasificación (Daniel Stutzbach): 111.68
- Redes de clasificación (Paul R): 66.36
- Clasificación de redes 12 con intercambio rápido: 58.86
- Ordenar redes 12 Reordenado Swap: 53.74
- Sorting Networks 12 reordenado Simple Swap: 31.54
- Red de clasificación reordenada con intercambio rápido: 31.54
- Red de clasificación reordenada con intercambio rápido V2: 33.63
- Clasificación de burbujas en línea (Paolo Bonzini): 48.85
- Tipo de inserción desenrollada (Paolo Bonzini): 75.30
Linux 64 bits, gcc 4.6.1 64 bits, Intel Core 2 Duo E8400, -O1
- Llamada directa a la función de biblioteca qsort: 705.93
- Implementación ingenua (tipo de inserción): 135,60
- Tipo de inserción (Daniel Stutzbach): 142.11
- Tipo de inserción desenrollado: 126.75
- Orden de rango: 46.42
- Orden de clasificación con registros: 43.58
- Redes de clasificación (Daniel Stutzbach): 115.57
- Redes de clasificación (Paul R): 64.44
- Clasificación de redes 12 con intercambio rápido: 61,98
- Ordenar redes 12 Reordenado Swap: 54.67
- Sorting Networks 12 reordenado Simple Swap: 31.54
- Red de clasificación reordenada con intercambio rápido: 31.24
- Red de clasificación reordenada con intercambio rápido V2: 33.07
- Clasificación de burbujas en línea (Paolo Bonzini): 45,79
- Tipo de inserción desenrollada (Paolo Bonzini): 80.15
Incluí los resultados de -O1 y -O2 porque, sorprendentemente, para varios programas, O2 es menos eficiente que O1. Me pregunto qué optimización específica tiene este efecto.
Comentarios sobre soluciones propuestas
Tipo de inserción (Daniel Stutzbach)
Como se esperaba, minimizar las ramas es una buena idea.
Redes de clasificación (Daniel Stutzbach)
Mejor que el tipo de inserción. Me preguntaba si el efecto principal no se obtenía al evitar el bucle externo. Lo probé mediante un tipo de inserción desenrollado para verificar y, de hecho, obtenemos aproximadamente las mismas cifras (el código está aquí ).
Redes de clasificación (Paul R)
Lo mejor por mucho. El código real que solía probar está aquí . Todavía no sé por qué es casi dos veces más rápido que la otra implementación de red de clasificación. Paso de parámetros? Max rápido?
Clasificación de redes 12 SWAP con intercambio rápido
Como sugirió Daniel Stutzbach, combiné su red de clasificación de 12 intercambios con un intercambio rápido sin ramificaciones (el código está aquí ). De hecho, es más rápido, el mejor hasta ahora con un pequeño margen (aproximadamente 5%) como se podría esperar con 1 intercambio menos.
También es interesante notar que el intercambio sin ramas parece ser mucho (4 veces) menos eficiente que el simple que usa if en la arquitectura PPC.
Llamar a la biblioteca qsort
Para dar otro punto de referencia, también intenté, como se sugiere, llamar a la biblioteca qsort (el código está aquí ). Como se esperaba, es mucho más lento: de 10 a 30 veces más lento ... como se hizo evidente con el nuevo conjunto de pruebas, el problema principal parece ser la carga inicial de la biblioteca después de la primera llamada, y no se compara tan mal con otros versión. Es solo entre 3 y 20 veces más lento en mi Linux. En algunas arquitecturas utilizadas para pruebas por otros, parece incluso más rápido (realmente estoy sorprendido por eso, ya que la biblioteca qsort usa una API más compleja).
Orden de rango
Rex Kerr propuso otro método completamente diferente: para cada elemento de la matriz, calcule directamente su posición final. Esto es eficiente porque el orden de rango de cómputo no necesita ramificación. El inconveniente de este método es que toma tres veces la cantidad de memoria de la matriz (una copia de la matriz y las variables para almacenar las órdenes de clasificación). Los resultados de rendimiento son muy sorprendentes (e interesantes). En mi arquitectura de referencia con sistema operativo de 32 bits e Intel Core2 Quad E8300, el recuento de ciclos fue ligeramente inferior a 1000 (como ordenar redes con intercambio de ramificación). Pero cuando se compiló y ejecutó en mi caja de 64 bits (Intel Core2 Duo) funcionó mucho mejor: se convirtió en el más rápido hasta ahora. Finalmente descubrí la verdadera razón. Mi caja de 32 bits usa gcc 4.4.1 y mi caja de 64 bits gcc 4.4.
actualización :
Como las cifras publicadas arriba muestran que este efecto aún se mejoró con versiones posteriores de gcc y el orden de clasificación se volvió consistentemente dos veces más rápido que cualquier otra alternativa.
Clasificación de redes 12 con intercambio reordenado
La sorprendente eficacia de la propuesta de Rex Kerr con gcc 4.4.3 me hizo preguntarme: ¿cómo podría un programa con 3 veces más uso de memoria ser más rápido que las redes de clasificación sin ramificaciones? Mi hipótesis era que tenía menos dependencias del tipo lectura después de escritura, lo que permite un mejor uso del planificador de instrucciones superescalar del x86. Eso me dio una idea: reordenar los intercambios para minimizar las dependencias de lectura después de escritura. En pocas palabras: cuando lo hace SWAP(1, 2); SWAP(0, 2);
, debe esperar a que termine el primer intercambio antes de realizar el segundo porque ambos acceden a una celda de memoria común. Cuando lo hace, SWAP(1, 2); SWAP(4, 5);
el procesador puede ejecutar ambos en paralelo. Lo probé y funciona como se esperaba, las redes de clasificación se ejecutan aproximadamente un 10% más rápido.
Clasificación de redes 12 con intercambio simple
Un año después de la publicación original, Steinar H. Gunderson sugirió que no deberíamos intentar burlar al compilador y mantener el código de intercambio simple. De hecho, es una buena idea ya que el código resultante es aproximadamente un 40% más rápido. También propuso un intercambio optimizado a mano utilizando el código de ensamblaje en línea x86 que aún puede ahorrar algunos ciclos más. Lo más sorprendente (dice mucho sobre la psicología del programador) es que hace un año ninguno de los usuarios intentó esa versión de intercambio. El código que solía probar está aquí . Otros sugirieron otras formas de escribir un intercambio rápido en C, pero produce el mismo rendimiento que el simple con un compilador decente.
El "mejor" código es ahora el siguiente:
static inline void sort6_sorting_network_simple_swap(int * d){
#define min(x, y) (x<y?x:y)
#define max(x, y) (x<y?y:x)
#define SWAP(x,y) { const int a = min(d[x], d[y]); \
const int b = max(d[x], d[y]); \
d[x] = a; d[y] = b; }
SWAP(1, 2);
SWAP(4, 5);
SWAP(0, 2);
SWAP(3, 5);
SWAP(0, 1);
SWAP(3, 4);
SWAP(1, 4);
SWAP(0, 3);
SWAP(2, 5);
SWAP(1, 3);
SWAP(2, 4);
SWAP(2, 3);
#undef SWAP
#undef min
#undef max
}
Si creemos que nuestro conjunto de pruebas (y, sí, es bastante pobre, su simple beneficio es ser corto, simple y fácil de entender lo que estamos midiendo), el número promedio de ciclos del código resultante para un tipo es inferior a 40 ciclos ( Se ejecutan 6 pruebas). Eso coloca cada intercambio en un promedio de 4 ciclos. A eso lo llamo asombrosamente rápido. ¿Alguna otra mejora posible?
x-y
yx+y
no causará desbordamiento o desbordamiento?__asm__ volatile (".byte 0x0f, 0x31; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");
debe a que rdtsc pone la respuesta en EDX: EAX mientras que GCC lo espera en un único registro de 64 bits. Puede ver el error compilando en -O3. También vea a continuación mi comentario a Paul R sobre un SWAP más rápido.CMP EAX, EBX; SBB EAX, EAX
pondrá 0 o 0xFFFFFFFFEAX
dependiendo de siEAX
es mayor o menor queEBX
, respectivamente.SBB
es "restar con préstamo", la contrapartida deADC
("agregar con acarreo"); el bit de estado al que se refiere es el bit de acarreo. Por otra parte, recuerdo esoADC
ySBB
tuve una latencia y un rendimiento terribles en el Pentium 4 vs.ADD
ySUB
, y todavía eran dos veces más lentos en las CPU Core. Desde el 80386 también hay instrucciones deSETcc
almacenamientoCMOVcc
condicional y movimiento condicional, pero también son lentas.Respuestas:
Para cualquier optimización, siempre es mejor probar, probar, probar. Intentaría al menos ordenar las redes y la inserción. Si estuviera apostando, apostaría mi dinero en el tipo de inserción basado en la experiencia pasada.
¿Sabes algo sobre los datos de entrada? Algunos algoritmos funcionarán mejor con ciertos tipos de datos. Por ejemplo, la ordenación por inserción funciona mejor en datos ordenados o casi ordenados, por lo que será la mejor opción si hay una probabilidad superior a la media de datos casi ordenados.
El algoritmo que publicó es similar a un tipo de inserción, pero parece que ha minimizado el número de intercambios a costa de más comparaciones. Sin embargo, las comparaciones son mucho más caras que los intercambios, ya que las ramas pueden hacer que la tubería de instrucciones se detenga.
Aquí hay una implementación de clasificación de inserción:
Así es como construiría una red de clasificación. Primero, use este sitio para generar un conjunto mínimo de macros SWAP para una red de la longitud adecuada. Terminar eso en una función me da:
fuente
n < SMALL_CONSTANT
.Aquí hay una implementación usando redes de clasificación :
Realmente necesita implementaciones
min
y ramificaciones muy eficientesmax
para esto, ya que eso es efectivamente a lo que se reduce este código: una secuenciamin
ymax
operaciones (13 de cada una, en total). Lo dejo como ejercicio para el lector.Tenga en cuenta que esta implementación se presta fácilmente a la vectorización (por ejemplo, SIMD - la mayoría de las ISA SIMD tienen instrucciones mínimas / máximas de vectores) y también a implementaciones de GPU (por ejemplo, CUDA - al no tener ramificaciones, no hay problemas con la divergencia de deformación, etc.).
Ver también: Implementación rápida del algoritmo para ordenar una lista muy pequeña
fuente
Sort3
sería más rápido (en la mayoría de las arquitecturas, de todos modos) si notaras que(a+b+c)-(min+max)
es el número central.#define SWAP(x,y) { int dx = d[x], dy = d[y], tmp; tmp = d[x] = dx < dy ? dx : dy; d[y] ^= dx ^ tmp; }
. Aquí no estoy usando?: Para d [y] porque da un rendimiento ligeramente peor, pero está casi en el ruido.Dado que estos son enteros y las comparaciones son rápidas, ¿por qué no calcular el orden de clasificación de cada uno directamente?
fuente
0+1+2+3+4+5=15
Dado que falta uno de ellos, 15 menos la suma del resto produce uno perdidoParece que llegué a la fiesta un año tarde, pero aquí vamos ...
Al observar el ensamblaje generado por gcc 4.5.2, observé que se realizan cargas y almacenes para cada intercambio, lo que realmente no es necesario. Sería mejor cargar los 6 valores en los registros, ordenarlos y almacenarlos nuevamente en la memoria. Ordené que las cargas en las tiendas estuvieran lo más cerca posible de allí, los registros se necesitan primero y se usan por última vez. También utilicé la macro SWAP de Steinar H. Gunderson. Actualización: Cambié a la macro SWAP de Paolo Bonzini, que gcc se convierte en algo similar a Gunderson, pero gcc puede ordenar mejor las instrucciones, ya que no se dan como ensamblaje explícito.
Utilicé el mismo orden de intercambio que la red de intercambio reordenada dada como el mejor rendimiento, aunque puede haber un mejor orden. Si encuentro algo más de tiempo, generaré y probaré un montón de permutaciones.
Cambié el código de prueba para considerar más de 4000 arreglos y mostrar el número promedio de ciclos necesarios para ordenar cada uno. En un i5-650 obtengo ~ 34.1 ciclos / clasificación (usando -O3), en comparación con la red de clasificación reordenada original obteniendo ~ 65.3 ciclos / clasificación (usando -O1, late -O2 y -O3).
Cambié, modifiqué el conjunto de pruebas para informar también los relojes por tipo y ejecuté más pruebas (la función cmp también se actualizó para manejar el desbordamiento de enteros), aquí están los resultados en algunas arquitecturas diferentes. Intenté probar en una CPU AMD pero rdtsc no es confiable en el X6 1100T que tengo disponible.
fuente
-O3
optimización no sea contraproducente.#define SWAP(x,y) { int oldx = x; x = x < y ? x : y; y ^= oldx ^ x; }
.Me encontré con esta pregunta de Google hace unos días porque también tenía la necesidad de ordenar rápidamente una matriz de longitud fija de 6 enteros. Sin embargo, en mi caso, mis enteros son solo de 8 bits (en lugar de 32) y no tengo el requisito estricto de usar solo C. Pensé que compartiría mis hallazgos de todos modos, en caso de que puedan ser útiles para alguien ...
Implementé una variante de un tipo de red en el ensamblaje que usa SSE para vectorizar las operaciones de comparación e intercambio, en la medida de lo posible. Se necesitan seis "pases" para ordenar completamente la matriz. Utilicé un mecanismo novedoso para convertir directamente los resultados de PCMPGTB (comparación vectorizada) a parámetros aleatorios para PSHUFB (intercambio vectorizado), usando solo una instrucción PADDB (vectorized add) y en algunos casos también una instrucción PAND (bit a bit Y).
Este enfoque también tuvo el efecto secundario de producir una función verdaderamente sin ramas. No hay instrucciones de salto de ningún tipo.
Parece que esta implementación es aproximadamente un 38% más rápida que la implementación que actualmente está marcada como la opción más rápida en la pregunta ("Ordenar redes 12 con intercambio simple"). Modifiqué esa implementación para usar
char
elementos de matriz durante mis pruebas, para que la comparación sea justa.Debo señalar que este enfoque se puede aplicar a cualquier tamaño de matriz de hasta 16 elementos. Espero que la ventaja de velocidad relativa sobre las alternativas crezca para las matrices más grandes.
El código está escrito en MASM para procesadores x86_64 con SSSE3. La función utiliza la "nueva" convención de llamadas de Windows x64. Aquí está...
Puede compilar esto en un objeto ejecutable y vincularlo a su proyecto C. Para obtener instrucciones sobre cómo hacer esto en Visual Studio, puede leer este artículo . Puede usar el siguiente prototipo C para llamar a la función desde su código C:
fuente
pxor / pinsrd xmm4, mem, 0
, ¡solo úsalomovd
!El código de prueba es bastante malo; desborda la matriz inicial (¿no leen aquí las advertencias del compilador?), printf está imprimiendo los elementos incorrectos, usa .byte para rdtsc sin ninguna buena razón, solo hay una ejecución (!), no hay nada que verifique que el los resultados finales son realmente correctos (por lo que es muy fácil "optimizar" en algo sutilmente incorrecto), las pruebas incluidas son muy rudimentarias (¿no hay números negativos?) y no hay nada que impida que el compilador descarte toda la función como código muerto.
Dicho esto, también es bastante fácil mejorar la solución de red bitónica; simplemente cambie las cosas min / max / SWAP a
y sale aproximadamente un 65% más rápido para mí (Debian gcc 4.4.5 con -O2, amd64, Core i7).
fuente
Si bien me gusta la macro de intercambio proporcionada:
Veo una mejora (que un buen compilador podría hacer):
Tomamos nota de cómo funcionan min y max y extraemos la sub-expresión común explícitamente. Esto elimina las macros mín. Y máx. Por completo.
fuente
d[x]
lugar dex
(lo mismo paray
), yd[y] < d[x]
para la desigualdad aquí (sí, diferente del código min / max).Nunca optimice min / max sin benchmarking y mirando el ensamblaje generado por el compilador real. Si dejo que GCC optimice el mínimo con instrucciones de movimiento condicionales, obtengo un 33% de aceleración:
(280 vs. 420 ciclos en el código de prueba). Doing max with?: Es más o menos lo mismo, casi se pierde en el ruido, pero lo anterior es un poco más rápido. Este SWAP es más rápido con GCC y Clang.
Los compiladores también están haciendo un trabajo excepcional en la asignación de registros y el análisis de alias, moviendo efectivamente d [x] a las variables locales por adelantado y solo copiando de nuevo a la memoria al final. De hecho, lo hacen aún mejor que si trabajaras completamente con variables locales (como
d0 = d[0], d1 = d[1], d2 = d[2], d3 = d[3], d4 = d[4], d5 = d[5]
). Escribo esto porque está asumiendo una fuerte optimización y, sin embargo, está intentando burlar al compilador en min / max. :)Por cierto, probé Clang y GCC. Hacen la misma optimización, pero debido a las diferencias de programación, los dos tienen alguna variación en los resultados, no puedo decir cuál es más rápido o más lento. GCC es más rápido en las redes de clasificación, Clang en los tipos cuadráticos.
Solo para completar, también es posible el tipo de burbuja desenrollada y los tipos de inserción. Aquí está el tipo de burbuja:
y aquí está el tipo de inserción:
Este tipo de inserción es más rápido que el de Daniel Stutzbach, y es especialmente bueno en una GPU o una computadora con predicción porque ITER se puede hacer con solo 3 instrucciones (vs. 4 para SWAP). Por ejemplo, aquí está la
t = d[2]; ITER(1); ITER(0);
línea en el ensamblaje ARM:Para seis elementos, la clasificación de inserción es competitiva con la red de clasificación (12 swaps vs. 15 iteraciones equilibra 4 instrucciones / intercambio vs. 3 instrucciones / iteración); tipo de burbuja, por supuesto, es más lento. Pero no será cierto cuando aumente el tamaño, ya que la ordenación por inserción es O (n ^ 2) mientras que las redes de ordenación son O (n log n).
fuente
Porté el conjunto de pruebas a una máquina de arquitectura PPC que no puedo identificar (no tuve que tocar el código, solo aumente las iteraciones de la prueba, use 8 casos de prueba para evitar resultados contaminantes con modificaciones y reemplace el rdtsc específico x86):
Llamada directa a la función de biblioteca qsort : 101
Implementación ingenua (tipo de inserción) : 299
Tipo de inserción (Daniel Stutzbach) : 108
Tipo de inserción desenrollado : 51
Redes de clasificación (Daniel Stutzbach) : 26
Redes de clasificación (Paul R) : 85
Clasificación de redes 12 con intercambio rápido : 117
Ordenar redes 12 Reordenado Intercambio : 116
Orden de rango : 56
fuente
subfc r5,r4,r3; subfe r6,r6,r6; andc r6,r5,r6; add r4,r6,r4; subf r3,r6,r3
. r3 / r4 son entradas, r5 / r6 son registros de memoria virtual, en la salida r3 obtiene el mínimo y r4 obtiene el máximo. Debe ser decentemente programable a mano. Lo encontré con el superoptimizador GNU, comenzando con secuencias mín. Y máx. De 4 instrucciones y buscando manualmente dos que pudieran combinarse. Para entradas con signo, por supuesto, puede agregar 0x80000000 a todos los elementos al principio y restarlo nuevamente al final, y luego trabajar como si no estuvieran firmados.Un intercambio XOR puede ser útil en sus funciones de intercambio.
El if puede causar demasiada divergencia en su código, pero si tiene la garantía de que todos sus ints son únicos, esto podría ser útil.
fuente
x
yy
apunta a la misma ubicación.Estoy ansioso por probar esto y aprender de estos ejemplos, pero primero algunos tiempos de mi Powerbook G4 PPC de 1.5 GHz con 1 GB de RAM DDR. (Tomé prestado un temporizador similar a rdtsc para PPC de http://www.mcs.anl.gov/~kazutomo/rdtsc.html para los horarios). Ejecuté el programa varias veces y los resultados absolutos variaron, pero el la prueba más rápida fue "Insertion Sort (Daniel Stutzbach)", con "Insertion Sort Unrolled" en segundo lugar.
Aquí está el último conjunto de veces:
fuente
Aquí está mi contribución a este hilo: un shellsort optimizado de 1, 4 gap para un vector int (valp) de 6 miembros que contiene valores únicos.
En mi computadora portátil HP dv7-3010so con un Athlon M300 de doble núcleo a 2 Ghz (memoria DDR2) se ejecuta en 165 ciclos de reloj. Este es un promedio calculado a partir del tiempo de cada secuencia única (6! / 720 en total). Compilado a Win32 usando OpenWatcom 1.8. El bucle es esencialmente un tipo de inserción y tiene 16 instrucciones / 37 bytes de longitud.
No tengo un entorno de 64 bits para compilar.
fuente
Si el tipo de inserción es razonablemente competitivo aquí, recomendaría probar un shellsort. Me temo que 6 elementos probablemente sean demasiado pequeños para estar entre los mejores, pero puede valer la pena intentarlo.
Código de ejemplo, sin probar, sin depurar, etc. Desea ajustar la secuencia inc = 4 e inc - = 3 para encontrar la secuencia óptima (pruebe inc = 2, inc - = 1, por ejemplo).
No creo que esto gane, pero si alguien publica una pregunta sobre cómo ordenar 10 elementos, quién sabe ...
Según Wikipedia, esto puede incluso combinarse con redes de clasificación: Pratt, V (1979). Shellsort y redes de clasificación (disertaciones sobresalientes en ciencias de la computación). Guirnalda. ISBN 0-824-04406-1
fuente
Sé que llego muy tarde, pero estaba interesado en experimentar con algunas soluciones diferentes. Primero, limpié esa pasta, la compilé y la puse en un repositorio. Mantuve algunas soluciones indeseables como callejones sin salida para que otros no lo intentaran. Entre esto estaba mi primera solución, que intentaba garantizar que x1> x2 se calculara una vez. Después de la optimización, no es más rápido que las otras versiones simples.
Agregué una versión en bucle del orden de clasificación, ya que mi propia aplicación de este estudio es para ordenar de 2 a 8 elementos, por lo que, dado que hay un número variable de argumentos, es necesario un bucle. Esta es también la razón por la que ignoré las soluciones de red de clasificación.
El código de prueba no probó que los duplicados se manejaran correctamente, por lo que si bien las soluciones existentes eran correctas, agregué un caso especial al código de prueba para garantizar que los duplicados se manejaran correctamente.
Luego, escribí un tipo de inserción que está completamente en los registros AVX. En mi máquina es un 25% más rápido que los otros tipos de inserción, pero un 100% más lento que el orden de clasificación. Lo hice solo para experimentar y no esperaba que fuera mejor debido a la ramificación en el tipo de inserción.
Luego, escribí un orden de clasificación usando AVX. Esto coincide con la velocidad de las otras soluciones de orden de rango, pero no es más rápido. El problema aquí es que solo puedo calcular los índices con AVX, y luego tengo que hacer una tabla de índices. Esto se debe a que el cálculo se basa en el destino y no en la fuente. Consulte Conversión de índices basados en origen a índices basados en destino
El repositorio se puede encontrar aquí: https://github.com/eyepatchParrot/sort6/
fuente
vmovmskps
en vectores enteros (con un reparto para mantener felices a los intrínsecos), evitando la necesidad de desplazar a la derecha el resultado de bitscan (ffs
).cmpgt
resultado restándolo , en lugar de enmascararlo conset1(1)
. por ejemplo,index = _mm256_sub_epi32(index, gt)
lo haceindex -= -1 or 0;
eq = _mm256_insert_epi32(eq, 0, I)
no es una manera eficiente de poner a cero un elemento si se compila tal como está escrito (especialmente para elementos fuera del 4 bajo, porquevpinsrd
solo está disponible con un destino XMM; los índices superiores a 3 tienen que ser emulados). En cambio,_mm256_blend_epi32
(vpblendd
) con un vector cero.vpblendd
es una instrucción single-uop que se ejecuta en cualquier puerto, frente a una combinación aleatoria que necesita el puerto 5 en las CPU de Intel. ( agner.org/optimize ).rot
vectores con diferentes barajaduras de la misma fuente, o al menos ejecutar 2 cadenas dep en paralelo que utilice alternativamente, en lugar de una sola cadena dep a través de una barajadura de cruce de carril (latencia de 3 ciclos). Eso aumentará ILP dentro de un solo tipo. 2 dep chain limita el número de constantes vectoriales a un número razonable, solo 2: 1 para una rotación y uno para 2 pasos de rotación combinados.Esta pregunta se está volviendo bastante antigua, pero en realidad tuve que resolver el mismo problema en estos días: agoritmos rápidos para ordenar pequeños arreglos. Pensé que sería una buena idea compartir mis conocimientos. Si bien comencé a usar redes de clasificación, finalmente logré encontrar otros algoritmos para los cuales el número total de comparaciones realizadas para clasificar cada permutación de 6 valores fue menor que con las redes de clasificación, y menor que con la clasificación de inserción. No conté el número de permutas; Esperaría que sea más o menos equivalente (tal vez un poco más alto a veces).
El algoritmo
sort6
usa el algoritmosort4
que usa el algoritmosort3
. Aquí está la implementación en alguna forma ligera de C ++ (el original tiene muchas plantillas para que pueda funcionar con cualquier iterador de acceso aleatorio y cualquier función de comparación adecuada).Ordenar 3 valores
El siguiente algoritmo es un tipo de inserción desenrollado. Cuando se tienen que realizar dos swaps (6 asignaciones), en su lugar utiliza 4 asignaciones:
Parece un poco complejo porque el orden tiene más o menos una rama para cada permutación posible de la matriz, usando 2 ~ 3 comparaciones y como máximo 4 asignaciones para ordenar los tres valores.
Ordenar 4 valores
Éste llama
sort3
luego realiza una ordenación de inserción desenrollada con el último elemento de la matriz:Este algoritmo realiza de 3 a 6 comparaciones y como máximo 5 intercambios. Es fácil desenrollar un tipo de inserción, pero usaremos otro algoritmo para el último tipo ...
Ordenar 6 valores
Este usa una versión desenrollada de lo que llamé un tipo de inserción doble . El nombre no es tan bueno, pero es bastante descriptivo, así es como funciona:
Después del intercambio, el primer elemento siempre es más pequeño que el último, lo que significa que, al insertarlos en la secuencia ordenada, no habrá más de N comparaciones para insertar los dos elementos en el peor de los casos: por ejemplo, si el el primer elemento ha sido insertado en la 3ra posición, luego el último no se puede insertar más abajo que la 4ta posición.
Mis pruebas en cada permutación de 6 valores muestran que este algoritmo siempre realiza entre 6 y 13 comparaciones. No calculé el número de intercambios realizados, pero no espero que sea superior a 11 en el peor de los casos.
Espero que esto ayude, incluso si esta pregunta ya no representa un problema real :)
EDITAR: después de ponerlo en el punto de referencia proporcionado, es claramente más lento que la mayoría de las alternativas interesantes. Tiende a funcionar un poco mejor que el tipo de inserción desenrollada, pero eso es todo. Básicamente, no es el mejor tipo para enteros, pero podría ser interesante para tipos con una operación de comparación costosa.
fuente
operator<
para la comparación. Además del recuento objetivo de comparaciones e intercambios, también cronometré adecuadamente mis algoritmos; esta solución fue la genérica más rápida, pero de hecho me perdí la de @ RexKerr. Voy a probarlo :)-O3
. Supongo que adoptaré otra estrategia para mi biblioteca de clasificación: proporcionar tres tipos de algoritmos para tener un número bajo de comparaciones, un número bajo de intercambios o potencialmente el mejor rendimiento. Al menos, lo que pase será transparente para el lector. Gracias por sus ideas :)Creo que su pregunta tiene dos partes.
No me preocuparía demasiado por vaciar las tuberías (suponiendo x86 actual): la predicción de rama ha recorrido un largo camino. Lo que me preocupa es asegurarme de que el código y los datos quepan en una línea de caché cada uno (tal vez dos para el código). Una vez allí, las latencias de recuperación son refrescantemente bajas, lo que compensará cualquier pérdida. También significa que su bucle interno tendrá unas diez instrucciones o menos, que es justo donde debería estar (hay dos bucles internos diferentes en mi algoritmo de clasificación, son 10 instrucciones / 22 bytes y 9/22 de largo respectivamente). Suponiendo que el código no contiene ningún divs, puede estar seguro de que será cegadoramente rápido.
fuente
Sé que esta es una vieja pregunta.
Pero acabo de escribir un tipo diferente de solución que quiero compartir.
Usando nada más que MIN MAX anidado,
No es rápido, ya que usa 114 de cada uno,
podría reducirlo a 75 bastante simplemente así -> pastebin
Pero entonces ya no es puramente min max.
Lo que podría funcionar es hacer min / max en múltiples enteros a la vez con AVX
Referencia de PMINSW
EDITAR:
solución de orden de clasificación inspirada en Rex Kerr, mucho más rápido que el desastre anterior
fuente
int16_t
). Pero su función C afirma que ordena una matriz deint
(que es de 32 bits en todas las implementaciones de C que admiten esaasm
sintaxis). ¿Lo probó solo con pequeños enteros positivos que solo tienen 0 en sus mitades altas? Eso funcionará ... Paraint
que necesite SSE4.1pmin/maxsd
(d = dword). felixcloutier.com/x86/pminsd:pminsq opminusd
parauint32_t
.Descubrí que al menos en mi sistema, las funciones
sort6_iterator()
y lassort6_iterator_local()
definidas a continuación se ejecutan al menos tan rápido, y con frecuencia notablemente más rápido, que el poseedor del récord actual anterior:Pasé esta función de un
std::vector
iterador en mi código de tiempo.Sospecho (por comentarios como este y en otros lugares) que el uso de iteradores le da a g ++ ciertas garantías sobre lo que puede y no puede sucederle a la memoria a la que se refiere el iterador, que de lo contrario no tendría y son estas garantías las que permiten a g ++ optimice mejor el código de clasificación (por ejemplo, con punteros, el compilador no puede estar seguro de que todos los punteros apuntan a diferentes ubicaciones de memoria). Si recuerdo correctamente, esto también es parte de la razón por la cual tantos algoritmos STL, como
std::sort()
, generalmente tienen un rendimiento tan obsceno.Por otra parte,
sort6_iterator()
es algunas veces (de nuevo, dependiendo del contexto en el que se llama a la función) superado constantemente por la siguiente función de clasificación, que copia los datos en variables locales antes de la clasificación ellos. 1 Tenga en cuenta que dado que solo hay 6 variables locales definidas, si estas variables locales son primitivas, es probable que nunca se almacenen en la RAM y que solo se almacenen en los registros de la CPU hasta el final de la llamada a la función, lo que ayuda a hacer esta clasificación Funcionan rápido. (También ayuda que el compilador sepa que distintas variables locales tienen ubicaciones distintas en la memoria).Tenga en cuenta que la definición de la
SWAP()
siguiente manera algunas veces resulta en un rendimiento ligeramente mejor, aunque la mayoría de las veces resulta en un rendimiento ligeramente peor o una diferencia insignificante en el rendimiento.Si solo desea un algoritmo de ordenación que en los tipos de datos primitivos, gcc -O3 sea consistentemente bueno para la optimización, sin importar en qué contexto aparezca la llamada a la función de ordenación en 1 , luego, dependiendo de cómo pase la entrada, intente uno de los dos siguientes algoritmos:
O si desea pasar las variables por referencia, use esto (la función a continuación difiere de la anterior en sus primeras 5 líneas):
La razón para usar la
register
palabra clave es porque esta es una de las pocas veces que sabe que desea estos valores en los registros. Sinregister
, el compilador resolverá esto la mayor parte del tiempo, pero a veces no lo hace. El uso de laregister
palabra clave ayuda a resolver este problema. Sin embargo, normalmente no use laregister
palabra clave ya que es más probable que ralentice su código que lo acelere.Además, tenga en cuenta el uso de plantillas. Esto se hace a propósito ya que, incluso con la
inline
palabra clave, las funciones de plantilla generalmente están mucho más agresivamente optimizadas por gcc que las funciones vanilla C (esto tiene que ver con gcc que necesita tratar con punteros de función para funciones vanilla C pero no con funciones de plantilla).fuente
Intente "fusionar la lista ordenada". :) Use dos arreglos. Más rápido para pequeñas y grandes series.
Si concatena, solo verifica dónde se inserta. Otros valores más grandes que no necesita comparar (cmp = ab> 0).
Para 4 números, puede usar el sistema 4-5 cmp (~ 4.6) o 3-6 cmp (~ 4.9). El tipo de burbuja utiliza 6 cmp (6). Un montón de cmp para grandes números de código más lento.
Este código usa 5 cmp (no ordena MSL):
if (cmp(arr[n][i+0],arr[n][i+1])>0) {swap(n,i+0,i+1);} if (cmp(arr[n][i+2],arr[n][i+3])>0) {swap(n,i+2,i+3);} if (cmp(arr[n][i+0],arr[n][i+2])>0) {swap(n,i+0,i+2);} if (cmp(arr[n][i+1],arr[n][i+3])>0) {swap(n,i+1,i+3);} if (cmp(arr[n][i+1],arr[n][i+2])>0) {swap(n,i+1,i+2);}
Principial MSL
9 8 7 6 5 4 3 2 1 0 89 67 45 23 01 ... concat two sorted lists, list length = 1 6789 2345 01 ... concat two sorted lists, list length = 2 23456789 01 ... concat two sorted lists, list length = 4 0123456789 ... concat two sorted lists, list length = 8
código js
fuente
Ordenar 4 elementos con uso cmp == 0. Los números de cmp son ~ 4.34 (los nativos de FF tienen ~ 4.52), pero tardan 3 veces más que fusionar listas. Pero mejor menos operaciones cmp, si tiene números grandes o texto grande. Editar: error reparado
Prueba en línea http://mlich.zam.slu.cz/js-sort/x-sort-x2.htm
fuente
Tal vez yo soy tarde a la fiesta, pero al menos mi aportación es un nuevo enfoque.
swap
fuera más alto (es decir, el costo decompare
)SWAP()
dos elementos, los ciclos se persiguen, necesitando solo una temperatura y un intercambio (registro-> registro) (nuevo <- antiguo).Actualización: cambió un poco el código, algunas personas usan compiladores C ++ para compilar código C ...
fuente
wsort6()
función final tiene la interfaz correcta.o1..o5
, no hay necesidad de la segundae[6]
matriz temporal . Y: ¿compilar código C en un compilador de C ++ y culpar al código?#include
. Solucionadofuente
Bueno, si son solo 6 elementos y puede aprovechar el paralelismo, quiere minimizar la ramificación condicional, etc. ¿Por qué no genera todas las combinaciones y prueba el orden? Me atrevería a decir que en algunas arquitecturas, puede ser bastante rápido (siempre que tenga la memoria preasignada)
fuente
Aquí hay tres métodos de clasificación típicos que representan tres clases diferentes de Algoritmos de clasificación:
¿Pero mira la discusión de Stefan Nelsson sobre el algoritmo de clasificación más rápido? donde discute una solución que se reduce a
O(n log log n)
... verifique su implementación en CEste algoritmo de clasificación semi-lineal fue presentado por un artículo en 1995:
A. Andersson, T. Hagerup, S. Nilsson y R. Raman. ¿Clasificación en tiempo lineal? En Actas del 27º Simposio anual de ACM sobre la teoría de la informática, páginas 427-436, 1995.
fuente