Seguindo o interesse nesta pergunta , achei interessante tornar as respostas um pouco mais objetivas e quantitativas ao propor um concurso.
A ideia é simples: eu gerei um arquivo binário contendo 50 milhões de duplicados gaussianos distribuídos (média: 0, stdev 1). O objetivo é criar um programa que os classifique na memória o mais rápido possível. Uma implementação de referência muito simples em python leva 1m4s para ser concluída. Quão baixo nós podemos ir?
As regras são as seguintes: responda com um programa que abra o arquivo "gaussian.dat" e classifique os números na memória (não é necessário produzi-los) e instruções para criar e executar o programa. O programa deve poder funcionar na minha máquina Arch Linux (o que significa que você pode usar qualquer linguagem ou biblioteca de programação que seja facilmente instalável neste sistema).
O programa deve ser razoavelmente legível, para que eu possa ter certeza de que é seguro iniciar (nenhuma solução somente para montadores, por favor!).
Vou executar as respostas na minha máquina (quad core, 4 Gigabytes de RAM). A solução mais rápida receberá a resposta aceita e uma recompensa de 100 pontos :)
O programa usado para gerar os números:
#!/usr/bin/env python
import random
from array import array
from sys import argv
count=int(argv[1])
a=array('d',(random.gauss(0,1) for x in xrange(count)))
f=open("gaussian.dat","wb")
a.tofile(f)
A implementação de referência simples:
#!/usr/bin/env python
from array import array
from sys import argv
count=int(argv[1])
a=array('d')
a.fromfile(open("gaussian.dat"),count)
print "sorting..."
b=sorted(a)
EDIT: apenas 4 GB de RAM, desculpe
EDIÇÃO # 2: Observe que o objetivo do concurso é verificar se podemos usar informações anteriores sobre os dados . não é para ser uma partida irritante entre diferentes implementações de linguagem de programação!
fonte
Respostas:
Aqui está uma solução em C ++ que primeiro particiona os números em intervalos com o mesmo número esperado de elementos e depois classifica cada intervalo separadamente. Ele pré-calcula uma tabela da função de distribuição cumulativa com base em algumas fórmulas da Wikipedia e interpola os valores dessa tabela para obter uma aproximação rápida.
Várias etapas são executadas em vários threads para fazer uso dos quatro núcleos.
Para compilar e executá-lo, use este comando:
EDIT: agora todos os buckets são colocados na mesma matriz para remover a necessidade de copiar os buckets novamente para a matriz. Além disso, o tamanho da tabela com valores pré-computados foi reduzido, porque os valores são precisos o suficiente. Ainda assim, se eu alterar o número de buckets acima de 256, o programa levará mais tempo para ser executado do que com esse número de buckets.
EDIT: O mesmo algoritmo, linguagem de programação diferente. Usei C ++ em vez de Java e o tempo de execução foi reduzido de ~ 3,2s para ~ 2,35s na minha máquina. O número ideal de buckets ainda está em torno de 256 (novamente, no meu computador).
By the way, tbb é realmente incrível.
EDIT: Fui inspirado pela ótima solução de Alexandru e substituí o std :: sort na última fase por uma versão modificada de sua classificação radix. Eu usei um método diferente para lidar com os números positivos / negativos, mesmo que ele precise de mais passagens pela matriz. Também decidi classificar a matriz exatamente e remover a classificação de inserção. Mais tarde, passarei algum tempo testando como essas alterações influenciam o desempenho e, possivelmente, as revertem. No entanto, usando a classificação radix, o tempo diminuiu de ~ 2,35s para ~ 1,63s.
fonte
Sem ser esperto, apenas para fornecer um classificador ingênuo muito mais rápido, aqui está um em C que deve ser praticamente equivalente ao seu Python:
Compilado com
gcc -O3
, na minha máquina, isso leva mais de um minuto a menos que o Python: cerca de 11 s comparado a 87 s.fonte
Particionei em segmentos com base no desvio padrão que melhor deveria ser dividido em 4º. Editar: reescrito para particionar com base no valor x em http://en.wikipedia.org/wiki/Error_function#Table_of_values
http://www.wolframalpha.com/input/?i=percentages+by++normal+distribution
Tentei usar baldes menores, mas parecia ter pouco efeito uma vez 2 * além do número de núcleos disponíveis. Sem coleções paralelas, levaria 37 segundos na minha caixa e 24 nas coleções paralelas. Se particionar via distribuição, você não pode simplesmente usar uma matriz, para que haja mais sobrecarga. Não sei ao certo quando um valor seria colocado na caixa / fora da caixa no scala.
Estou usando o scala 2.9, para a coleção paralela. Você pode simplesmente fazer o download da distribuição tar.gz dele.
Para compilar: scalac SortFile.scala (acabei de copiá-lo diretamente na pasta scala / bin.
Para executar: JAVA_OPTS = "- Xmx4096M" ./scala SortFile (executei-o com 2 GB de RAM e obtive o mesmo tempo)
Editar: Removido assignateDirect, mais lento que alocar. Removido o priming do tamanho inicial para buffers de matriz. Na verdade, fez ler todos os valores 50000000. Reescreva para evitar problemas de autoboxing (ainda mais lento que o ingênuo c)
fonte
Basta colocar isso em um arquivo cs e compilá-lo com o csc na teoria: (Requer mono)
fonte
Como você sabe qual é a distribuição, é possível usar uma classificação O (N) de indexação direta. (Se você está se perguntando o que é isso, suponha que você tenha um baralho de 52 cartas e queira classificá-lo. Basta ter 52 posições e jogar cada carta em sua própria posição.)
Você tem 5e7 duplos. Aloque uma matriz de resultados R de 5e7 duplos. Pegue cada número
x
e peguei = phi(x) * 5e7
. Basicamente façaR[i] = x
. Tenha uma maneira de lidar com colisões, como mover o número com o qual ela pode estar colidindo (como na codificação de hash simples). Como alternativa, você pode tornar R um pouco maior, preenchido com um valor vazio exclusivo . No final, você apenas varre os elementos de R.phi
é apenas a função de distribuição cumulativa gaussiana. Ele converte um número distribuído gaussiano entre +/- infinito em um número distribuído uniforme entre 0 e 1. Uma maneira simples de calculá-lo é com consulta e interpolação de tabela.fonte
Aqui está outra solução seqüencial:
Duvido que seja melhor que a solução multiencadeada, mas os tempos no meu laptop i7 são (stdsort é a solução C ++ fornecida em outra resposta):
Observe que esta solução possui complexidade de tempo linear (porque usa a representação especial de duplas).
EDIT : Corrigida a ordem dos elementos a serem aumentados.
EDIT : Melhor velocidade em quase meio segundo.
EDIT : Melhor velocidade por mais 0,7 segundos. Tornou o algoritmo mais amigável ao cache.
EDIT : Melhor velocidade por mais 1 segundo. Como existem apenas 50.000.000 de elementos, posso classificar parcialmente a mantissa e usar a classificação por inserção (que é compatível com o cache) para corrigir elementos fora do local. Essa idéia remove cerca de duas iterações do último loop de classificação de raiz.
EDIT : 0,16 menos segundos. O primeiro std :: reverse pode ser eliminado se a ordem de classificação for revertida.
fonte
Tomando a solução de Christian Ammer e paralelizando-a com os blocos de construção rosqueados da Intel
Se você tiver acesso à biblioteca IPP (Performance Primitives) da Intel, poderá usar sua classificação radix. Apenas substitua
com
e
com
No meu laptop dual core, os tempos são
fonte
Que tal uma implementação do quicksort paralelo que escolha seus valores de pivô com base nas estatísticas da distribuição, garantindo assim partições de tamanhos iguais? O primeiro pivô estaria na média (zero neste caso), o próximo par estaria nos percentis 25 e 75 (desvios padrão de +/- -0,67449) e assim por diante, com cada partição cortando pela metade o conjunto de dados restante mais ou menos menos perfeitamente.
fonte
Muito feio (por que usar matrizes quando posso usar variáveis que terminam com números), mas código rápido (minha primeira tentativa de std :: threads), tempo inteiro (tempo real) no meu sistema 1,8 s (comparando com std :: sort () 4,8 s), compile com g ++ -std = c ++ 0x -O3 -march = native -pthread Apenas passe os dados pelo stdin (funciona apenas para 50M).
// Edição alterada para ler o arquivo gaussian.dat.
fonte
Uma solução C ++ usando
std::sort
(eventualmente mais rápido que qsort, em relação ao desempenho de qsort vs std :: sort )Não sei dizer quanto tempo leva, porque eu tenho apenas 1 GB na minha máquina e, com o código Python fornecido, eu só conseguia criar um
gaussian.dat
arquivo com apenas 25 milhões de pares (sem obter um erro de memória). Mas estou muito interessado em quanto tempo o algoritmo std :: sort é executado.fonte
sort.h
arquivo para compilá-lo com C ++. Foi duas vezes mais lento questd::sort
. Não sei por que, talvez por causa das otimizações do compilador?Aqui está uma mistura do tipo de raiz de Alexandru com o giro inteligente rosqueado de Zjarek. Compile com
Você pode alterar o tamanho da raiz definindo STEP (por exemplo, adicione -DSTEP = 11). Eu achei o melhor para o meu laptop é 8 (o padrão).
Por padrão, ele divide o problema em quatro partes e o executa em vários segmentos. Você pode mudar isso passando um parâmetro de profundidade para a linha de comando. Portanto, se você tiver dois núcleos, execute-o como
e se você tem 16 núcleos
A profundidade máxima agora é 6 (64 threads). Se você colocar muitos níveis, apenas abrandará o código.
Uma coisa que eu também tentei foi a classificação radix da biblioteca Intel Performance Primitives (IPP). A implementação da Alexandru supera profundamente o IPP, com o IPP sendo cerca de 30% mais lento. Essa variação também está incluída aqui (comentada).
EDIT : Eu implementei as melhorias de cache do Alexandru, e isso diminuiu cerca de 30% do tempo na minha máquina.
EDIT : Isso implementa uma classificação recursiva, portanto deve funcionar bem na máquina de 16 núcleos do Alexandru. Ele também usa o último aprimoramento do Alexandru e remove um dos reversos. Para mim, isso deu uma melhoria de 20%.
EDIT : Corrigido um bug de sinal que causava ineficiência quando há mais de 2 núcleos.
EDIT : Removido o lambda, para que ele compile com versões mais antigas do gcc. Inclui a variação do código IPP comentada. Também corrigi a documentação para rodar em 16 núcleos. Tanto quanto posso dizer, esta é a implementação mais rápida.
EDIT : Corrigido um erro quando STEP não era 8. Aumentado o número máximo de threads para 64. Adicionadas algumas informações de tempo.
fonte
step
(11 foi o ideal no meu laptop).int cnt[mask]
deveria serint cnt[mask + 1]
. Para melhores resultados, use um valor fixoint cnt[1 << 16]
.Eu acho que isso realmente depende do que você quer fazer. Se você quiser classificar um monte de gaussianos, isso não ajudará. Mas se você quiser um monte de gaussianos classificados, isso vai acontecer. Mesmo que isso perca um pouco o problema, acho que será interessante comparar as rotinas reais de classificação.
Se você quiser que algo seja rápido, faça menos.
Em vez de gerar várias amostras aleatórias a partir da distribuição normal e, em seguida, classificá-las, é possível gerar várias amostras a partir da distribuição normal na ordem classificada.
Você pode usar a solução aqui para gerar n números aleatórios uniformes em ordem classificada. Em seguida, você pode usar o cdf inverso (scipy.stats.norm.ppf) da distribuição normal para transformar os números aleatórios uniformes em números da distribuição normal via amostragem por transformação inversa .
Se você quiser sujar as mãos, acho que poderá acelerar os muitos cálculos inversos de cdf usando algum tipo de método iterativo e usando o resultado anterior como seu palpite inicial. Como as suposições serão muito próximas, provavelmente uma única iteração fornecerá grande precisão.
fonte
Experimente esta solução em mudança da Guvante com este Main (), ele começa a classificar assim que a leitura de 1/4 IO é concluída, é mais rápido no meu teste:
fonte
Como você conhece a distribuição, minha idéia seria fazer k buckets, cada um com o mesmo número esperado de elementos (já que você conhece a distribuição, é possível calcular isso). Então, em O (n) tempo, varra a matriz e coloque elementos em seus baldes.
Em seguida, classifique os baldes simultaneamente. Suponha que você tenha k buckets e n elementos. Um balde levará (n / k) lg (n / k) tempo para classificar. Agora, suponha que você tenha processadores p que você pode usar. Como as caçambas podem ser classificadas independentemente, você tem um multiplicador de teto (k / p) para lidar. Isso fornece um tempo de execução final de n + ceil (k / p) * (n / k) lg (n / k), que deve ser muito mais rápido que n lg n se você escolher k bem.
fonte
std::sort()
, mas é bem mais lento que a solução radixsort da Alexandru.Uma idéia de otimização de baixo nível é ajustar duas duplas em um registro SSE, para que cada thread funcione com dois itens por vez. Isso pode ser complicado para alguns algoritmos.
Outra coisa a fazer é classificar a matriz em pedaços compatíveis com o cache e depois mesclar os resultados. Dois níveis devem ser usados: por exemplo, primeiro 4 KB para L1 e 64 KB para L2.
Isso deve ser muito compatível com o cache, pois a classificação do bucket não sai do cache e a mesclagem final percorre a memória sequencialmente.
Atualmente, o cálculo é muito mais barato que o acesso à memória. No entanto, temos um grande número de itens, por isso é difícil dizer qual é o tamanho da matriz quando a classificação com reconhecimento de cache estúpida é mais lenta que uma versão sem reconhecimento de cache de baixa complexidade.
Mas não fornecerei uma implementação do acima, pois o faria no Windows (VC ++).
fonte
Aqui está uma implementação de classificação de balde de verificação linear. Eu acho que é mais rápido do que todas as implementações atuais de thread único, exceto a classificação radix. Deveria ter um tempo de execução linear esperado, se eu estiver estimando o CD com precisão suficiente (estou usando a interpolação linear dos valores que encontrei na Web) e não cometer nenhum erro que possa causar uma verificação excessiva:
fonte
Eu não sei, por que não consigo editar minha postagem anterior, então aqui está a nova versão, 0,2 segundos mais rápida (mas cerca de 1,5 s mais rápida no tempo da CPU (usuário)). Essa solução possui 2 programas, primeiro pré-calcula quantis para distribuição normal para classificação de buckets e armazena-os na tabela t [double * scale] = índice de buckets, em que scale é um número arbitrário que torna possível a conversão para o dobro. Em seguida, o programa principal pode usar esses dados para colocar duplas no balde correto. Ele tem uma desvantagem: se os dados não forem gaussianos, eles não funcionarão corretamente (e também há quase zero chance de funcionar incorretamente para a distribuição normal), mas a modificação para casos especiais é fácil e rápida (apenas o número de verificações de baldes e a queda para std ::ordenar()).
Compilando: g ++ => http://pastebin.com/WG7pZEzH programa auxiliar
g ++ -std = c ++ 0x -O3 -march = native -pthread => http://pastebin.com/T3yzViZP principal programa de classificação
fonte
Aqui está outra solução seqüencial. Este usa o fato de que os elementos são distribuídos normalmente, e acho que a idéia é geralmente aplicável para obter uma classificação próxima ao tempo linear.
O algoritmo é assim:
phi()
função na implementação)size * phi(x)
Infelizmente, a constante oculta é muito grande e esta solução é duas vezes mais lenta que o algoritmo de classificação de raiz.
fonte
Meu favorito pessoal usando os Threaded Building Blocks da Intel já foi publicado, mas aqui está uma solução paralela grosseira usando o JDK 7 e sua nova API de junção / junção:
Isenção de responsabilidade importante : Aceitei a adaptação de classificação rápida para fork / join em: https://github.com/pmbauer/parallel/tree/master/src/main/java/pmbauer/parallel
Para executar isso, você precisa de uma versão beta do JDK 7 (http://jdk7.java.net/download.html).
No meu quad core 2.97Ghz i7 (OS X):
Referência do Python
Java JDK 7 bifurcação / junção
Também tentei fazer algumas experiências com leitura paralela e converter os bytes em duplos, mas não vi nenhuma diferença lá.
Atualizar:
Se alguém quiser experimentar o carregamento paralelo dos dados, a versão do carregamento paralelo está abaixo. Em teoria, isso poderia torná-lo um pouco mais rápido ainda, se o seu dispositivo IO tiver capacidade paralela suficiente (os SSDs costumam ter). Também há alguma sobrecarga na criação de Doubles a partir de bytes, de modo que também poderia ser mais rápido em paralelo. Nos meus sistemas (Ubuntu 10.10 / Nehalem Quad / Intel X25M SSD e OS X 10.6 / i7 Quad / Samsung SSD), não vi nenhuma diferença real.
Update2:
Eu executei o código em uma das nossas 12 máquinas de desenvolvimento com uma pequena modificação para definir uma quantidade fixa de núcleos. Isso deu os seguintes resultados:
Nesse sistema, também tentei a versão Python, que possuía 1m2.994s, e a versão C ++ de Zjarek, que levou 1.925s (por alguma razão, a versão C ++ de Zjarek parece correr relativamente mais rápido no computador do static_rtti).
Eu também tentei o que aconteceu se dobrar o tamanho do arquivo para 100.000.000 duplos:
Nesse caso, a versão C ++ de Zjarek levou 3.968s. O Python demorou muito tempo aqui.
150.000.000 duplos:
Nesse caso, a versão C ++ de Zjarek era 6.044s. Eu nem tentei Python.
A versão C ++ é muito consistente com seus resultados, onde o Java oscila um pouco. Primeiro, fica um pouco mais eficiente quando o problema aumenta, mas depois é menos eficiente novamente.
fonte
Uma versão usando pthreads tradicionais. Código de fusão copiado da resposta de Guvante. Compile com
g++ -O3 -pthread
.No meu laptop, obtenho os seguintes resultados:
fonte
Aqui está uma implementação seqüencial do C99 que tenta realmente fazer uso da distribuição conhecida. Basicamente, ele executa uma única rodada de classificação de balde usando as informações de distribuição, depois algumas rodadas de classificação rápida em cada intervalo, assumindo uma distribuição uniforme dentro dos limites do balde e, finalmente, uma classificação de seleção modificada para copiar os dados de volta ao buffer original. O quicksort memoriza os pontos de divisão, portanto, a classificação por seleção precisa apenas operar em pequenos baús. E apesar (por quê?) De toda essa complexidade, nem é muito rápido.
Para acelerar a avaliação, os valores são amostrados em alguns pontos e, posteriormente, somente a interpolação linear é usada. Na verdade, não importa se Φ é avaliado exatamente, desde que a aproximação seja estritamente monotônica.
Os tamanhos dos compartimentos são escolhidos de forma que a chance de transbordamento seja desprezível. Mais precisamente, com os parâmetros atuais, a chance de um conjunto de dados de 50000000 elementos causar um estouro de caixa é 3,65e-09. (Isso pode ser calculado usando a função de sobrevivência da distribuição de Poisson .)
Para compilar, use
Como há consideravelmente mais computação do que nas outras soluções, esses sinalizadores do compilador são necessários para torná-lo pelo menos razoavelmente rápido. Sem que
-msse3
as conversõesdouble
seint
tornem realmente lentas. Se sua arquitetura não suportar SSE3, essas conversões também poderão ser feitas usando alrint()
funçãoO código é bastante feio - não tenho certeza se isso atende ao requisito de ser "razoavelmente legível" ...
fonte
Isso usa erf () para colocar cada elemento adequadamente em uma posição e depois classifica cada posição. Mantém a matriz totalmente no local.
Primeira passagem: docensus () conta o número de elementos em cada posição.
Segunda passagem: partition () permite a matriz, colocando cada elemento em sua bandeja apropriada
Terceira passagem: sortbins () executa um qsort em cada posição.
É meio ingênuo e chama a função erf () cara duas vezes para cada valor. O primeiro e o terceiro passes são potencialmente paralelos. O segundo é altamente serial e provavelmente é desacelerado por seus padrões de acesso à memória altamente aleatórios. Também pode valer a pena armazenar em cache o número de cada compartimento duplo, dependendo da taxa de velocidade da CPU e da velocidade da memória.
Este programa permite escolher o número de posições a serem usadas. Basta adicionar um segundo número à linha de comando. Eu o compilei com gcc -O3, mas minha máquina é tão fraca que não posso contar nenhum bom número de desempenho.
Editar: Poof! Meu programa C se transformou magicamente em um programa C ++ usando std :: sort!
fonte
Dê uma olhada na implementação de classificação radix por Michael Herf ( Radix Tricks ). Na minha máquina, a classificação foi 5 vezes mais rápida em comparação com o
std::sort
algoritmo da minha primeira resposta. O nome da função de classificação éRadixSort11
.fonte