Agora que todos desenvolveram sua (muitas vezes incrível) experiência em codificação de baixo nível para Quão lento é realmente o Python? (Ou qual a velocidade da sua linguagem?) E Qual a velocidade do Python (parte II)? é hora de um desafio que também aumentará sua capacidade de melhorar um algoritmo.
O código a seguir calcula uma lista de comprimento 9. A posição i
na lista conta o número de vezes que i
foram encontrados pelo menos zeros consecutivos ao calcular os produtos internos entre F
e S
. Para fazer isso exatamente, ele itera sobre todas as listas possíveis F
de comprimento n
e listas S
de comprimento n+m-1
.
#!/usr/bin/python
import itertools
import operator
n=8
m=n+1
leadingzerocounts = [0]*m
for S in itertools.product([-1,1], repeat = n+m-1):
for F in itertools.product([-1,1], repeat = n):
i = 0
while (i<m and sum(map(operator.mul, F, S[i:i+n])) == 0):
leadingzerocounts[i] +=1
i+=1
print leadingzerocounts
A saída é
[4587520, 1254400, 347648, 95488, 27264, 9536, 4512, 2128, 1064]
Se você aumentar n para 10,12,14,16,18,20 usando esse código, muito rapidamente se tornará muito lento.
Regras
- O desafio é fornecer a saída correta para o maior número possível de n. Apenas valores pares de n são relevantes.
- Se houver um empate, a vitória vai para o código mais rápido da minha máquina para o maior n.
- Reservo-me o direito de não testar o código que leva mais de 10 minutos.
- Você pode alterar o algoritmo da maneira que desejar, desde que ele produza a saída correta. Na verdade, você terá que alterar o algoritmo para fazer um progresso decente em direção à vitória.
- O vencedor será premiado uma semana a partir do momento em que a pergunta foi feita.
- A recompensa será concedida no vencimento, um pouco depois do vencimento do vencedor.
Minha máquina Os horários serão executados na minha máquina. Esta é uma instalação padrão do ubuntu em um processador AMD FX-8350 de oito núcleos. Isso também significa que eu preciso poder executar seu código. Como conseqüência, use apenas software livre facilmente disponível e inclua instruções completas sobre como compilar e executar seu código.
Status .
- C . n = 12 em 49 segundos por @Fors
- Java . n = 16 em 3:07 por @PeterTaylor
- C ++ . n = 16 em 2:21 por @ilmale
- Rpython . n = 22 em 3:11 por @primo
- Java . n = 22 em 6:56 por @PeterTaylor
- Nimrod . n = 24 em 9:28 segundos por @ReimerBehrends
O vencedor foi Reimer Behrends com uma entrada em Nimrod!
Como verificação, a saída para n = 22 deve ser [12410090985684467712, 2087229562810269696, 351473149499408384, 59178309967151104, 9975110458933248, 1682628717576192, 284866824372224, 48558946385920, 8416739196928, 1518499004416, 301448822784, 71620493312, 22100246528, 8676573184, 3897278464, 1860960256, 911646720, 451520512, 224785920, 112198656, 56062720, 28031360, 14015680]
.
A competição terminou agora ... Oferecerei 200 pontos por cada envio que aumentar n em 2 (dentro de 10 minutos no meu computador) até que eu fique sem pontos. Esta oferta está aberta para sempre .
fonte
Respostas:
Nimrod (N = 22)
Ajuntar com
(O Nimrod pode ser baixado aqui .)
Isso é executado no tempo alocado para n = 20 (e para n = 18 ao usar apenas um único encadeamento, levando cerca de 2 minutos no último caso).
O algoritmo usa uma pesquisa recursiva, removendo a árvore de pesquisa sempre que um produto interno diferente de zero é encontrado. Também cortamos o espaço de pesquisa pela metade, observando que, para qualquer par de vetores
(F, -F)
, precisamos considerar apenas um porque o outro produz exatamente os mesmos conjuntos de produtos internos (negandoS
também).A implementação usa os recursos de metaprogramação do Nimrod para desenrolar / alinhar os primeiros níveis da pesquisa recursiva. Isso economiza um pouco de tempo ao usar o gcc 4.8 e 4.9 como o back-end do Nimrod e uma quantidade razoável de clang.
O espaço de pesquisa poderia ser ainda mais podado, observando que só precisamos considerar valores de S que diferem em um número par das primeiras posições N da escolha de F. de N, dado que o corpo do loop é completamente ignorado nesses casos.
Tabular onde o produto interno é zero parece ser mais rápido do que usar qualquer funcionalidade de contagem de bits no loop. Aparentemente, acessar a tabela tem uma boa localidade.
Parece que o problema deve ser passível de programação dinâmica, considerando como a pesquisa recursiva funciona, mas não há uma maneira aparente de fazer isso com uma quantidade razoável de memória.
Exemplo de saídas:
N = 16:
N = 18:
N = 20:
Para fins de comparação do algoritmo com outras implementações, N = 16 leva cerca de 7,9 segundos na minha máquina ao usar um único encadeamento e 2,3 segundos ao usar quatro núcleos.
N = 22 leva cerca de 15 minutos em uma máquina de 64 núcleos com o gcc 4.4.6, como o back-end do Nimrod e transborda números inteiros de 64 bits
leadingZeros[0]
(possivelmente os não assinados, que ainda não viram).Atualização: Encontrei espaço para mais algumas melhorias. Primeiro, para um determinado valor de
F
, podemos enumerar as 16 primeiras entradas dosS
vetores correspondentes com precisão, porque elas devem diferir exatamente emN/2
locais. Portanto, pré-calculamos uma lista de vetores de bits de tamanhoN
comN/2
bits definidos e os usamos para derivar a parte inicial deS
fromF
.Segundo, podemos melhorar a pesquisa recursiva, observando que sempre sabemos o valor de
F[N]
(como o MSB é zero na representação de bits). Isso nos permite prever com precisão em qual ramo recorreremos do produto interno. Embora isso nos permita transformar toda a pesquisa em um loop recursivo, isso acaba atrapalhando bastante a previsão do ramo, por isso mantemos os níveis mais altos em sua forma original. Ainda economizamos algum tempo, principalmente reduzindo a quantidade de ramificações que estamos realizando.Para alguma limpeza, o código agora está usando números inteiros não assinados e os corrige em 64 bits (caso alguém queira executar isso em uma arquitetura de 32 bits).
A aceleração geral está entre um fator de x3 e x4. N = 22 ainda precisa de mais de oito núcleos para rodar em menos de 10 minutos, mas em uma máquina de 64 núcleos agora é de cerca de quatro minutos (com
numThreads
aumento de acordo). Não acho que haja muito mais espaço para melhorias sem um algoritmo diferente.N = 22:
Atualizado novamente, fazendo uso de outras reduções possíveis no espaço de pesquisa. É executado em cerca de 9:49 minutos para N = 22 na minha máquina quadcore.
Atualização final (eu acho). Melhores classes de equivalência para opções de F, reduzindo o tempo de execução de N = 22 para
3:19 minutos e57 segundos (editar: eu acidentalmente executei isso com apenas um thread) na minha máquina.Essa alteração utiliza o fato de que um par de vetores produz os mesmos zeros à esquerda se um puder ser transformado no outro girando-o. Infelizmente, uma otimização de baixo nível bastante crítica exige que o bit superior de F na representação de bits seja sempre o mesmo e, ao usar essa equivalência, reduziu bastante o espaço de pesquisa e reduziu o tempo de execução em cerca de um quarto, usando um espaço de estado diferente redução em F, a sobrecarga de eliminar mais a otimização de baixo nível do que compensá-la. No entanto, verifica-se que esse problema pode ser eliminado considerando-se também o fato de que F que são inversos um do outro também são equivalentes. Embora isso tenha aumentado um pouco a complexidade do cálculo das classes de equivalência, ele também me permitiu manter a otimização de baixo nível mencionada, levando a uma aceleração de cerca de x3.
Mais uma atualização para oferecer suporte a números inteiros de 128 bits para os dados acumulados. Para compilar com 128 bit inteiros, você vai precisar
longint.nim
de aqui e para compilar com-d:use128bit
. N = 24 ainda leva mais de 10 minutos, mas incluí o resultado abaixo para os interessados.N = 24:
fonte
Java (
n=22
?)Penso que a maioria das respostas é melhor do que
n=16
usar uma abordagem semelhante a essa, embora elas diferem nas simetrias que exploram e na maneira como dividem a tarefa entre os threads.Os vetores definidos na pergunta podem ser substituídos por cadeias de bits, e o produto interno com XOR na janela sobreposta e verificando se há exatamente os
n/2
bits definidos (e, portanto, osn/2
bits limpos). Existemn! / ((n/2)!)
(coeficiente binomial central) cadeias den
bits comn/2
bits configurados (que eu chamo de cadeias balanceadas ); portanto, para qualquer dado dado,F
existem muitas janelasS
que dão um produto interno nulo. Além disso, a ação de deslizarS
ao longo de uma e verificar se ainda podemos encontrar um bit de entrada que dê um produto interno zero corresponde a procurar uma aresta em um gráfico cujos nós são as janelas e cujas arestas vinculam um nóu
a um nóv
cujos primeirosn-1
bits são os últimosn-1
pedaços deu
.Por exemplo, com
n=6
eF=001001
obtemos este gráfico:e para
F=001011
obtermos este gráfico:Então precisamos contar para cada
i
a partir0
den
quantos caminhos de comprimentoi
há, somando sobre os gráficos para cadaF
. Eu acho que a maioria de nós está usando a pesquisa profunda.Observe que os gráficos são escassos: é fácil provar que cada nó possui um grau máximo de 1 e um grau máximo de um. Isso também significa que as únicas estruturas possíveis são cadeias simples e loops simples. Isso simplifica um pouco o DFS.
Exploro algumas simetrias: as seqüências balanceadas são fechadas sob bits inversos (a
~
operação em vários idiomas da família ALGOL) e sob rotação de bits, para que possamos agrupar valoresF
relacionados por essas operações e fazer apenas o DFS uma vez.No meu Core 2 de 2,5 GHz, recebo
Como o computador de Lembik possui 8 núcleos e executou meu programa de thread único anterior duas vezes mais rápido que o meu, estou otimista de que seja executado
n=22
em menos de 8 minutos.fonte
C
É basicamente apenas uma implementação levemente otimizada do algoritmo na questão. Ele pode gerenciar
n=12
dentro do prazo.Execução de teste para
n=12
, incluindo compilação:Comentário: Acabei de ligar meu cérebro e usei algumas combinatórias simples para calcular que o primeiro valor sempre será
n! / ((n / 2)!)^2 * 2^(n + m - 1)
. Parece-me que deve haver uma solução completamente algébrica para esse problema.fonte
Java,
n=16
Para qualquer valor dado de,
F
existem\binom{n}{n/2}
vetores que possuem um produto interno nulo. Assim, podemos construir um gráfico cujos vértices são os vetores correspondentes e cujas bordas correspondem à mudança deS
, e então precisamos contar os caminhos de comprimento atén
o gráfico.Eu não tentei microoptimizar isso substituindo condicionais por operações bit a bit, mas cada incremento duplo de
n
aumenta o tempo de execução em cerca de 16 vezes, para que isso não faça diferença suficiente, a menos que eu esteja bem próximo do limite. Na minha máquina, não estou.No meu Core 2 de 2,5 GHz, recebo
fonte
f
e vértices iniciais, itere sobre todosf_xor_g
com osn/2
bits definidos exatamente . Para cada um deles, itere sobre todosf
e tomeg = f ^ f_xor_g
.RPython, N = 22 ~ 3: 23
Multiencadeado, usando uma descida recursiva sem pilha. O programa aceita dois argumentos de linha de comando: N e o número de threads de trabalho.
Compilar
Crie um clone local do repositório PyPy usando mercurial, git ou o que você preferir. Digite o seguinte encantamento (assumindo que o script acima tenha o nome
convolution-high.py
):onde
%PYPY_REPO%
representa uma variável de ambiente apontando para o repositório que você acabou de clonar. A compilação leva cerca de um minuto.Exemplo de tempos
N = 16, 4 threads:
N = 18, 4 threads:
N = 20, 4 threads:
N = 22, 4 threads:
fonte
Python 3.3, N = 20, 3.5min
Isenção de responsabilidade: minha intenção não é postar isso como minha própria resposta, pois o algoritmo que estou usando é apenas uma porta sem vergonha da solução RPython do primo . Meu objetivo aqui é apenas mostrar o que você pode fazer em Python se você combinar a mágica dos módulos Numpy e Numba .
Numba explicou resumidamente:
Atualização 1 : notei que, depois de lançar os números, podemos simplesmente pular alguns deles completamente. Então agora maxf torna-se (1 << n) // 2 e maxs torna-se maxf 2 **. Isso irá acelerar bastante o processo. n = 16 agora leva apenas ~ 48s (abaixo de 4,5min). Eu também tenho uma outra idéia que vou tentar e ver se consigo fazê-lo um pouco mais rápido.
Atualização 2: algoritmo alterado (solução do primo). Embora minha porta ainda não suporte multithreading, é bastante simples de adicionar. É ainda possível liberar o CPython GIL usando Numba e ctypes. Esta solução, no entanto, também é executada muito rapidamente em núcleo único!
E finalmente:
Isso funciona na minha máquina em 212688ms ou ~ 3,5min.
fonte
C ++ N = 16
Estou testando em um EEEPC com um átomo .. meu tempo não faz muito sentido. : D
O átomo resolve n = 14 em 34 segundos. E n = 16 em 20 minutos. Quero testar n = 16 no OP pc. Eu sou otimista.
A idéia é que toda vez que encontramos uma solução para um dado F, encontramos uma solução 2 ^ i porque podemos alterar a parte inferior de S, levando ao mesmo resultado.
compilar:
gcc 26459.cpp -std = c ++ 11 -O3 -march = nativo-restrito-alias--ftree-vectorize -Wall -pedantic -o 26459
fonte
JAVASCRIPT n: 12
No meu computador, foram necessários 231,242 segundos. Na demonstração, estou usando trabalhadores da web para evitar o congelamento do navegador. Isso com certeza pode ser melhorado com trabalhadores paralelos. Eu sei que JS não tem chance nesse desafio, mas eu fiz isso por diversão!
Clique para executar a demonstração online
fonte
n=22
[235388928,86292480,19031048,5020640,1657928,783920,545408,481256,463832,460256,459744,459744,459744,459744,459744,459744,459744,459744,459744,459744,459744,459744]
i.imgur.com/FIJa2Ch.pngFortran: n = 12
Acabei de fazer uma versão rápida no Fortran, sem otimizações, exceto o OpenMP. Ele deve se espremer em pouco menos de 10 minutos para n = 12 na máquina OP, leva 10:39 na minha máquina, que é levemente mais lenta.
Inteiros de 64 bits têm um impacto negativo no desempenho; acho que eu teria que repensar todo o algoritmo para que isso seja muito mais rápido. Não sei se vou me incomodar, acho que prefiro passar algum tempo pensando em um bom desafio que seja mais do meu próprio gosto. Se alguém quiser pegar isso e continuar com isso, vá em frente :)
fonte
Lua: n = 16
Isenção de responsabilidade: minha intenção NÃO é postar isso como minha própria resposta, pois o algoritmo que estou usando é descaradamente roubado da resposta inteligente de Anna Jokela . que foi descaradamente roubado da resposta inteligente de ilmale .
Além disso, nem é válido - possui imprecisões causadas por números de ponto flutuante (seria melhor se Lua suportasse números inteiros de 64 bits). No entanto, ainda estou carregando, apenas para mostrar a rapidez com que esta solução é. É uma linguagem de programação dinâmica, e ainda assim eu posso calcular n = 16 em tempo razoável (1 minuto na CPU de 800 MHz).
Execute com LuaJIT, o intérprete padrão está muito lento.
fonte
long long
vez dedouble
com uma configuração de compilação), não LuaJIT.