Respondendo a outra pergunta do Stack Overflow ( esta ), deparei-me com um sub-problema interessante. Qual é a maneira mais rápida de classificar uma matriz de 6 números inteiros?
Como a pergunta é de nível muito baixo:
- não podemos assumir que as bibliotecas estão disponíveis (e a chamada em si tem seu custo), apenas C simples
- para evitar esvaziar o pipeline de instruções (que tem um custo muito alto), provavelmente devemos minimizar desvios, saltos e todos os outros tipos de interrupção do fluxo de controle (como aqueles ocultos atrás dos pontos de sequência em
&&
ou||
). - o espaço é restrito e a minimização de registros e o uso da memória são um problema; a classificação ideal é provavelmente a melhor.
Realmente, essa pergunta é um tipo de golfe em que o objetivo não é minimizar o comprimento da fonte, mas o tempo de execução. Eu chamo de código 'Zening', como usado no título do livro Zen of Code optimization, de Michael Abrash e suas sequências .
Por que é interessante, existem várias camadas:
- o exemplo é simples e fácil de entender e medir, não há muita habilidade em C envolvida
- mostra efeitos de escolha de um bom algoritmo para o problema, mas também efeitos do compilador e do hardware subjacente.
Aqui está minha implementação de referência (ingênua, não otimizada) e meu conjunto de testes.
#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 brutos
À medida que o número de variantes aumenta, reuni todas em uma suíte de testes que pode ser encontrada aqui . Os testes atuais usados são um pouco menos ingênuos do que os mostrados acima, graças a Kevin Stock. Você pode compilá-lo e executá-lo em seu próprio ambiente. Estou bastante interessado no comportamento em diferentes arquiteturas / compiladores de destino. (OK pessoal, coloque isso em respostas, adicionarei +1 a cada colaborador de um novo conjunto de resultados).
Dei a resposta a Daniel Stutzbach (para jogar golfe) há um ano, pois ele estava na fonte da solução mais rápida na época (redes de classificação).
Linux 64 bits, gcc 4.6.1 64 bits, Intel Core 2 Duo E8400, -O2
- Chamada direta à função da biblioteca qsort: 689.38
- Implementação ingênua (tipo de inserção): 285,70
- Classificação de inserção (Daniel Stutzbach): 142.12
- Classificação de inserção desenrolada: 125.47
- Ordem de classificação: 102.26
- Ordem de classificação com registros: 58.03
- Redes de classificação (Daniel Stutzbach): 111.68
- Redes de classificação (Paul R): 66,36
- Classificação de redes 12 com troca rápida: 58,86
- Redes de classificação 12 reordenadas Swap: 53.74
- Redes de classificação 12 reorganizadas Simple Swap: 31.54
- Rede de classificação reordenada com troca rápida: 31,54
- Rede de classificação reordenada com troca rápida V2: 33.63
- Classificação de bolha embutida (Paolo Bonzini): 48,85
- Classificação de inserção desenrolada (Paolo Bonzini): 75,30
Linux 64 bits, gcc 4.6.1 64 bits, Intel Core 2 Duo E8400, -O1
- Chamada direta à função da biblioteca qsort: 705.93
- Implementação ingênua (classificação por inserção): 135,60
- Classificação de inserção (Daniel Stutzbach): 142.11
- Classificação de inserção desenrolada: 126.75
- Ordem de classificação: 46.42
- Ordem de classificação com registros: 43,58
- Redes de classificação (Daniel Stutzbach): 115.57
- Redes de classificação (Paul R): 64,44
- Classificando redes 12 com troca rápida: 61,98
- Redes de classificação 12 reordenadas Swap: 54.67
- Redes de classificação 12 reorganizadas Simple Swap: 31.54
- Rede de classificação reordenada com troca rápida: 31,24
- Rede de classificação reordenada com troca rápida V2: 33.07
- Classificação de bolha embutida (Paolo Bonzini): 45,79
- Classificação de inserção desenrolada (Paolo Bonzini): 80.15
Incluí os resultados -O1 e -O2 porque, surpreendentemente, para vários programas, o O2 é menos eficiente que o O1. Gostaria de saber que otimização específica tem esse efeito?
Comentários sobre soluções propostas
Classificação de inserção (Daniel Stutzbach)
Como esperado, minimizar ramos é realmente uma boa ideia.
Redes de classificação (Daniel Stutzbach)
Melhor que a classificação por inserção. Eu me perguntava se o efeito principal não era o de evitar o loop externo. Eu tentei por tipo de inserção desenrolado para verificar e, de fato, obtemos aproximadamente as mesmas figuras (o código está aqui ).
Redes de classificação (Paul R)
O melhor até agora. O código real que eu usei para testar é aqui . Ainda não sabemos por que é quase duas vezes mais rápido que a outra implementação de rede de classificação. Passagem de parâmetro? Max rápido?
Classificação de redes 12 SWAP com troca rápida
Conforme sugerido por Daniel Stutzbach, combinei sua rede de classificação de 12 trocas com a troca rápida sem ramificação (o código é aqui ). É realmente mais rápido, o melhor até agora com uma pequena margem (aproximadamente 5%), como seria de esperar usando 1 swap a menos.
Também é interessante notar que a troca sem ramificação parece ser muito (4 vezes) menos eficiente do que a simples usando na arquitetura PPC.
Biblioteca de chamadas qsort
Para dar outro ponto de referência, também tentei, como sugerido, chamar a biblioteca qsort (o código está aqui ). Como esperado, é muito mais lento: 10 a 30 vezes mais lento ... como ficou óbvio com o novo conjunto de testes, o principal problema parece ser o carregamento inicial da biblioteca após a primeira chamada, e não se compara tão mal com outros versão. É apenas entre 3 e 20 vezes mais lento no meu Linux. Em algumas arquiteturas usadas para testes por outras pessoas, parece até mais rápido (estou realmente surpreso com essa, pois a biblioteca qsort usa uma API mais complexa).
Ordem de classificação
Rex Kerr propôs outro método completamente diferente: para cada item da matriz, calcule diretamente sua posição final. Isso é eficiente porque a ordem de classificação da computação não precisa de ramificação. A desvantagem desse método é que ele leva três vezes a quantidade de memória da matriz (uma cópia da matriz e variáveis para armazenar ordens de classificação). Os resultados do desempenho são muito surpreendentes (e interessantes). Na minha arquitetura de referência com sistema operacional de 32 bits e Intel Core2 Quad E8300, a contagem de ciclos ficou um pouco abaixo de 1000 (como redes de classificação com troca de ramificação). Mas quando compilado e executado na minha caixa de 64 bits (Intel Core2 Duo), teve um desempenho muito melhor: tornou-se o mais rápido até agora. Eu finalmente descobri a verdadeira razão. Minha caixa de 32 bits usa o gcc 4.4.1 e minha caixa de 64 bits gcc 4.4.
update :
Como as figuras publicadas acima mostram esse efeito ainda foi aprimorado pelas versões posteriores do gcc e o Rank Order se tornou consistentemente duas vezes mais rápido do que qualquer outra alternativa.
Classificando redes 12 com Swap reordenado
A incrível eficiência da proposta Rex Kerr com o gcc 4.4.3 me fez pensar: como um programa com uso de memória três vezes maior pode ser mais rápido que as redes de classificação sem ramificação? Minha hipótese era que ele tinha menos dependências do tipo lido após gravação, permitindo um melhor uso do agendador de instruções superescalares do x86. Isso me deu uma idéia: reorganizar trocas para minimizar as dependências de leitura após gravação. Em outras palavras: quando você SWAP(1, 2); SWAP(0, 2);
precisa aguardar a conclusão da primeira troca antes de executar a segunda, porque ambos acessam uma célula de memória comum. Quando você faz SWAP(1, 2); SWAP(4, 5);
o processador pode executar os dois em paralelo. Eu tentei e funciona como esperado, as redes de classificação estão executando 10% mais rápido.
Classificando redes 12 com troca simples
Um ano após o post original sugerido por Steinar H. Gunderson, não devemos tentar enganar o compilador e manter o código de troca simples. É realmente uma boa ideia, pois o código resultante é cerca de 40% mais rápido! Ele também propôs uma troca otimizada manualmente usando o código de montagem em linha x86 que ainda pode poupar mais alguns ciclos. O mais surpreendente (diz volumes sobre a psicologia do programador) é que, um ano atrás, nenhum dos usados tentou essa versão do swap. O código que eu costumava testar está aqui . Outros sugeriram outras maneiras de escrever uma troca rápida C, mas produz as mesmas performances que a simples com um compilador decente.
O "melhor" código é agora o seguinte:
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
}
Se acreditarmos que nosso conjunto de testes (e, sim, é muito ruim, seu benefício é curto, simples e fácil de entender o que estamos medindo), o número médio de ciclos do código resultante para um tipo é inferior a 40 ciclos ( 6 testes são executados). Isso coloca cada troca em uma média de 4 ciclos. Eu chamo isso incrivelmente rápido. Quaisquer outras melhorias possíveis?
x-y
ex+y
não irá causar underflow ou estouro?__asm__ volatile (".byte 0x0f, 0x31; shlq $32, %%rdx; orq %%rdx, %0" : "=a" (x) : : "rdx");
porque o rdtsc coloca a resposta no EDX: EAX enquanto o GCC espera isso em um único registro de 64 bits. Você pode ver o bug compilando em -O3. Veja também abaixo meu comentário a Paul R sobre um SWAP mais rápido.CMP EAX, EBX; SBB EAX, EAX
colocará 0 ou 0xFFFFFFFFEAX
dependendo seEAX
é maior ou menor queEBX
, respectivamente.SBB
é "subtrair com empréstimo", a contrapartida deADC
("adicionar com transporte"); o bit de status a que você se refere é o bit de transporte. Então, lembro-me dissoADC
eSBB
tinha uma latência e taxa de transferência terríveis no Pentium 4 vs.ADD
eSUB
, e ainda era duas vezes mais lento nas CPUs Core. Desde o 80386, também existem instruções deSETcc
armazenamentoCMOVcc
condicional e movimentação condicional, mas também são lentas.Respostas:
Para qualquer otimização, é sempre melhor testar, testar, testar. Eu tentaria pelo menos classificar redes e classificar por inserção. Se eu estivesse apostando, apostaria meu dinheiro na inserção com base em experiências anteriores.
Você sabe alguma coisa sobre os dados de entrada? Alguns algoritmos terão melhor desempenho com certos tipos de dados. Por exemplo, a classificação por inserção tem melhor desempenho em dados classificados ou quase classificados, portanto, será a melhor opção se houver uma chance acima da média de dados quase classificados.
O algoritmo que você postou é semelhante a uma classificação por inserção, mas parece que você minimizou o número de trocas ao custo de mais comparações. As comparações são muito mais caras que as trocas, porque as filiais podem causar a paralisação do pipeline de instruções.
Aqui está uma implementação de classificação por inserção:
Aqui está como eu construiria uma rede de classificação. Primeiro, use este site para gerar um conjunto mínimo de macros SWAP para uma rede do comprimento apropriado. Agrupar isso em uma função me dá:
fonte
n < SMALL_CONSTANT
.Aqui está uma implementação usando redes de classificação :
Você realmente precisa de implementações
min
e ramificações muito eficientesmax
para isso, já que é efetivamente o que esse código se resume - uma sequência demin
emax
operações (13 de cada, no total). Deixo isso como um exercício para o leitor.Observe que esta implementação se presta facilmente à vetorização (por exemplo, SIMD - a maioria dos ISAs SIMD tem instruções mínimas / máximas de vetor) e também às implementações de GPU (por exemplo, CUDA - sem ramificação, não há problemas com divergência de urdidura etc.).
Consulte também: Implementação rápida de algoritmo para classificar lista muito pequena
fonte
Sort3
seria mais rápido (na maioria das arquiteturas), se você notasse que esse(a+b+c)-(min+max)
é o 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; }
. Aqui não estou usando?: Para d [y] porque apresenta um desempenho um pouco pior, mas está quase no barulho.Como esses números são inteiros e as comparações são rápidas, por que não calcular diretamente a ordem de classificação de cada um:
fonte
0+1+2+3+4+5=15
Desde um deles está faltando, 15 menos a soma dos rendimentos de descanso faltando umParece que cheguei à festa um ano atrasado, mas aqui vamos nós ...
Observando a montagem gerada pelo gcc 4.5.2, observei que cargas e lojas estão sendo feitas para cada troca, o que realmente não é necessário. Seria melhor carregar os 6 valores nos registradores, classificá-los e armazená-los novamente na memória. Ordenei que as cargas nas lojas estivessem o mais próximo possível dos registros necessários e usados pela última vez. Também usei a macro SWAP de Steinar H. Gunderson. Atualização: mudei para a macro SWAP de Paolo Bonzini, que o gcc se converte em algo semelhante ao de Gunderson, mas o gcc é capaz de ordenar melhor as instruções, pois elas não são fornecidas como montagem explícita.
Usei a mesma ordem de troca que a rede de troca reordenada, que apresentou o melhor desempenho, embora possa haver uma melhor ordem. Se eu encontrar mais tempo, gerarei e testarei várias permutações.
Alterei o código de teste para considerar mais de 4000 matrizes e mostrar o número médio de ciclos necessários para classificar cada um. Em um i5-650, estou recebendo ~ 34,1 ciclos / classificação (usando -O3), em comparação com a rede de classificação reordenada original obtendo ~ 65,3 ciclos / classificação (usando -O1, supera -O2 e -O3).
Mudei modificado o conjunto de testes para também relatam relógios por tipo e executar mais testes (a função cmp foi atualizado para estouro punho inteiro também), aqui estão os resultados em algumas arquiteturas diferentes. Tentei testar em uma CPU AMD, mas o rdtsc não é confiável no X6 1100T que tenho disponível.
fonte
-O3
otimização não seja contraproducente.#define SWAP(x,y) { int oldx = x; x = x < y ? x : y; y ^= oldx ^ x; }
.Eu me deparei com essa pergunta do Google há alguns dias atrás, porque eu também precisava classificar rapidamente uma matriz de comprimento fixo de 6 números inteiros. No entanto, no meu caso, meus números inteiros são apenas 8 bits (em vez de 32) e não tenho um requisito estrito de usar apenas C. Eu pensei em compartilhar minhas descobertas de qualquer maneira, caso possam ser úteis para alguém ...
Eu implementei uma variante de uma classificação de rede no assembly que usa o SSE para vetorizar as operações de comparação e troca, na medida do possível. São necessários seis "passes" para classificar completamente a matriz. Usei um novo mecanismo para converter diretamente os resultados do PCMPGTB (comparação vetorizada) em parâmetros aleatórios para PSHUFB (troca vetorizada), usando apenas um PADDB (adição vetorizada) e, em alguns casos, também uma instrução PAND (AND bit a bit).
Essa abordagem também teve o efeito colateral de produzir um função verdadeiramente sem ramo. Não há instruções de salto.
Parece que esta implementação é cerca de 38% mais rápida que a implementação atualmente marcada como a opção mais rápida na pergunta ("Classificando redes 12 com troca simples"). Modifiquei essa implementação para usar
char
elementos de matriz durante meus testes, para tornar a comparação justa.Devo observar que essa abordagem pode ser aplicada a qualquer tamanho de matriz com até 16 elementos. Espero que a vantagem da velocidade relativa sobre as alternativas cresça mais para as matrizes maiores.
O código está escrito no MASM para processadores x86_64 com SSSE3. A função usa a "nova" convenção de chamada do Windows x64. Aqui está...
Você pode compilar isso em um objeto executável e vinculá-lo ao seu projeto C. Para obter instruções sobre como fazer isso no Visual Studio, você pode ler este artigo . Você pode usar o seguinte protótipo C para chamar a função do seu código C:
fonte
pxor / pinsrd xmm4, mem, 0
, basta usarmovd
!O código de teste é muito ruim; transborda a matriz inicial (as pessoas aqui não leem avisos do compilador?), o printf está imprimindo os elementos errados, usa .byte para rdtsc sem nenhuma boa razão, há apenas uma execução (!), não há nada verificando se o os resultados finais estão realmente corretos (por isso é muito fácil “otimizar” algo subtilmente errado), os testes incluídos são muito rudimentares (sem números negativos?) e não há nada para impedir o compilador de descartar toda a função como código morto.
Dito isto, também é muito fácil melhorar a solução de rede bitônica; basta alterar o material mínimo / máximo / SWAP para
e sai 65% mais rápido para mim (Debian gcc 4.4.5 com -O2, amd64, Core i7).
fonte
Embora eu realmente goste da macro de troca fornecida:
Vejo uma melhoria (que um bom compilador pode fazer):
Tomamos nota de como min e max funcionam e extraímos explicitamente a subexpressão comum. Isso elimina completamente as macros min e max.
fonte
d[x]
vez dex
(o mesmo paray
), ed[y] < d[x]
a desigualdade aqui (sim, diferente do código min / max).Nunca otimize o mínimo / o máximo sem fazer comparações e observar o conjunto gerado pelo compilador. Se eu permitir que o GCC otimize o mínimo com instruções de movimentação condicional, recebo uma aceleração de 33%:
(280 vs. 420 ciclos no código de teste). Fazer max com?: É mais ou menos o mesmo, quase perdido no barulho, mas o acima é um pouco mais rápido. Esse SWAP é mais rápido com o GCC e o Clang.
Os compiladores também estão fazendo um trabalho excepcional na alocação de registros e na análise de alias, movendo efetivamente d [x] para variáveis locais antecipadamente e apenas copiando de volta para a memória no final. De fato, eles o fazem ainda melhor do que se você trabalhasse inteiramente com variáveis locais (como
d0 = d[0], d1 = d[1], d2 = d[2], d3 = d[3], d4 = d[4], d5 = d[5]
). Estou escrevendo isso porque você está assumindo uma otimização forte e ainda tentando enganar o compilador em mín / máx. :)A propósito, tentei Clang e GCC. Eles fazem a mesma otimização, mas devido às diferenças de agendamento, os dois têm alguma variação nos resultados, não podem dizer realmente qual é mais rápido ou mais lento. O GCC é mais rápido nas redes de classificação, Clang nos tipos quadráticos.
Apenas para completar, também é possível a classificação desenrolada de bolhas e tipos de inserção. Aqui está o tipo de bolha:
e aqui está o tipo de inserção:
Esse tipo de inserção é mais rápido que o de Daniel Stutzbach e é especialmente bom em uma GPU ou em um computador com predicação porque o ITER pode ser feito com apenas 3 instruções (vs. 4 para SWAP). Por exemplo, aqui está a
t = d[2]; ITER(1); ITER(0);
linha na montagem do ARM:Para seis elementos, a classificação por inserção é competitiva com a rede de classificação (12 swaps vs. 15 iterações equilibram 4 instruções / swap vs. 3 instruções / iteração); o tipo de bolha, é claro, é mais lento. Mas não será verdade quando o tamanho aumentar, pois a classificação de inserção é O (n ^ 2) enquanto as redes de classificação são O (n log n).
fonte
Portei o conjunto de testes para uma máquina de arquitetura PPC que não consigo identificar (não precisava tocar no código, apenas aumente as iterações do teste, use 8 casos de teste para evitar poluir os resultados com os mods e substitua o rdtsc específico do x86):
Chamada direta para a função da biblioteca qsort : 101
Implementação ingênua (classificação por inserção) : 299
Classificação de inserção (Daniel Stutzbach) : 108
Classificação de inserção desenrolada : 51
Redes de classificação (Daniel Stutzbach) : 26
Redes de classificação (Paul R) : 85
Classificando redes 12 com troca rápida : 117
Redes de classificação 12 troca reordenada : 116
Ordem de classificação : 56
fonte
subfc r5,r4,r3; subfe r6,r6,r6; andc r6,r5,r6; add r4,r6,r4; subf r3,r6,r3
. r3 / r4 são entradas, r5 / r6 são registros de arranhões, na saída r3 obtém o mínimo e r4 obtém o máximo. Ele deve ser decentemente agendado manualmente. Encontrei-o com o superoptimizador GNU, iniciando com sequências mín e máx de 4 instruções e procurando manualmente duas que poderiam ser combinadas. Para entradas assinadas, é claro que você pode adicionar 0x80000000 a todos os elementos no início e subtraí-lo novamente no final e trabalhar como se não tivessem assinado.Uma troca XOR pode ser útil em suas funções de troca.
O if pode causar muita divergência no seu código, mas se você tiver uma garantia de que todos os seus ints são únicos, isso pode ser útil.
fonte
x
ey
aponte para o mesmo local.Ansioso para tentar fazer isso e aprender com esses exemplos, mas primeiro alguns tempos do meu PowerBook G4 de 1,5 GHz e PPC Power4 G / 1 GB de RAM DDR. (Emprestei um temporizador semelhante ao rdtsc para PPC em http://www.mcs.anl.gov/~kazutomo/rdtsc.html para os cronogramas.) o programa algumas vezes e os resultados absolutos variaram, mas os resultados consistentes O teste mais rápido foi "Insertion Sort (Daniel Stutzbach)", com "Insertion Sort Unrolled" um segundo próximo.
Aqui está o último conjunto de horários:
fonte
Aqui está minha contribuição para este segmento: um shellsort otimizado de 1, 4 hiatos para um vetor int de 6 membros (valp) contendo valores exclusivos.
No meu laptop HP dv7-3010so, com um Athlon M300 a 2 Ghz de dois núcleos (memória DDR2), ele é executado em 165 ciclos de clock. Esta é uma média calculada a partir do tempo de cada sequência única (6! / 720 no total). Compilado no Win32 usando o OpenWatcom 1.8. O loop é essencialmente uma classificação de inserção e tem 16 instruções / 37 bytes de comprimento.
Não tenho um ambiente de 64 bits para compilar.
fonte
Se o tipo de inserção for razoavelmente competitivo aqui, eu recomendaria tentar um shellsort. Receio que 6 elementos sejam provavelmente muito pouco para estar entre os melhores, mas pode valer a pena tentar.
Código de exemplo, não testado, sem erros, etc. Você deseja ajustar a sequência inc = 4 e inc - = 3 para encontrar o ideal (tente inc = 2, inc - = 1 por exemplo).
Eu não acho que isso vai ganhar, mas se alguém postar uma pergunta sobre a classificação de 10 elementos, quem sabe ...
Segundo a Wikipedia, isso pode até ser combinado com redes de classificação: Pratt, V (1979). Shellsort e redes de triagem (excelentes dissertações em ciências da computação). Festão. ISBN 0-824-04406-1
fonte
Sei que estou super atrasado, mas estava interessado em experimentar algumas soluções diferentes. Primeiro, limpei a pasta, compilei e coloquei em um repositório. Eu mantive algumas soluções indesejáveis como becos sem saída para que outros não tentassem. Entre isso, estava minha primeira solução, que tentou garantir que x1> x2 fosse calculado uma vez. Após a otimização, não é mais rápido que as outras versões simples.
Eu adicionei uma versão em loop da classificação por ordem de classificação, já que minha própria aplicação deste estudo é para classificar de 2 a 8 itens; portanto, como há um número variável de argumentos, é necessário um loop. É também por isso que ignorei as soluções de rede de classificação.
O código de teste não testou se as duplicatas foram tratadas corretamente, portanto, enquanto as soluções existentes estavam todas corretas, adicionei um caso especial ao código de teste para garantir que as duplicatas fossem manipuladas corretamente.
Em seguida, escrevi uma classificação de inserção inteiramente nos registros do AVX. Na minha máquina, é 25% mais rápido que os outros tipos de inserção, mas 100% mais lento que a ordem de classificação. Fiz isso puramente para o experimento e não esperava que isso fosse melhor devido à ramificação no tipo de inserção.
Em seguida, escrevi uma ordem de classificação usando o AVX. Isso corresponde à velocidade das outras soluções de ordem de classificação, mas não é mais rápido. O problema aqui é que eu só posso calcular os índices com o AVX e, em seguida, tenho que fazer uma tabela de índices. Isso ocorre porque o cálculo é baseado no destino e não na origem. Consulte Convertendo de índices baseados na fonte para índices baseados no destino
O repo pode ser encontrado aqui: https://github.com/eyepatchParrot/sort6/
fonte
vmovmskps
em vetores inteiros (com uma conversão para manter os intrínsecos felizes), evitando a necessidade de deslocar offs
resultado bitscan ( ) à direita .cmpgt
resultado, subtraindo -o, em vez de mascará-loset1(1)
. por exemplo,index = _mm256_sub_epi32(index, gt)
fazindex -= -1 or 0;
eq = _mm256_insert_epi32(eq, 0, I)
não é uma maneira eficiente de zerar um elemento se ele compilar como gravado (especialmente para elementos fora do ponto baixo 4, porquevpinsrd
só está disponível com um destino XMM; índices maiores que 3 precisam ser emulados). Em vez disso,_mm256_blend_epi32
(vpblendd
) com um vetor zerado.vpblendd
é uma instrução de operação única executada em qualquer porta, em comparação com uma reprodução aleatória que precisa da porta 5 nas CPUs Intel. ( agner.org/optimize ).rot
vetores com diferentes shuffles da mesma fonte ou, pelo menos, executar 2 cadeias dep em paralelo que você usa alternadamente, em vez de uma única cadeia dep através de uma shuffle de cruzamento de faixa (latência de 3 ciclos). Isso aumentará o ILP em uma única classificação. 2 dep chains limitam o número de constantes do vetor a um número razoável, apenas 2: 1 para uma rotação e uma para 2 etapas de rotação combinadas.Essa questão está se tornando bastante antiga, mas na verdade eu tive que resolver o mesmo problema hoje em dia: agoritmos rápidos para classificar pequenas matrizes. Eu pensei que seria uma boa idéia compartilhar meu conhecimento. Enquanto eu comecei a usar redes de classificação, finalmente consegui encontrar outros algoritmos para os quais o número total de comparações realizadas para classificar cada permutação de 6 valores era menor que nas redes de classificação e menor que na classificação por inserção. Não contei o número de swaps; Eu esperaria que fosse aproximadamente equivalente (talvez um pouco maior às vezes).
O algoritmo
sort6
usa o algoritmosort4
que usa o algoritmosort3
. Aqui está a implementação em alguma forma C ++ leve (o original é pesado para modelos, para que possa trabalhar com qualquer iterador de acesso aleatório e qualquer função de comparação adequada).Classificando 3 valores
O algoritmo a seguir é uma classificação de inserção desenrolada. Quando dois swaps (6 atribuições) precisam ser executados, ele usa 4 atribuições:
Parece um pouco complexo porque a classificação possui mais ou menos um ramo para cada permutação possível da matriz, usando 2 a 3 comparações e no máximo 4 atribuições para classificar os três valores.
Classificando 4 valores
Essa chamada
sort3
então executa uma classificação de inserção desenrolada com o último elemento da matriz:Esse algoritmo realiza 3 a 6 comparações e, no máximo, 5 swaps. É fácil desenrolar uma classificação de inserção, mas usaremos outro algoritmo para a última classificação ...
Classificando 6 valores
Este usa uma versão desenrolada do que chamei de classificação de inserção dupla . O nome não é tão bom, mas é bastante descritivo, eis como ele funciona:
Após a troca, o primeiro elemento é sempre menor que o último, o que significa que, ao inseri-los na sequência classificada, não haverá mais de N comparações para inserir os dois elementos no pior caso: por exemplo, se o primeiro elemento foi inserido na 3ª posição e, em seguida, o último não pode ser inserido abaixo da 4ª posição.
Meus testes em cada permutação de 6 valores sempre mostram que esse algoritmo sempre executa entre 6 e 13 comparações. Não calculei o número de swaps realizados, mas não espero que seja maior que 11 no pior caso.
Espero que isso ajude, mesmo que essa pergunta possa não representar mais um problema real :)
EDIT: depois de colocá-lo no benchmark fornecido, é claramente mais lento que a maioria das alternativas interessantes. Ele tende a ter um desempenho um pouco melhor do que o tipo de inserção desenrolada, mas é praticamente isso. Basicamente, não é o melhor tipo para números inteiros, mas pode ser interessante para tipos com uma operação de comparação cara.
fonte
operator<
para a comparação. Além da contagem objetiva de comparações e swaps, também cronometrei corretamente meus algoritmos; essa solução foi a mais rápida genérica, mas eu realmente senti falta da @ RexKerr. Vou tentar :)-O3
. Acho que vou adotar outra estratégia para minha biblioteca de classificação: fornecer três tipos de algoritmos para ter um baixo número de comparações, um baixo número de trocas ou potencialmente o melhor desempenho. Pelo menos, o que acontece será transparente para o leitor. Obrigado por suas idéias :)Eu acredito que há duas partes em sua pergunta.
Eu não me preocuparia muito em esvaziar pipelines (assumindo o x86 atual): a previsão de ramificação já percorreu um longo caminho. O que me preocuparia é garantir que o código e os dados se encaixem em uma linha de cache cada (talvez duas para o código). Uma vez lá, as latências de busca são refrescantemente baixas, o que compensará qualquer interrupção. Isso também significa que seu loop interno terá talvez dez instruções, aproximadamente o que deveria estar (existem dois loops internos diferentes no meu algoritmo de classificação, são 10 instruções / 22 bytes e 9/22 de comprimento, respectivamente). Supondo que o código não contenha divs, você pode ter certeza de que será incrivelmente rápido.
fonte
Eu sei que esta é uma pergunta antiga.
Mas acabei de escrever um tipo diferente de solução que quero compartilhar.
Usando nada além de MIN MAX aninhado,
Não é rápido como ele usa 114 de cada um,
poderia reduzi-la a 75 muito simplesmente como assim -> pastebin
Mas então não é mais puramente min max.
O que pode funcionar é fazer min / max em vários números inteiros de uma só vez com o AVX
Referência PMINSW
EDIT:
Solução de ordem de classificação inspirada em Rex Kerr, muito mais rápida que a bagunça acima
fonte
int16_t
). Mas sua função C afirma que classifica uma matriz deint
(que é de 32 bits em todas as implementações em C que suportam essaasm
sintaxe). Você o testou apenas com números inteiros positivos pequenos que têm apenas 0 na metade superior? Isso vai funcionar ... Paraint
você precisar do SSE4.1pmin/maxsd
(d = dword). felixcloutier.com/x86/pminsd:pminsq oupminusd
parauint32_t
.Descobri que, pelo menos no meu sistema, as funções
sort6_iterator()
e assort6_iterator_local()
definidas a seguir executavam pelo menos com a mesma rapidez e com frequência notavelmente mais rápidas do que o atual detentor de registro acima:Passei
std::vector
o iterador dessa função a no meu código de temporização.Eu suspeito (de comentários como este e em outros lugares) que o uso de iteradores fornece ao g ++ certas garantias sobre o que pode e o que não pode acontecer com a memória à qual o iterador se refere, o que de outra forma não teria e são essas garantias que permitem ao g ++ otimizar melhor o código de classificação (por exemplo, com ponteiros, o compilador não pode ter certeza de que todos os ponteiros estão apontando para diferentes locais de memória). Se bem me lembro, isso também faz parte do motivo pelo qual muitos algoritmos STL, como
std::sort()
, geralmente têm um desempenho tão obscenamente bom.Além disso,
sort6_iterator()
é algumas vezes (mais uma vez, dependendo do contexto em que a função é chamada) consistentemente superado pela função de classificação seguinte, que copia os dados em locais variáveis antes separá-los. 1 Observe que, como existem apenas 6 variáveis locais definidas, se essas variáveis locais são primitivas, elas provavelmente nunca serão realmente armazenadas na RAM e, em vez disso, serão armazenadas nos registros da CPU até o final da chamada de função, o que ajuda a fazer essa classificação função rápida. (Também ajuda que o compilador saiba que variáveis locais distintas têm locais distintos na memória).Observe que a definição a
SWAP()
seguir algumas vezes resulta em desempenho ligeiramente melhor, embora na maioria das vezes resulte em desempenho ligeiramente pior ou em uma diferença insignificante de desempenho.Se você deseja apenas um algoritmo de classificação que, nos tipos de dados primitivos, o gcc -O3 seja sempre bom em otimizar, independentemente do contexto em que a chamada para a função de classificação apareça em 1 , dependendo de como você passa a entrada, tente um dos seguintes dois algoritmos:
Ou, se você deseja passar as variáveis por referência, use-as (a função abaixo difere das anteriores nas 5 primeiras linhas):
O motivo para usar a
register
palavra-chave é que essa é uma das poucas vezes em que você sabe que deseja esses valores nos registros. Semregister
, o compilador descobrirá isso na maioria das vezes, mas às vezes não. O uso daregister
palavra - chave ajuda a resolver esse problema. Normalmente, no entanto, não use aregister
palavra-chave, pois é mais provável que seu código fique mais lento do que acelerá-lo.Observe também o uso de modelos. Isso é feito de propósito, já que, mesmo com a
inline
palavra - chave, as funções de modelo geralmente são otimizadas de maneira muito mais agressiva pelo gcc do que as funções vanilla C (isso tem a ver com o gcc que precisa lidar com ponteiros de função para funções vanilla C, mas não com funções de modelo).fonte
Tente 'mesclar lista classificada'. :) Use duas matrizes. O mais rápido para pequenas e grandes séries.
Se você concordar, verifique apenas onde inserir. Outros valores maiores que você não precisa comparar (cmp = ab> 0).
Para 4 números, você pode usar o sistema 4-5 cmp (~ 4.6) ou 3-6 cmp (~ 4.9). A classificação por bolha usa 6 cmp (6). Muitos cmp para grandes números de código mais lento.
Este código usa 5 cmp (não classificação 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
fonte
Classifique 4 itens com o uso cmp == 0. O número de cmp é ~ 4,34 (o nativo do FF tem ~ 4,52), mas leva 3x o tempo que as listas mescladas. Mas melhor, menos operações de cmp, se você tiver grandes números ou grandes textos. Edit: bug reparado
Teste online http://mlich.zam.slu.cz/js-sort/x-sort-x2.htm
fonte
Talvez eu esteja atrasado para a festa, mas pelo menos minha contribuição é uma nova abordagem.
swap
fosse maior (em relação ao custo decompare
)SWAP()
dois elementos, os ciclos são perseguidos, necessitando apenas de uma temp e uma troca (register-> register) (new <- old).Atualização: mudou um pouco o código, algumas pessoas usam compiladores C ++ para compilar o código C ...
fonte
wsort6()
função final tem a interface correta.o1..o5
, não há necessidade da segundae[6]
matriz temporária . E: compilar o código C em um compilador C ++ e culpar o código?#include
. Corrigidofonte
Bem, se são apenas 6 elementos e você pode alavancar o paralelismo, deseja minimizar a ramificação condicional etc. Por que você não gera todas as combinações e testa a ordem? Eu arriscaria que, em algumas arquiteturas, pode ser bem rápido (desde que você tenha a memória pré-alocada)
fonte
Aqui estão três métodos típicos de classificação que representam três classes diferentes de algoritmos de classificação:
Mas confira a discussão de Stefan Nelsson sobre o algoritmo de classificação mais rápido? onde ele discute uma solução que desce para
O(n log log n)
.. confira sua implementação em CEste algoritmo de classificação semi-linear foi apresentado por um artigo em 1995:
A. Andersson, T. Hagerup, S. Nilsson e R. Raman. Classificando em tempo linear? Em Anais do 27º Simpósio Anual da ACM sobre a Teoria da Computação, páginas 427-436, 1995.
fonte