Quão lento é realmente o Python (parte II)?


52

Este é um acompanhamento de Quão lento é realmente o Python? (Ou quão rápido é o seu idioma?) .

Aconteceu que foi um pouco fácil obter uma aceleração de x100 para minha última pergunta. Para aqueles que gostaram do desafio, mas querem algo mais difícil, onde realmente podem usar suas habilidades de baixo nível, aqui está a parte II. O desafio é obter uma aceleração de x100 para o seguinte código python, testado no meu computador.

Para tornar mais difícil, estou usando o pypy neste momento. O tempo atual para mim é de 1 minuto e 7 segundos usando o pypy 2.2.1.

Regras

  1. A primeira pessoa a enviar o código que eu posso executar, está correta e é 100 vezes mais rápida na minha máquina, receberá uma recompensa de 50 pontos.
  2. Atribuirei a vitória ao código mais rápido depois de uma semana.
import itertools 
import operator 
import random

n = 8 
m  = 8 
iters = 1000  

# creates an array of 0s with length m
# [0, 0, 0, 0, 0, 0, 0, 0]
leadingzerocounts = [0]*m

# itertools.product creates an array of all possible combinations of the 
# args passed to it.
#
# Ex:
#   itertools.product("ABCD", "xy") --> Ax Ay Bx By Cx Cy Dx Dy
#   itertools.product("AB", repeat=5) --> [
#    ('A', 'A', 'A', 'A', 'A'),
#    ('A', 'A', 'A', 'A', 'B'),
#    ('A', 'A', 'A', 'B', 'A'),
#    ('A', 'A', 'A', 'B', 'B'),
#    etc.
#   ]
for S in itertools.product([-1,1], repeat = n+m-1):
    for i in xrange(iters):
        F = [random.choice([-1,0,0,1]) for j in xrange(n)]

        # if the array is made up of only zeros keep recreating it until
        # there is at least one nonzero value.
        while not any(F):
            F = [random.choice([-1,0,0,1]) for j in xrange(n)]

        j = 0
        while (j < m and sum(map(operator.mul, F, S[j:j+n])) == 0):
            leadingzerocounts[j] +=1
            j += 1
print leadingzerocounts

A saída deve ser semelhante a

[6335185, 2526840, 1041967, 439735, 193391, 87083, 40635, 19694]

Você deve usar uma semente aleatória no seu código e qualquer gerador de números aleatórios que seja bom o suficiente para fornecer respostas próximas ao acima será aceito.

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.

Explicação do código

Esse código itera sobre todas as matrizes S de comprimento n + m-1 compostas por -1s e 1s. Para cada matriz S, são amostradas 1000 matrizes aleatórias diferentes de zero, F, de comprimento n, compostas de -1,0 ou 1, com probabilidade de 1/4, 1/2 / / 14 de obter cada valor. Ele calcula os produtos internos entre F e cada janela de S de comprimento n até encontrar um produto interno diferente de zero. Ele adiciona 1 a leadingzerocountscada posição em que encontrou um produto interno zero.

Status

  • Perl . 2,7 vezes mais lento por @tobyink. (Comparado com pypy, não cpython.)

  • J . 39 vezes acelerado por @Eelvex.

  • C . 59 vezes mais rápido por @ace.
  • Julia . 197 vezes mais rápido, sem incluir o tempo de inicialização por mais um minuto. Acelerar 8,5 vezes, incluindo o tempo de inicialização (neste caso, é mais rápido usar 4 processadores do que 8).
  • Fortran . 438 vezes mais rápido por @ semi-extrínseco.
  • Rpython . 258 vezes mais rápido por @primo.
  • C ++ . 508 vezes mais rápido por @ilmale.

(Parei de cronometrar as novas melhorias porque elas são muito rápidas e o iter era muito pequeno.)


Observou-se que os intervalos abaixo de um segundo não são confiáveis ​​e também alguns idiomas têm um custo inicial. O argumento é que, se você incluir, inclua também o tempo de compilação do C / C ++ etc. Aqui estão os horários para o código mais rápido, com o número de iterações aumentado para 100.000.

  • Julia . 42 segundos por mais um minuto.
  • C ++ . 14 segundos por @GuySirton.
  • Fortran . 14s por @ semi-extrínseco.
  • C ++ . 12s por @ilmale.
  • Rpython . 18s por @primo.
  • C ++ . 5s por @Stefan.

O vencedor é .. Stefan!

Desafio de acompanhamento publicado. Quão alto você pode ir? (Um desafio de codificação + algoritmos) . Este é mais difícil.


3
uma explicação sobre o que o código é suposto alcançar seria bom, assim podemos reescrevê-lo e não simplesmente porta-lo
Einacio

6
" A primeira pessoa a enviar o código que eu posso executar, está correta e é 100 vezes mais rápida na minha máquina vence imediatamente e a competição é encerrada. " Qual é o objetivo de encerrar a competição assim? Por que não usar um prazo de data como a maioria dos outros, para que possamos vê-lo reduzido em outros idiomas?
grovesNL

5
@Einacio Essa é uma boa ideia. Mudei as regras das quais espero que ninguém se importe.

11
@Lembik Melhorei minha versão do Fortran, tornando-a 2x mais rápida novamente na minha máquina. Você poderia cronometrar de novo? :)
semi-extrínseco

11
@ semi-extrínseco Concluído.

Respostas:


13

C ++ bit magic

~ 16ms multithread, 56ms singlethreaded. ~ 4000 aceleração.

(a aceleração é baseada no código multithread no meu i7-2820QM e nos 1 min e 9 segundos mencionados na pergunta. Como o sistema do OP tem pior desempenho de thread único do que minha CPU, mas melhor desempenho multithread, espero que esse número seja preciso)

A peça multithread é bastante ineficiente devido à geração de threads. Provavelmente, eu poderia fazer melhor aproveitando minha biblioteca de tarefas personalizada, mas essa possui bugs nos sistemas unix. Para obter uma explicação e código quase idêntico sem o encadeamento, consulte https://codegolf.stackexchange.com/a/26485/20965 .

editar

Eu dei a cada thread o seu próprio RNG e reduzi o tamanho do bit para 32, o que reduziu o tempo de execução em alguns ms.

#include <iostream>
#include <bitset>
#include <random>
#include <chrono>
#include <stdint.h>
#include <cassert>
#include <array>
#include <tuple>
#include <memory>
#include <thread>
#include <future>
#include <string.h>


#ifdef _MSC_VER
uint32_t popcnt( uint32_t x ){ return _mm_popcnt_u32(x); }
#else
uint32_t popcnt( uint32_t x ){ return __builtin_popcount(x); }
#endif



void convolve()
{
    static const unsigned threadCount = 32;
    static const unsigned n = 8;
    static const unsigned m = 8;
    static const unsigned totalIters = 1000;
    static_assert( n <= 16, "packing of F fails when n > 16.");
    static uint32_t fmask = (1 << n) -1; fmask |= fmask << 16;

    std::array< uint32_t, m * threadCount > out;
    std::vector< std::future<void> > threads;

    for( int threadId = 0; threadId < threadCount; threadId++)
    {
        threads.emplace_back( std::async( [&, threadId]
        {
            std::random_device rd;
            std::knuth_b gen(rd());
            uint32_t nextRandomNumber = gen();

            const unsigned iters = totalIters / threadCount;

            std::array< uint32_t, m > leadingZeros;
            for( auto& x : leadingZeros )
                x = 0;

            for( unsigned i = 0; i < iters; i++ )
            {
                // generate random bit mess
                uint32_t F;
                do {
                    // this funky looking construction shortens the dependancy chain of F
                    F = nextRandomNumber & fmask;
                    nextRandomNumber = gen();
                } while ( 0 == ((F % (1 << n)) ^ (F >> 16 )) );

                // Assume F is an array with interleaved elements such that F[0] || F[16] is one element
                // here MSB(F) & ~LSB(F) returns 1 for all elements that are positive
                // and  ~MSB(F) & LSB(F) returns 1 for all elements that are negative
                // this results in the distribution ( -1, 0, 0, 1 )
                // to ease calculations we generate r = LSB(F) and l = MSB(F)

                uint32_t r = F % ( 1 << n );
                // modulo is required because the behaviour of the leftmost bit is implementation defined
                uint32_t l = ( F >> 16 ) % ( 1 << n );

                uint32_t posBits = l & ~r;
                uint32_t negBits = ~l & r;
                assert( (posBits & negBits) == 0 );

                uint32_t mask = posBits | negBits;
                uint32_t totalBits = popcnt( mask );
                // if the amount of -1 and +1's is uneven, sum(S*F) cannot possibly evaluate to 0
                if ( totalBits & 1 )
                    continue;

                uint32_t adjF = posBits & ~negBits;
                uint32_t desiredBits = totalBits / 2;

                uint32_t S = (1 << (n + m -1));
                // generate all possible N+1 bit strings
                // 1 = +1
                // 0 = -1
                while ( S-- )
                {
                    for( int shift = 0; shift < m; shift++ )
                    {
                        uint32_t s = (S >> shift) % ( 1 << n );
                        auto firstBits = (s & mask) ^ adjF;

                        if ( desiredBits == popcnt( firstBits ) )
                        {
                            leadingZeros[shift] = leadingZeros[shift] + 1;
                        }
                        else
                        {
                            break;
                        }
                    }
                }
            }

            memcpy( out.data() + (threadId * m), leadingZeros.data(), sizeof( leadingZeros[0] ) * m );
        } ));

    };

    std::array< uint32_t, m > leadingZeros;
    for( auto& x : leadingZeros )
        x = 0;

    for( auto& thread : threads )
    {
        thread.wait();
    }

    for( int i = 0; i < (threadCount * m); i++ )
    {
        leadingZeros[i % m] += out[i];
    }


    for( auto x : leadingZeros )
        std::cout << x << ", ";

    std::cout << std::endl;
}

int main()
{
    typedef std::chrono::high_resolution_clock clock;
    int rounds = 100;

    // do some rounds to get the cpu up to speed..
    for( int i = 0; i < rounds / 10; i++ )
    {
        convolve();
    }


    auto start = clock::now();

    for( int i = 0; i < rounds; i++ )
        convolve();

    auto end = clock::now();
    double seconds = std::chrono::duration_cast< std::chrono::microseconds >( end - start ).count() / 1000000.0;

    std::cout << seconds/rounds*1000 << " msec/round" << std::endl;

    return 0;
}

Saída de amostra:

   6317312, 2515072, 1034368, 434048, 190144, 85200, 39804, 19168,
   6226944, 2481408, 1031168, 438080, 192896, 86816, 40484, 19490,
   6321152, 2524672, 1045376, 442880, 195680, 88464, 41656, 20212,
   6330624, 2517504, 1031104, 430208, 187696, 83976, 38976, 18708,
   6304768, 2510336, 1030720, 433056, 190880, 86824, 40940, 19840,
   6272512, 2494720, 1028160, 432352, 189168, 84752, 39540, 19052,
   6233600, 2507520, 1046912, 447008, 198224, 89984, 42092, 20292,

A saída não está correta, eu acho, talvez haja um bug? Compare com o que está em questão. Em particular a última coluna deve ser um número perto de 19180.

@Embembik eu posso ver o que você quer dizer. Eu acho que a saída aleatória não é aleatória o suficiente, o que cria uma saída descolada às vezes. Com o gerador aleatório C ++ 11, ele funciona bem. Corrigirei o código hoje mais tarde.
Stefan

Eu recebo Stefan.cpp: 104: 101: erro: 'memcpy' não foi declarado neste escopo memcpy (out.data () + (threadId * m), LeadZeros.data (), sizeof (LeadZeros [0]) * m );

Eu acho que preciso incluir string.h. Tente de novo.
Stefan

Você compilar este com g ++ O3 -std = c ++ 0x -pthread -Wl, - não-conforme a necessidade Stefan.cpp -o Stefan

16

C ++ x150 x450 x530

Em vez de array, usei bits (e magia negra).
Obrigado @ace pela função aleatória mais rápida.

Como funciona: os primeiros 15 bits do número inteiro srepresentam a matriz S[15]; os zeros representam -1, os que representam +1. A matriz Fé construída de maneira semelhante. Mas com dois bits para cada símbolo.

  • 00 representam -1
  • 01 e 10 representam 0
  • 11 representam 1

Causar Se Fter uma representação diferente. Tenho que intercalar Sconsigo mesmo para ser comparável F.

  • 0 (-1) passou a 00 (-1 na representação de F)
  • 1 (+1) passou a 11 (+1 na representação de F)

Agora podemos simplesmente usar o Carnot para calcular o produto interno. Lembre-se de que uma variável pode assumir apenas o valor 00 ou 11

0 00 = 11 (-1 * -1 = +1)
0. 01 = 10 (-1 * 0 = 0)
0. 10 = 01 (-1 * 0 = 0)
0. 11 = 00 (-1 * +1 = -1)
1. 00 = 00 (+1 * -1 = -1)
1. 10 = 10 (+1 * 0 = 0)
1. 01 = 01 (+1 * 0 = 0)
1. 11 = 11 (+1 * +1 = +1)

Parece um não xor para mim. :)

Resumindo, esses são apenas um jogo de mudança e máscara, nada realmente complexo.

#include <array>
#include <ctime>

// From standford bithacks
// http://graphics.stanford.edu/~seander/bithacks.html
inline int32_t interleaveBit(int32_t x)
{
   static const uint32_t B[] = { 0x55555555, 0x33333333, 0x0F0F0F0F, 0x00FF00FF };
   x = (x | ( x << 8)) & B[3];
   x = (x | ( x << 4)) & B[2];
   x = (x | ( x << 2)) & B[1];
   x = (x | ( x << 1)) & B[0];
   return x | (x << 1);
}

inline int32_t sumOnes(int32_t v)
{
   static int b[] = { 1, 0, 0, 1};
   int s = 0;
   for( int i = 0; i < 8; ++i)
   {
      const int a = 3&(v>>(i*2));
      s += b[a];
   }
   return s;
}

inline int32_t sumArray(int32_t v)
{
   static int b[] = { -1, 0, 0, 1};
   int s = 0;
   for( int i = 0; i < 8; ++i)
   {
      const int a = 3&(v>>(i*2));
      s += b[a];
   }
   return s;
}

uint32_t x, y = 24252, z=57768, w=1564; //PRNG seeds

int32_t myRand()
{
   uint32_t t;
   t = x ^ (x<<1);
   x = y;
   y = z;
   z = w;
   w = w ^ ( w >> 19) ^ t ^ (t >> 8);
   return w;
}

int main()
{
   std::array<int, 8> leadingZero{0};
   x = static_cast<int32_t>(time(nullptr)); // seed PRNG
   const int maxS = 1 << 15;
   for(int s = 0; s < maxS; ++s)
   {
      const int32_t x = interleaveBit(s);
      for(int i = 0; i < 1000; ++i)
      {
         int32_t random;
         do
         {
            random = 0xFFFF & myRand();
         }while(sumOnes(random) == 0);
         int j = 7;
         while( j >= 0 )
         {
            const int32_t h = (x >> (j*2));
            const int32_t l = 0xFFFF & (~(random ^ h)); // inner product
            if(sumArray(l) == 0)
            {
               leadingZero[j]++;
            } else
            {
               break;
            }
            j--;
         }

      }
   }
   for(int i = 7; i >= 0; --i)
   {
      printf("%d ", leadingZero[i]);
   }
   printf("\n");
   return 0;
}

Aqui está um exemplo de saída:

6332350 2525218 1041716 438741 192917 87159 41023 19908 

real 0m0.372s
user 0m0.371s
sys  0m0.001s

O programa foi compilado com:

gcc -std=c++11 -O3 -msse4.2 -Wall -lstdc++ 26371.cpp -o fastPy

no Fedora 20 com gcc 4.8.2 O processador é um i7 8core.

Provavelmente eu posso obter alguns parâmetros de compilação do ms.

Embora este seja o tempo da solução OP na minha máquina:

time pypy 26371.py
[6330609, 2523914, 1040885, 439303, 192708, 86987, 40710, 19498]

real 0m53.061s
user 0m53.016s
sys  0m0.022s

Editar:

Apenas adicionando o openmp e altere a ordem dos, pois tenho um ganho de x3, levando a uma melhoria de desempenho do x450 em relação ao código OP. : D Nesse caso, o leadingZeroarray deve ser atômico. O aleatório global ... são aleatórios, eles serão mais aleatórios.

 #pragma omp parallel for
 for(int i = 0; i < 1000; ++i)
 {
    int32_t random;
    do
    {
       random = 0xFFFF & myRand();
    }while(sumOnes(random) == 0);
    for(int s = 0; s < maxS; ++s)
    {
       const int32_t x = interleaveBit(s);
       int j = 7;
       while( j >= 0 )
       {
          const int32_t h = (x >> (j*2));
          const int32_t l = 0xFFFF & (~(random ^ h)); // inner product
          if( sumArray(l) == 0 )
          {
             leadingZero[j]++;
          } else
          {
             break;
          }
          j--;
       }
    }
 }

precisa adicionar -fopenmpao sinalizador do compilador


Edit: 2 Como sugerido pelo user71404 eu mudei as funções sumOnes e sumArray e agora é super rápido.

real  0m0.101s
user  0m0.101s
sys   0m0.000s

Com o openmp é mais lento, os átomos acrescentam muita sobrecarga.

real  0m0.253s
user  0m1.870s
sys   0m0.001s

Sem atômica é ainda mais rápido, mas obtenho resultado errado.

2137992 1147218 619297 321243 155815 70946 32919 15579

real   0m0.048s
user   0m0.338s
sys    0m0.001s

Para entender o sumArray, considere que 16 bits representam e um array de 8 números.
00 não tem 1 e representa -1
01 e 10 tem um 1 e representa 0
11 tem dois 1s e representa 1
Portanto, a contagem interna do número de bits definido como 1 [ http://en.wikipedia.org/wiki/ Hamming_weight] e para cada grupo removemos 1. Cool.

sumOnes é apenas magia negra.

Aqui estão os últimos sinalizadores e códigos de compilação.

gcc -std = c ++ 11 -mfpmath = sse -O3 -flto -march = loops -funroll-nativos -Wall -lstdc ++

#include <cstdint>
#include <cstdio>
#include <ctime>

inline int32_t interleaveBit(int32_t x)
{
   static const uint32_t B[] = { 0x55555555, 0x33333333, 0x0F0F0F0F, 0x00FF00FF };
   x = (x | ( x << 8)) & B[3];
   x = (x | ( x << 4)) & B[2];
   x = (x | ( x << 2)) & B[1];
   x = (x | ( x << 1)) & B[0];
   return x | (x << 1);
}

inline int32_t sumOnes(int32_t v)
{
   /* 0xAAAA == 0b1010 1010 1010 1010 */
   return !!(0xAAAA & (v ^ ~(v << 1)));
}

inline int32_t sumArray(int32_t v)
{
   return __builtin_popcount(v) - 8;
}

uint32_t x, y = 24252, z = 57768, w = 1564; //PRNG seeds

int32_t myRand()
{
   uint32_t t;
   t = x ^ (x << 1);
   x = y;
   y = z;
   z = w;
   w = w ^ ( w >> 19) ^ t ^ (t >> 8);
   return w;
}

int main()
{
   int leadingZero[8] = { 0 };
   x = static_cast<int32_t>(time(nullptr)); // seed PRNG
   const int maxS = 1 << 15;
   for( int i = 0; i < 1000; ++i )
   {
      int32_t random;
      do
      {
         random = 0xFFFF & myRand();
      } while(sumOnes(random) == 0 );
      for( int s = 0; s < maxS; ++s )
      {
         const int32_t x = interleaveBit(s);
         int j = 7;
         while( j >= 0 )
         {
            const int32_t h = (x >> (j * 2));
            const int32_t l = 0xFFFF & (~(random ^ h)); // inner product
            if( sumArray(l) == 0 )
            {
               leadingZero[j]++;
            } else
            {
               break;
            }
            j--;
         }
      }
   }
   printf("[%d, %d, %d, %d, %d, %d, %d, %d]\n",
      leadingZero[7], leadingZero[6],
      leadingZero[5], leadingZero[4],
      leadingZero[3], leadingZero[2],
      leadingZero[1], leadingZero[0]);
   return 0;
}

Agora mal posso esperar para testar isso! Infelizmente isso não será por algumas horas.

11
O seguinte foi uma edição sugerida, mas acho que pode se encaixar melhor como um comentário. Você pode substituir seu sumOnes, sumArray pelo seguinte (parece me dar uma velocidade de 2x em relação à versão openmp). inline int32_t sumOnes(int32_t v) { /* 0xAAAA == 0b1010 1010 1010 1010 */ return !! (0xAAAA & (v ^ ~(v << 1))); } inline int32_t sumArray(int32_t v) { return __builtin_popcount(v) - 8; }isso foi sugerido por @ user71404
ace_HongKongIndependence

@ user71404: o profiler disse que o programa passou todo o tempo nessas duas funções, mas eu estava realmente cansada ontem, pensando em algo melhor que isso. Vou tentar esta noite (UTC). Obrigado.
precisa saber é

Você se importaria de alterar o segundo trecho de código para ser a cópia completa e o código colável? Devo estar fazendo algo errado nas minhas tentativas de obter seu código openmp funcionando, para que isso ajude muito.

Agradável. Eu pensei que isso poderia ser feito com operações de bits.
precisa saber é o seguinte

10

Julia: 0,7s, 120x mais rápido

Como o usuário20768 demonstrou, uma porta direta do código para Julia é cerca de duas vezes mais rápida que o PyPy. Mas podemos fazer muito melhor que isso.

function pleadingzerocounts(; n = 8,
                              m = 8,
                              iters = 1000)
  @parallel (+) for S = 1:2^(8+8-1)
    leading_counts = zeros(Int, m)
    F = Array(Int, n)
    for i = 1:iters
      flag = 0
      while flag == 0
        for i = 1:n
          v = (1-(rand(Int8)&3))%2
          @inbounds F[i] = v
          flag += v & 1
        end
      end
      for j = 1:m
        sum = 0
        for i = 1:n
          @inbounds sum += S & (1 << (j + i - 2)) > 0 ? F[i] : -F[i]
        end
        sum == 0 ?
          (leading_counts[j] += 1) :
          break
      end
    end
    leading_counts
  end
end

function main()
  # Warm up the JIT
  pleadingzerocounts()
  # Then go for real
  println(@time pleadingzerocounts())
end

Você pode executar isso usando julia -p 8 -e 'require("golf.jl");main()'(o 8 é o número de processos, convém brincar com ele). No pré-lançamento mais recente da Julia, são necessários 0,7s vs. 1m22s para o PyPy.

Se você possui núcleos suficientes no seu computador e, talvez, gere algumas instâncias da AWS, poderá economizar um pouco mais :)


Tenho certeza que você está medindo o tempo errado. O Python com Pypy também é uma linguagem baseada em JIT, mas os tempos feitos pelo OP incluem o tempo de compilação do JIT. Você está excluindo. Instalei a versão mais recente da Julia git e testei seu código, e na minha máquina seu comando com 8 processos leva 6,6 segundos para terminar, mas ele imprime "tempo decorrido 0,588 .. segundos".
semi-extrínseco

O tempo do Python inclui a inicialização do PyPy e o aquecimento do JIT, mas isso leva alguns segundos no máximo - a diferença em um minuto de tempo de execução é insignificante. Fico feliz se o OP mudar a maneira como o Python é cronometrado (não fará nenhuma diferença), mas incluir o tempo de inicialização de Julia não seria razoável.
mais um minuto

Eu perguntei ao OP nos comentários da pergunta original e ele disse "Os horários devem incluir tudo para os idiomas do JIT". Ele também afirmou que criará um novo desafio em que as soluções levarão muito mais que 1 segundo, deixando Julia na competição.
semi-extrínseco

Nesse caso, a solução ideal é usar um algoritmo serial - que leva cerca de 2s. Eu publicaria o código, mas essa competição é agora um pouco redundante, já que todo mundo já sabe que o C ++ é inicializado mais rapidamente do que tudo o resto.
mais um minuto

Acabei de publicar minha solução Fortran, portanto não vejo por que você não deve postar a mais rápida da Julia (se você já possui o código).
semi-extrínseco

5

C, 1.210s

Com o código do OP executando 1m45.729s na minha máquina.

Compilação:

gcc -O3 -march=native -fwhole-program -fstrict-aliasing -ftree-vectorize -Wall ./test2.c -o ./test2

Agradecimentos especiais: @dyp para sinalizadores de compilação e idéias para otimizações.

#include <stdio.h>
#include <time.h>

#define n (8)
#define m (8)
#define iters (1000)
int leadingzerocounts[m]; // declared as global so initialised to 0
unsigned int x,y=34353,z=57768,w=1564; //PRNG seeds

/* xorshift PRNG
 * Taken from https://en.wikipedia.org/wiki/Xorshift#Example_implementation
 * Used under CC-By-SA */
int myRand() {
    unsigned int t;
    t = x ^ (x << 11);
    x = y; y = z; z = w;
    return w = w ^ (w >> 19) ^ t ^ (t >> 8);
}

int dotproduct(int*F, int*S) {
    unsigned int i;
    int sum=0;
    for(i=0; i<n; i++) {
        sum+=F[i]*S[i];
    }
    return sum;
}

int main() {
    unsigned int i, j, tmp;
    x=(int)time(NULL); //seed PRNG

    int S[n+m-1];
    for(i=0; i<(1<<(n+m-1)); i++) {
        tmp=i;
        for(j=0; j<n+m-1; j++) {
            S[j]=(tmp&1)*(-2)+1;
            tmp>>=1;
        }
        for(j=0; j<iters; j++) {
            int F[n];
            unsigned int k, flag=0;
            do {
                for(k=0; k<n; k++) {
                    F[k]=(1-(myRand()&3))%2;
                    flag+=(F[k]&1);
                }
            } while(!flag);
            for(k=0; k<m&&!dotproduct(F, S+k); k++) {
                leadingzerocounts[k]++;
            }
        }
    }
    for(i=0; i<m; i++) printf("%d ", leadingzerocounts[i]);
    return 0;
}

Saída de amostra:

6334411 2527506 1042239 439328 192914 87005 40847 19728

11
Interessante, de fato, posso fazer observações semelhantes ao remover todos esses sinalizadores de otimização. Tente -march=native -fwhole-program -fstrict-aliasing -ftree-vectorizeBtw. Eu cheguei a <4 s usando algum C ++ 11, incluindo um MT19937 mais um uniform_int_distribution.
Dpr

11
1.119s fazendo um aumento de aproximadamente 59!

11
@ace Sim, eu só queria salientar isso. Para mim, era mais simples apenas experimentar alguns dos PRNGs da biblioteca padrão em C ++. Observe que você pode usar um resultado inteiro de 32 bits de um PRNG para produzir 8 entradas para F.
Dpr

11
Como né igual a 8, você provavelmente pode usar o AVX (ou 2 * SSE) para calcular o produto escalar com um Sarmazenamento adequado .
315 Michael M.

2
Versão SSE, a velocidade-up pequena: gist.github.com/anonymous/11394210 (não se esqueça de incluir smmintrin.h)
Michael M.

5

Perl

Isso não chega nem perto da velocidade da solução C, mas é bastante rápido para uma linguagem interpretada de alto nível, eu acho. Ele reduz cerca de 40% do tempo de execução da implementação do Python.

#!/usr/bin/env perl

use v5.10;
use strict;
use warnings;
use Algorithm::Combinatorics qw( variations_with_repetition );
use List::Util qw( any sum );

use constant {
  N        => 8,
  M        => 8,
  ITERS    => 1000,
};

my @leadingzerocounts;

my $variations = variations_with_repetition([-1, 1], N + M - 1);
while (my $S = $variations->next)
{
  for my $i (1 .. ITERS)
  {
    my @F;
    until (@F and any { $_ } @F)
    {
      @F = map +((-1,0,0,1)[rand 4]), 1..N;
    }

    my $j = 0;
    while ($j < M)
    {
      last if sum map $F[$_]*$S->[$j+$_], 0..N-1;
      $leadingzerocounts[$j++]++;
    }
  }
}

say join ", ", @leadingzerocounts;

O Algorithm :: Combinatorics está disponível no Ubuntu ( sudo apt-get install libalgorithm-combinatorics-perl). Os outros módulos usados ​​são os principais módulos Perl, portanto já devem estar instalados como parte da instalação básica do Ubuntu.


11
Não afeta a velocidade, mas é o 0..N-1alcance no último map, certo? Você esqueceu use warnings? :-) Embora a lógica no OP seja confusa, a janela deslizante nunca chega ao último elemento de S.
usar o seguinte comando

Ah Eu apenas imaginei que as matrizes tinham tamanhos incompatíveis, então desabilitei a warningspermissão para que os elementos ausentes fossem tratados como zero. N-1melhora isso. E, na verdade, melhora muito ligeiramente a velocidade - agora é cerca de 40% mais rápida que a implementação do Python.
tobyink

Eu acho que seu código requer uma versão muito moderna do List :: Util. No Ubuntu 14.04 eu recebo "qualquer" não é exportado pela Lista :: módulo Util

Ah, sim, é verdade - você provavelmente precisará instalar o List :: Util fora do CPAN. anyComo alternativa, pode ser encontrado em List :: MoreUtils, que embora não seja um módulo principal, é um dos módulos CPAN mais usados.
tobyink

4

Julia: 4,66x mais lenta!

Estou realmente começando a duvidar das estatísticas em seu site ...

Observe que o código Julia a seguir é efetivamente uma transcrição direta do código Python do OP sem nenhuma otimização. Eu uso a time()função para excluir o tempo lento de inicialização de Julia ...

srand(27182818284590)
t = time()

require("Iterators")

n = 8
m = 8
iters = 1000
bothzero = 0
leadingzerocounts = zeros(m)

for S in Iterators.product(fill([-1,1], n+m-1)...)
    for i = 1:iters
        F = [[-1 0 0 1][rand(1:4)] for j = 1:n]
        while all((x) -> x == 0, F)
            F = [[-1 0 0 1][rand(1:4)] for j = 1:n]
        end
        j = 1
        while j <= m && sum(map(*, F, S[j:j+n-1])) == 0
            leadingzerocounts[j] += 1
            j += 1
        end
    end
end

println(leadingzerocounts)

t = time() - t
println("$t seconds")

Julia: 5 m 32.912 s

Código do OP em PyPy: 1 m 11.506 s

Produção de Julia:

6332170
2525472
1041522
438761
193119
86873
40705
19662

7
+1 para o seu <s> descaramento </s> espírito esportivo.
ace_HongKongIndependence

Variáveis ​​globais, importações e compreensão de matriz são lentas. Não é assim que se costuma escrever um programa Julia para desempenho.
Alex A.

4

RPython 0.187s (258x mais rápido)

Fonte original com PyPy2.2.1: 1m 6.718s

Agora, com o encadeamento, o suporte para o Python padrão foi descartado. O número de threads de trabalho pode ser especificado como um parâmetro da linha de comandos; o padrão é dois.

from time import time, sleep
from math import fmod

from rpython.rlib.rthread import start_new_thread, allocate_lock, get_ident
class Random:
  __slots__ = ['s']

  def __init__(self, s=1):
    self.s = s

  def init_genrand(self, seed):
    self.s = seed

  def genrand32(self):
    # xorshift PRNG with period 2^32-1
    # see http://core.kmi.open.ac.uk/download/pdf/6250138.pdf
    self.s ^= (self.s << 13)
    self.s ^= (self.s >> 17)
    self.s ^= (self.s << 5)
    return self.s

class ThreadEnv:
  __slots__ = ['n', 'm', 'iters', 'counts', 'running', 'lock']

  def __init__(self):
    self.n = 8
    self.m = 8
    self.iters = 1000
    self.counts = [0]*8
    self.running = 0
    self.lock = None

env = ThreadEnv()
truth = [-1,0,0,1]

def main(argv):
  argc = len(argv)
  if argc < 4 or argc > 5:
    print 'Usage: %s N M ITERS [NUM_THREADS=2]'%argv[0]
    return 1

  if argc == 5:
    num_threads = int(argv[4])
  else:
    num_threads = 2

  env.n = int(argv[1])
  env.m = int(argv[2])
  env.iters = int(argv[3]) // num_threads
  env.counts = [0]*env.m
  env.lock = allocate_lock()

  for i in xrange(num_threads-1):
    start_new_thread(run,())
    env.running += 1

  env.running += 1

  # use the main process as a worker
  run()

  # wait for any laggers
  while env.running:
    sleep(0.01)

  print env.counts
  return 0

def run():
  n, m, iters = env.n, env.m, env.iters
  counts = [0]*m
  sbits = [0]*(n+m-1)

  random = Random()
  seed = int(fmod(time(), 2147483.648)*1000) ^ get_ident()
  random.init_genrand(seed)

  for S in xrange(1<<n+m-1):
    i = 0
    sbit = 0
    while not sbit:
      sbits[i] ^= 3
      sbit = sbits[i]
      i += 1

    for i in xrange(iters):
      f = 0
      while not f:
        F = random.genrand32()

        G, x = F, 0
        for k in xrange(n):
          x += truth[(G&3)^sbits[k]]
          f |= x
          G >>= 2

      if not x:
        counts[0] += 1
        for j in xrange(1, m):
          G, x = F, 0
          for k in xrange(j, n+j):
            x += truth[(G&3)^sbits[k]]
            G >>= 2
          if x: break
          counts[j] += 1

  # passing True stalls until a lock can be obtained
  env.lock.acquire(True)

  for i in xrange(m):
    env.counts[i] += counts[i]
  env.running -= 1

  env.lock.release()

def target(*args):
  return main, None

O RPython é um subconjunto restrito do Python, que pode ser traduzido para C e compilado usando o RPython Toolchain . Seu objetivo expresso é ajudar na criação de intérpretes de linguagem, mas também pode ser usado para compilar programas simples como o descrito acima. A maioria dos recursos mais sofisticados do Python, como itertoolsou mesmo mapnão estão disponíveis.

Para compilar, faça um clone local do repositório pypy atual e execute o seguinte:

$ pypy %PYPY_REPO%/rpython/bin/rpython --thread convolution.py

O executável resultante será nomeado convolution-cou semelhante no diretório de trabalho atual.

Eu parametrizei as variáveis ​​de entrada, então o programa deve ser executado como:

convolution-c 8 8 1000

para corresponder ao código de exemplo.


Notas de implementação

S in itertools.product([-1,1], repeat = n+m-1)torna-se S in xrange(1<<n+m-1), interpretando Scomo um mapa de bits: [ 0, 1] → [ -1, 1]

Da mesma forma, Fé também um mapa de bits, com cada um de dois bits que representam um único valor:
[ 00, 01, 10, 11] → [ -1, 0, 0, 1]

Uma tabela verdade é usada para procurar o produto, em vez de executar uma multiplicação.

Como números inteiros assinados de 32 bits são usados, npodem não ser maiores que 15 e n+mmaiores que 31. Suporte inteiro arbitrário pode ser obtido com o rpython.rlib.rbigintmódulo, se necessário.

A primeira iteração do loop do produto escalar é desenrolada e combinada com o teste de nulidade de F.

Um PRNG homebrew é usado, fonte listada. O autor do artigo demonstra um período de 2 32 -1 e afirma que passa em todos os testes de Diehard, exceto um, embora eu pessoalmente não tenha confirmado isso.

A semente aleatória muda a cada milissegundo, o que é tão bom quanto o uso de um carimbo de data / hora permitirá. Além disso, cada thread de trabalho tem xorseu ID de processo com esse valor, para garantir que cada um tenha uma semente diferente.


Exemplo de tempos

2 threads de trabalho:

$ timeit convolution-c 8 8 1000 2
[6331845, 2526161, 1042330, 440018, 193724, 87147, 40943, 19603]

Elapsed Time:     0:00:00.375
Process Time:     0:00:00.687
System Calls:     6927

4 threads de trabalho:

$ timeit convolution-c 8 8 1000 4
[6334565, 2527684, 1043502, 440216, 193225, 87398, 40799, 19338]

Elapsed Time:     0:00:00.218
Process Time:     0:00:00.796
System Calls:     3417

8 threads de trabalho:

$ timeit convolution-c 8 8 1000 8
[6327639, 2522483, 1039869, 437884, 192460, 86771, 40420, 19403]

Elapsed Time:     0:00:00.187
Process Time:     0:00:00.734
System Calls:     3165

Fonte original do OP:

$ timeit pypy convolution-orig.py
[6330610, 2525644, 1041481, 438980, 193001, 86622, 40598, 19449]

Elapsed Time:     0:01:06.718
Process Time:     0:01:06.718
System Calls:     11599808

Tempo para 100000 iterações:

$ timeit convolution-c 8 8 100000 8
[633156171, 252540679, 104129386, 43903716, 19307215, 8709157, 4072133, 1959124]

Elapsed Time:     0:00:16.625
Process Time:     0:01:02.406
System Calls:     171341

Eu nunca vi um programa rpython antes. Isso é ótimo. Agora, existe um programa python puro equivalente que o pypy pode executar em 1.03s?

@ Lembik eu gostaria de ver um. Eu pensei que 4.7s era muito bom, considerando que minha primeira tentativa de python puro tinha ~ 15s.
Primo

Sim, desculpe pelo atraso. Ainda não tenho o código em execução, mas o mais rápido possível.

Você deve tentar adicionar um JIT. Agora isso seria rápido!
Kirbyfan64sos

@Lembik, obrigado pela menção;) Por curiosidade, ele foi executado mais rapidamente com 4 threads de trabalho ou 8?
primo

3

Julia: 1 min 21,4s (2,2x mais rápido) (modificação do código de Arman)

Código do Op em PyPy: 3 min 1.4 s

Ambos feitos no REPL, não incluindo tempo para carregar pacotes.

function foo()                                                                                                                                                             
    n = 8                                                                                                                                                                  
    m = 8                                                                                                                                                                  
    iters = 1000                                                                                                                                                           
    bothzero = 0                                                                                                                                                           
    leadingzerocounts = zeros(Int,m)                                                                                                                                       
    P=[-1,0,0,1]                                                                                                                                                           

    for S in Iterators.product(fill([-1,1], n+m-1)...)                                                                                                                     
        Sm=[S...]                                                                                                                                                          
        for i = 1:iters                                                                                                                                                    
            F = P[rand(1:4,n)]                                                                                                                                             
            while all(F==0)                                                                                                                                                
                F = P[rand(1:4,n)]                                                                                                                                         
            end                                                                                                                                                            
            j = 1                                                                                                                                                          

            while j <= m && dot(F,Sm[j:j+n-1]) == 0                                                                                                                        
                leadingzerocounts[j] += 1                                                                                                                                  
                j += 1                                                                                                                                                     
            end                                                                                                                                                            
        end                                                                                                                                                                
    end                                                                                                                                                                    

    println(leadingzerocounts)                                                                                                                                             
end 

Existem alguns problemas com o código de Arman, tornando-o muito lento: ele usa muitas funções anônimas e funções de ordem superior desnecessariamente. Para testar se todo um vetor F é zero, por que não escrever todos (F == 0) em vez de todos (x-> x == 0, F)? É mais curto e literalmente mil vezes mais rápido.

Ele também usa soma (mapa (*, x, y)) como produto escalar em vez de simplesmente ponto (x, y). A primeira versão 650 vezes mais lenta para um vetor de 10k dobra. E a função do produto de ponto é implementada como um loop for na Julia pura.

Além disso, a compreensão da matriz é lenta. É melhor escrever [0,1,0, -1] [rand (1: 4, n)] em vez de [[-1 0 0 1] [rand (1: 4)] para j = 1: n] .

Finalmente, variáveis ​​globais são péssimas em Julia. Julia é rápida apenas se você escrever código de forma que permita que o JIT e a inferência de tipo funcionem. Uma grande parte disso é a estabilidade do tipo: o compilador deve ter certeza de que o tipo de uma variável não será alterado enquanto estiver dentro de um loop, por exemplo.


Obrigado! Vejo que ainda tenho muito a aprender sobre a linguagem Julia antes de poder fazer reivindicações sobre sua velocidade :) Realmente feliz em ver que algumas correções triviais no meu código aumentam o tempo de execução várias vezes.
Agar ágil

2

Nimrod

import times, locks, strutils, unsigned

const
  N = 8
  M = 8
  iters = 1000
  numThreads = 8

type
  SVec = array[0..N+M-1, int]
  FVec = array[0..N-1, int]
  ComputeThread = TThread[int]

var
  rngSeed = int(epochTime()*1000)
  totalLeadingZeros: array[0..M-1, int]
  lock: TLock

type
  RNGState = object
    x, y, z, w: uint32

proc newRNG(seed: int): RNGState =
  result.x = uint32(seed)

proc random(rng: var RNGState): int =
  let t = rng.x xor (rng.x shl 11)
  rng.x = rng.y; rng.y = rng.z; rng.z = rng.w
  rng.w = rng.w xor (rng.w shr 19) xor t xor (t shr 8)
  result = int(rng.w)

proc initVecRand(v: var FVec, rng: var RNGState) =
  const values = [ -1, 0, 0, 1 ]
  var rnd = rng.random
  var bitAcc = 0
  for i in 0 .. <len(v):
    let val = values[rnd and 3]
    rnd = rnd shr 2
    v[i] = val
    bitAcc = bitAcc or val
  if bitAcc == 0:
    initVecRand(v, rng)

proc convolve(s: SVec, f: FVec, offset: int): int =
  for i in 0 .. <len(f):
    result += s[i+offset]*f[i]

proc iterate(v: var SVec) =
  for i in 0 .. <len(v):
    if v[i] == -1:
      v[i] = 1
      return
    v[i] = -1

proc mainThread(id: int) {.thread.} =
  const numS = 1 shl (N+M-1)
  var
    s: SVec
    f: FVec
    leadingZeros: array[0..M-1, int]
    rng = newRNG(rngSeed + id)
  for k in 0 .. <len(s):
    s[k] = -1
  for i in 1..numS:
    for j in countUp(id, iters, numThreads):
      initVecRand(f, rng)
      if convolve(s, f, 0) == 0:
        leadingZeros[0] += 1
        for k in 1 .. <M:
          if convolve(s, f, k) == 0:
            leadingZeros[k] += 1
          else:
            break
    iterate(s)
  acquire(lock)
  for i in 0 .. <M:
    totalLeadingZeros[i] += leadingZeros[i]
  release(lock)

proc main =
  let startTime = epochTime()
  var threads: array[1..numThreads, ComputeThread]
  initLock(lock)
  for i in 1..numThreads:
    createThread(threads[i], mainThread, i)
  for i in 1..numThreads:
    joinThread(threads[i])
  echo("Leading zeros: ", @totalLeadingZeros)
  let endTime = epochTime()
  echo("Time taken:    ", formatFloat(endTime - startTime, ffDecimal, 3),
       " seconds")

main()

Exemplo de saída:

Leading zeros: @[6333025, 2525808, 1042466, 439138, 192391, 86751, 40671, 19525]
Time taken:    0.145 seconds

O Nimrod compila para C, portanto, a escolha do compilador C para o back-end também é importante.

Usando clang, compile com:

nimrod cc --threads:on --cc=clang --passc:-flto -d:release conv.nim

Usando o gcc, compile com:

nimrod cc --threads:on --cc=gcc --passc:-flto -d:release conv.nim

Omita --passc:-fltose você tem um compilador C mais antigo que não suporta LTO. Omita a --cc=...opção se você estiver bem com a opção padrão para o compilador C. O código requer o Nimrod 0.9.4 ou 0.9.5 .

No meu quadcore iMac (2,66 GHz core i5), o código é executado em cerca de 0,15 segundo com gcc 4,9, 0,16 segundo com clang, em comparação com 88 segundos no PyPy 2.2.1 (ou seja, mais de 500 vezes mais). Infelizmente, não tenho acesso a uma máquina com mais de quatro núcleos que também possua o PyPy instalado ou onde eu possa instalar facilmente o PyPy, embora eu receba cerca de 0,1 segundos (com muito ruído de medição) em uma AMD de 64 núcleos. Opteron 6376 1,4 GHz (de acordo com / proc / cpuinfo) com gcc 4.4.6.

A implementação tenta ser fiel ao código original em vez de otimizar o custo da legibilidade, sem renunciar às otimizações óbvias. Curiosamente, a recursão da cauda initVecRand()é um pouco mais rápida que um loop com uma instrução de interrupção com o gcc e o clang. Desenrolar manualmente uma iteração do convolveloop de teste dentro do loop principal também produziu uma aceleração, provavelmente devido a uma melhor previsão de ramificação.


Como você obtém o nimrod para o ubuntu?

Pesquisa @Lembik Uma rápida Google iria dar-lhe nimrod-lang.org/download.html
ace_HongKongIndependence

@ace Eu também incluí o link no meu post (embora seja difícil ver com azul no preto agora que olho para ele).
Reimer Behrends

Você poderia acelerar o processo ainda mais, aumentando o tamanho da semente para 128 bits: xorshift.di.unimi.it
user60561

2

Java

Traduzi a solução C ++ acima para Java:

import java.util.Random;
import java.util.Arrays;

public class Bench2 {
  public static int[] bits = { 0x55555555, 0x33333333, 0x0F0F0F0F, 0x00FF00FF };
  public static int[] oneValues = { 1, 0, 0, 1 };
  public static int[] values = { -1, 0, 0, 1 };
  public static int n = 8;
  public static int m = 8;
  public static int iters = 1000;

  private static int x,y=34353,z=57768,w=1564;

  public static void main( String[] args ) {
    x = (int) (System.currentTimeMillis()/1000l);

    int[] leadingzerocounts = new int[ m ];
    Arrays.fill( leadingzerocounts, 0 );

    int maxS = 1 << 15;

    for( int s = 0; s < maxS; s++ ) {
      int x = interleaveBit( s );

      for( int i=0; i<iters; i++ ) {
        int random;

        do {
          random = 0xFFFF & fastRandom( );
        } while( sumOnes( random ) == 0 );

        int j = 7;

        while( j >= 0 ) {
          int h = ( x >> (j*2) );
          int l = 0xFFFF & (~(random ^ h));

          if( sumArray( l ) == 0 ) {
            leadingzerocounts[ j ]++;
          } else {
            break;
          }

          j--;
        }
      }
    }

    for( int i = 7; i >= 0; --i ) {
      System.out.print( leadingzerocounts[ i ] + " " );
    }

    System.out.println( );
  }

  public static int interleaveBit( int x ) {
    x = (x | ( x << 8)) & bits[3];
    x = (x | ( x << 4)) & bits[2];
    x = (x | ( x << 2)) & bits[1];
    x = (x | ( x << 1)) & bits[0];
    return x | (x << 1);
  }

  public static int sumOnes( int v ) {
    return (0xAAAA & (v ^ ~(v << 1)));
    // int s = 0;

    // for( int i = 0; i < 8; ++i ) {
    //   int a = 3 & ( v >> (i*2) );
    //   s += oneValues[ a ];
    // }

    // return s;
  }

  public static int sumArray( int v ) {
    return Integer.bitCount( v ) - 8;
    // int s = 0;

    // for( int i=0; i<8; ++i ) {
    //   int a = 3 & ( v >> (i*2) );
    //   s += values[ a ];
    // }

    // return s;
  }

  public static int fastRandom( ) {
    long t;
    t = x ^ (x << 11);
    x = y; y = z; z = w;
    return w = (int)( w ^ (w >> 19) ^ t ^ (t >> 8));
  }
}

Na minha máquina, recebo a seguinte saída para o programa java:

time java Bench2
6330616 2524569 1040372 439615 193290 87131 40651 19607 
java Bench2  0.36s user 0.02s system 102% cpu 0.371 total

O programa OPs executa cerca de 53 segundos na minha máquina:

time pypy start.py
[6330944, 2524897, 1040621, 439317, 192731, 86850, 40830, 19555]
pypy start.py  52.96s user 0.06s system 99% cpu 53.271 total

O programa c ++ executou apenas cerca de 0,15 segundo:

time ./benchcc
[6112256, 2461184, 1025152, 435584, 193376, 87400, 40924, 19700]
./benchcc  0.15s user 0.00s system 99% cpu 0.151 total

Isso é cerca de 2,5x mais rápido que a solução java correspondente (não excluí a inicialização da VM). Essas soluções java são 142x mais rápidas que o programa executado com o PyPy.

Como eu estava pessoalmente interessado, configurei iterspara 100_000 para Java e C ++, mas o fator 2,5 não diminuiu em favor do Java se algo aumentasse.

Edição: Eu executei os programas em um PC Arch Linux de 64 bits.

EDIT2: Quero acrescentar que comecei com uma tradução aproximada do código python:

import java.util.Random;
import java.util.Arrays;

public class Bench {
    public static int[] values = { -1, 0, 0, 1 };
    public static int n = 8;
    public static int m = 8;
    public static int iters = 1000;

    private static int x,y=34353,z=57768,w=1564; 

    public static void main( String[] args ) {
        x = (int) (System.currentTimeMillis()/1000l);

        int[] leadingzerocounts = new int[ m ];
        Arrays.fill( leadingzerocounts, 0 );

        int[] S = new int[ n+m-1 ];
        Arrays.fill( S, -1 );

        do {
            for( int i=0; i<iters; i++ ) {
                int[] F = new int[ n ];

                do {
                    randomArray( F );
                } while( containsOnlyZeros( F ) );

                for( int j=0; j < m && check( F, S, j ); j++ ) {
                    leadingzerocounts[ j ] += 1;
                }
            }
        } while( next( S ) );

        System.out.println( Arrays.toString( leadingzerocounts ) );
    }

    public static void randomArray( int[] F ) {
        for( int i = 0; i<F.length; i++ ) {
            F[ i ] = (1-(fastRandom()&3))%2;
        }
    }

    public static boolean containsOnlyZeros( int[] F ) {
        for( int x : F ) {
            if( x != 0 ) {
                return false;
            }
        }

        return true;
    }

    public static boolean next( int[] S ) {
        for( int i=0; i<S.length; i++ ) {
            if( ( S[ i ] = -S[ i ] ) == 1 ) {
                return true;    
            }
        }

        return false;
    }

    public static boolean check( int[] F, int[] S, int j ) {
      int sum = 0;

      for( int i=0; i<n; i++ ) {
          sum += F[ i ] * S[ j + i ];
      }

      return sum == 0;
    }

    public static int fastRandom( ) {
        long t;
        t = x ^ (x << 11);
        x = y; y = z; z = w;
        return w = (int)( w ^ (w >> 19) ^ t ^ (t >> 8));
    }
}

Este programa executou cerca de 3,6 segundos:

time java Bench   
[6330034, 2524369, 1040723, 439261, 193673, 87338, 40840, 19567]
java Bench  3.64s user 0.01s system 101% cpu 3.600 total

O que é cerca de 14 vezes mais rápido que a solução PyPy. (Escolher a função aleatória padrão sobre a função fastRandom leva a um tempo de execução de 5 segundos)


2

Python 3.5 + numpy 1.10.1, 3.76 segundos

Os testes foram executados no meu Macbook Pro. O código do OP levou ~ 6 minutos na mesma máquina.

A razão pela qual estou respondendo a essa pergunta é que não tenho 10 reputações e não posso responder à Parte I :-p

Nos últimos dias, eu tenho tentado descobrir como executar voltas maciças eficientemente com numpy (sem depender de um pacote de terceiros, nem de um scipy). Quando me deparei com essa série de desafios durante minha pesquisa, decidi experimentá-la. Posso ter chegado tarde a este jogo, mas aqui está minha tentativa de usar o Python 3.5 e o numpy 1.10.1.

def test_convolv():
    n = 8 
    m  = 8 
    iters = 1000
    ilow = np.ceil(0+n/2).astype(int)
    ihigh = np.ceil(m+n/2).astype(int)

    leadingzerocounts = np.zeros(m)

    # Pre-compute S & F
    S = np.array(list(itertools.product([-1,1], repeat = n+m-1)))
    choicesF = np.random.choice(np.array([-1, 0, 0, 1], dtype=np.int8), size=n*iters).reshape(iters,n)
    imask = ~np.any(choicesF, axis=1)
    while np.any(imask):
        imasksize = np.count_nonzero(imask)
        choicesF[imask,:] = np.random.choice(np.array([-1, 0, 0, 1], dtype=np.int8), size=n*imasksize).reshape(imasksize, n)
        imask = ~np.any(choicesF, axis=1)

    for i in np.arange(iters):
        F = choicesF[i, :]
        # This is where the magic is: by flattening the S array, 
        # I try to take advantage of speed of the np.convolve 
        # (really numpy.multiarray.correlate). 
        FS = (np.convolve(S.reshape(-1), F, 'same').reshape(S.shape))[:, ilow:ihigh]
        jmask_not = (FS[:, 0] != 0)
        leadingzerocounts[0] = leadingzerocounts[0]+np.count_nonzero(~jmask_not)
        for j in np.arange(n-1)+1:
            jmask = (FS[jmask_not, j] != 0)
            leadingzerocounts[j] = leadingzerocounts[j] + np.count_nonzero(~jmask)
            jmask_not[(jmask_not.nonzero()[0])[jmask]] = False

    print(leadingzerocounts)

Eu pré-calculei as matrizes S e F e aplainou a matriz S enquanto realizava a convolução, que (com base em minhas experiências) poderia tirar proveito da velocidade do np.convolve. Em outras palavras, como não encontrei uma rotina de convolução vetorizada, fiz uma vetorização falsa do código achatando toda a matriz e esperei que o np.convolved fizesse a vetorização para mim, o que parecia estar funcionando. Observe que usei mode = 'same' e aparei os elementos iniciais e finais que eram inúteis.

No meu Macbook Pro, os resultados do teste dão 3,76 segundos . Quando executei o código do OP (modificado para Python 3.5), recebi cerca de 6 minutos . A aceleração é de cerca de 100 vezes.

Uma desvantagem é que, como as matrizes S e F devem ser armazenadas, o requisito de memória pode ser um problema se os tamanhos forem muito grandes.

Eu usei o mesmo método para a Parte I e recebi uma aceleração de ~ 60-100x no meu laptop.

Como fiz tudo no meu Macbook Pro, se alguém pudesse testar meu código e me informar como ele funciona na sua máquina, eu agradeceria muito!


1

J, aumento de 130x ~ 50x?

n =: m =: 8
len =: 1000
S =: (] - 0 = ])S0=: #:i.2^<:+/n,m
k =: (n#0) -.~ (_1 0 0 1) {~ (n#4) #: i.4^n
sn =: (]-0=])#:i.2^n
ku =: ~. k
M =: 0=+/"1 sn *"1/ ku
fs =: (ku&i.)"1 k
snum =: n #.\"1 S0

run =: 3 : 0
 r =: n#0
 for_t. (snum) do.
   rn =: fs{~? len # #k
   r =: r + +/"1*/\rn{"1 t{M
 end.
 r
)
echo run 0
exit''

Vezes em um debian aleatório:

u#>time j slowpy.ijs
6334123 2526955 1041600 440039 193567 87321 40754 19714

real    0m2.453s
user    0m2.368s
sys     0m0.084s


u#>time python slow_pyth.py
[6331017, 2524166, 1041731, 438731, 193599, 87578, 40919, 19705]

real    5m25.541s
user    5m25.548s
sys     0m0.012s

Eu acho que há espaço para melhorias.


O script Python deve ser executado usando pypy, não python, e é por isso que seu script parece estar dando velocidade 130x.
ace_HongKongIndependence

@ace sim, notei, mas não consigo instalar o pypy: - / Acho que a ordem de magnitude permanecerá.
Eelvex


De fato, não necessariamente.
Eelvex

Qual problema você tem instalando pypy?

1

C ++: x200 (i7 de 4 núcleos, deve ser dimensionado para x400 em 8 núcleos)

Tentando uma solução C ++ 11 mais simples (testada com o VS 2012, gcc e clang) com paralelização.

Para que isso seja compilado e executado no Linux com o gcc 4.8.1:

g ++ -O3 -msse -msse2 -msse3 -march = native -std = c ++ 11 -pthread -Wl, - não como necessário golf.cpp

No Linux, também precisamos std::launch::asyncforçar vários threads. Eu estava sentindo falta disso em uma versão anterior.

No Visual Studio (2012+), isso deve funcionar, mas criar uma compilação para o tempo ...

No meu antigo dual core i3, isso é executado em ~ 0,9 segundos. No meu i7 quad core, isso é 0,319s vs. pypy 66 segundos.

Em um i7 de 8 núcleos, isso deve estar na faixa de aceleração x400. Mudar para matrizes de estilo C aceleraria, mas eu estava interessado em ficar com contêineres C ++. Para mim, é interessante ver a velocidade que você pode obter mantendo-se relativamente próximo do domínio do problema e em um nível relativamente alto, algo em que eu acho que o C ++ é realmente bom. Também digno de nota é a relativa facilidade de paralelização usando construções C ++ 11.

A solução de bits do @ ilmale é muito legal e funciona para -1/1/0. Pode-se também jogar SSE nisso e talvez obter uma aceleração significativa.

Além da paralelização, há outro "truque" que reduz o número de somatórios. Resultados da amostra: 6332947 2525357 1041957 438353 193024 87331 40902 19649

#include <vector>
#include <iostream>
#include <thread>
#include <future>
#include <time.h>
#include <ctime>
#include <algorithm>

using namespace std;

// Bring some of these constants out to share
const size_t m = 8;
const int nthreads = 16;
const size_t cn = 15;
const int two_to_cn = 32768;

static unsigned int seed = 35;

int my_random() // not thread safe but let's call that more random!
{
   seed = seed*1664525UL + 1013904223UL; // numberical recipes, 32 bit
   return ((seed>>30)&1)-!!((seed>>30)&2); // Credit to Dave!
}

bool allzero(const vector<int>& T)
{
   for(auto x : T)
   {
      if(x!=0)
      {
         return false;
      }
   }
   return true;
}

// Return the position of the first non-zero element
size_t convolve_until_nonzero(size_t max_n, const vector<int>& v1, const vector<int>& v2)
{
   for(size_t i = 0; i<max_n; ++i)
   {
      int result = 0;
      for(size_t j = 0; j<v2.size(); ++j)
      {
         result += v1[i+j]*v2[j];
      }
      if(result!=0)
      {
         return i;
      }
   }
   return max_n;
}

void advance(vector<int>& v)
{
   for(auto &x : v)
   {
      if(x==-1)
      {
         x = 1;
         return;
      }
      x = -1;
   }
}

vector<int> convolve_random_arrays(vector<int> S, int range)
{
   const int iters = 1000;
   int bothzero = 0;
   int firstzero = 0;

   time_t current_time;
   time(&current_time);
   seed = current_time;


   vector<int> F(m);
   vector<int> leadingzerocounts(m+1);

   for(auto &x: leadingzerocounts)
   {
      x = 0;
   }

   for(int i=0; i<range; ++i)
   {
      for(int j=0; j<iters; ++j)
      {
         do
         {
            for(auto &x : F)
            {
               x = my_random();
            }
         } while(allzero(F));
         leadingzerocounts[convolve_until_nonzero(m, S, F)]++;
      }
      advance(S);
   }

   // Finish adding things up...
   for(int i=m-1; i>0; --i)
   {
      leadingzerocounts[i] += leadingzerocounts[i+1];
   }

   vector<int> withoutfirst(leadingzerocounts.begin()+1, leadingzerocounts.end());
   return withoutfirst;
}

int main(int argc, char* argv[])
{

   vector<int> leadingzerocounts(m);

   for(auto &x: leadingzerocounts)
   {
      x = 0;
   }

   clock_t start = clock();

   vector<int> S(cn);
   for(auto &x : S)
   {
      x = -1;
   }

   vector< future< vector< int > > > fs; // The future results of the threads

   // Go make threads to work on parts of the problem
   for(int i=0; i<nthreads; ++i)
   {
      vector<int> S_reversed = S; // S counts using LSBs but we want the thread start to be in MSBs
      reverse(S_reversed.begin(), S_reversed.end());
      fs.push_back(async(std::launch::async, convolve_random_arrays, S_reversed, two_to_cn/nthreads));
      advance(S);
   }
   // And now collect the data
   for(auto &f : fs)
   {
      vector<int> result = f.get();
      for(int i=0; i<result.size(); ++i)
      {
         leadingzerocounts[i] += result[i];
      }
   }

   for(auto count : leadingzerocounts)
   {
      cout << count << endl;
   }

   return 0;
}

1

Fortran: 316x

Ok, Fortran: consegui uma velocidade de 106x 155x 160x 316x ao usar um Xorshift RNG e OpenMP em uma CPU i7 de 4 núcleos. Fora isso, não há grandes truques. Para o iterador construir S, eu apenas uso a representação binária do inteiro de 16 bits i. Você observará que, além do RNG embutido e do "iterador" / mapeamento de i para S, o código é tão alto quanto o código Python.

Edit: removeu o "if" no Xorshift, agora usando "r = abs (w / ...)" em vez de "r = w / ...". Vai de 106x para 155x.

Edit2: Isso gera 15x tantos números aleatórios quanto a solução C ++. Se alguém tiver uma solução zero de sobrecarga para converter um int aleatório em uma matriz de 0s e 1s no Fortran, eu sou todo ouvidos. Então poderíamos vencer C ++ :)

Edit3: A primeira edição introduziu um bug, como Lembik apontou. Isso foi corrigido agora, com uma pequena melhoria no aumento de velocidade. Vou tentar usar a sugestão da Eelvex para obter mais velocidade.

Edit4: a criação de perfil indicava que a conversão para real e de volta para inteiro com nint () era lenta. Substituí isso por uma divisão inteira fazendo o dimensionamento e o arredondamento, passando de 160x para 316x.

Ajuntar com:

gfortran -O3 -march = nativo -fopenmp golf.f90

program golf
implicit none
integer, parameter :: m=8, n=8
integer :: F(n), S(m+n-1), leadingzerocounts(m)
integer :: j,k,bindec,enc,tmp,x=123456789,y=362436069,z=521288629,w=88675123
integer*2 :: i
real :: r

leadingzerocounts=0

!$OMP parallel do private(i,enc,j,bindec,S,F,k,tmp,x,y,z,w,r) reduction(+:leadingzerocounts) schedule(dynamic)
do i=0,32766
  enc=i
  ! Short loop to convert i into the array S with -1s and 1s
  do j=16,2,-1
    bindec=2**(j-1)
    if (enc-bindec .ge. 0) then
      S(j-1)=1
      enc=enc-bindec
    else
      S(j-1)=-1
    endif
  end do
  do j=1,1000
    F=0
    do while (.not. any(F /= 0))
      do k=1,n
        ! Start Xorshift RNG
        tmp = ieor(x,ishft(x,11))
        x = y
        y = z
        z = w
        w = ieor(ieor(w,ishft(w,-19)),ieor(tmp,ishft(tmp,-8)))
        ! End Xorshift RNG
        ! Just scale it inside the nint:
        !F(k)=nint(w/2147483648.0)
        ! Scaling by integer division is faster, but then we need the random 
        ! number to be in (-2,2) instead of [-1,1]:
        F(k)=w/1073741824

      end do
    end do
    do k=1,m
      if (dot_product(F,S(k:k+n-1)) /= 0) exit
      leadingzerocounts(k)=leadingzerocounts(k)+1
    end do
  end do
end do
!$OMP end parallel do

print *, leadingzerocounts

end

Exemplo de saída:

$ time ./a.out
6329624 2524831 1039787 438809 193044 6860 40486 19517
./a.out 1,45s usuário 0,00s sistema 746% cpu 0,192 total

Código do OP:

$ time pypy golf.py
pypy golf.py 60.68s sistema do usuário 0.04s 99% da CPU 1: 00.74 total


O que eu usei em J foi uma lista de pré-construção de 4 ^ n números na base-4, depois convertida em triádica e excluindo 0. O RNG apenas escolhe dessa lista.
Eelvex

Não sei se o seu código está correto. Para 100.000 iterações, recebo 633140285 271390368 118307997 52751245 23725837 10744292 4944464 2388125, mas acho que deve estar mais próximo de 633170604 252560981 104156146 43911426 19316309 8713324 4073378 1959440. Essa é uma diferença consistente.

11
Ah, obrigado, @Lembik, minha edição para acelerar (remover a instrução if) foi realmente um bug. Atualizei meu código para que esteja correto agora. Tentarei postar uma versão usando a sugestão da Eelvex posteriormente.
semi-extrínseco

Isso também acelerou, parece!

Sim, ligeira aceleração, eu acho. Percebi que estava adicionando 1,0 e subtraindo 1,0 dentro de um loop apertado.
semi-extrínseco
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.