Quanto è lento Python (parte II)?


52

Questo è il seguito di Quanto è lento Python? (O quanto è veloce la tua lingua?) .

Si scopre che era un po 'troppo facile ottenere uno speedup x100 per la mia ultima domanda. Per coloro che hanno apprezzato la sfida ma vogliono qualcosa di più difficile in cui possono davvero usare le loro abilità di basso livello, ecco la parte II. La sfida è ottenere un speedup x100 per il seguente codice Python testato sul mio computer.

Per renderlo più difficile, sto usando pypy questa volta. Il tempismo attuale per me è di 1 minuto e 7 secondi usando pypy 2.2.1.

Regole

  1. La prima persona a inviare il codice che posso eseguire, è corretta ed è x100 volte più veloce sulla mia macchina riceverà un premio di 50 punti.
  2. Assegnerò la vittoria al codice più veloce dopo una settimana.
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

L'output dovrebbe essere simile a

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

Devi usare un seme casuale nel tuo codice e sarà accettato qualsiasi generatore di numeri casuali che sia abbastanza buono da dare risposte vicine a quanto sopra.

La mia macchina I tempi verranno eseguiti sulla mia macchina. Questa è un'installazione ubuntu standard su un processore a otto core AMD FX-8350. Questo significa anche che devo essere in grado di eseguire il tuo codice.

Spiegazione del codice

Questo codice scorre su tutte le matrici S di lunghezza n + m-1 create per -1 secondi e 1 secondo. Per ogni array S campiona 1000 array casuali diversi da zero F di lunghezza n costituiti da -1,0 o 1 con probabilità di 1/4, 1/2, / 14 di prendere ciascun valore. Quindi calcola i prodotti interni tra F e ciascuna finestra di S di lunghezza n fino a trovare un prodotto interno diverso da zero. Aggiunge 1 per leadingzerocountsogni posizione in cui ha trovato un prodotto interno pari a zero.

Stato

  • Perl . 2,7 volte rallentamento di @tobyink. (Rispetto a pypy non a cpython.)

  • J . Accelerazione di 39 volte di @Eelvex.

  • C . 59 volte più veloce di @ace.
  • Julia . 197 volte più veloce senza includere il tempo di avvio di @ un altro minuto. 8,5 volte più veloce incluso il tempo di avvio (è più veloce usando 4 processori in questo caso di 8).
  • Fortran . 438 volte accelerato da @ semi-estrinsic.
  • Rpython . 258 volte più veloce di @primo.
  • C ++ . 508 volte accelerato da @ilmale.

(Ho smesso di programmare i nuovi miglioramenti perché sono troppo veloci e iter era troppo piccolo.)


È stato sottolineato che i tempi inferiori a un secondo sono inaffidabili e anche alcune lingue hanno un costo di avvio. L'argomento è che se si desidera includere anche il tempo di compilazione di C / C ++ ecc. Ecco i tempi per il codice più veloce con il numero di iterazioni aumentato a 100.000.

  • Julia . 42 secondi di @ un altro minuto.
  • C ++ . 14 secondi di @GuySirton.
  • Fortran . 14s di @ semi-estrinsic.
  • C ++ . 12s di @ilmale.
  • Rpython . 18s di @primo.
  • C ++ . 5s di @Stefan.

Il vincitore è .. Stefan!

Sfida di follow-up pubblicata. Quanto in alto puoi andare? (Una sfida di codifica + algoritmi) . Questo è più difficile.


3
sarebbe utile una spiegazione di ciò che il codice dovrebbe raggiungere, quindi possiamo riscriverlo e non semplicemente portarlo
Einacio,

6
" La prima persona a inviare il codice che posso eseguire, è corretta ed è x100 volte più veloce sulla mia macchina vince immediatamente e la competizione si chiude. " Qual è lo scopo di chiudere la competizione in quel modo? Perché non utilizzare una scadenza come la maggior parte degli altri, in modo che possiamo vederlo ulteriormente ridotto in altre lingue?
GroveNL

5
@Einacio Questa è una bella idea. Ho cambiato le regole che spero non piaccia a nessuno.

1
@Lembik Ho migliorato la mia versione di Fortran, rendendola di nuovo 2 volte più veloce sulla mia macchina. Potresti ripeterlo? :)
semi-estrinseco

1
@ semi-estrinseco Fatto.

Risposte:


13

C ++ bit magic

~ 16ms multithread, 56ms singlethreaded. ~ 4000 speedup.

(l'accelerazione si basa sul codice multithread sul mio i7-2820QM e sui 1 minuto e 9 secondi menzionati nella domanda. Dato che il sistema dell'OP ha prestazioni single threaded peggiori rispetto alla mia CPU ma migliore perfiormance multi-thread mi aspetto che questo numero sia accurato)

La parte multithread è abbastanza inefficiente a causa della generazione di thread. Probabilmente potrei fare di meglio sfruttando la mia libreria di lavori personalizzati, ma che uno ha dei bug nei sistemi unix. Per una spiegazione e un codice quasi identico senza thread fare riferimento a https://codegolf.stackexchange.com/a/26485/20965 .

modificare

Ho dato ad ogni thread il proprio RNG e ho ridotto la lunghezza dei bit a 32, riducendo il tempo di esecuzione di alcuni 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;
}

Uscita campione:

   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,

L'output non è giusto penso, forse c'è un bug? Confronta con ciò che è nella domanda. In particolare l'ultima colonna dovrebbe essere un numero vicino a 19180.

@Lembik posso vedere cosa intendi. Penso che l'output casuale non sia abbastanza casuale, il che a volte crea output funky. Con il generatore casuale C ++ 11 funziona benissimo. Riparerò il codice più tardi oggi.
Stefan,

Ottengo Stefan.cpp: 104: 101: errore: 'memcpy' non è stato dichiarato in questo ambito memcpy (out.data () + (threadId * m), leadingZeros.data (), sizeof (leadingZeros [0]) * m );

Penso che devo includere string.h. Provaci ancora.
Stefan,

Compilalo con g ++ -O3 -std = c ++ 0x -pthread -Wl, - non necessario Stefan.cpp -o Stefan

16

C ++ x150 x450 x530

Invece di array ho usato bit (e magia oscura).
Grazie @ace per la funzione casuale più veloce.

Come funziona: i primi 15 bit dell'intero srappresentano l'array S[15]; gli zeri rappresentano -1, quelli rappresentano +1. L'array Fè costruito in modo simile. Ma con due bit per ogni simbolo.

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

Causa Se Favere una rappresentazione diversa con cui devo interagire Scon se stessa per essere comparabile F.

  • 0 (-1) è diventato 00 (-1 nella rappresentazione di F )
  • 1 (+1) è diventato 11 (+1 nella rappresentazione di F)

Ora possiamo semplicemente usare Carnot per calcolare il prodotto interno. Ricorda che una variabile può assumere solo il valore 00 o 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)

A me non sembra un xor. :)

Somma quelli è solo un gioco di turni e maschere, niente di veramente complesso.

#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;
}

Ecco un esempio di output:

6332350 2525218 1041716 438741 192917 87159 41023 19908 

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

Il programma è stato compilato con:

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

su Fedora 20 con gcc 4.8.2 Il Cpu è un i7 8core.

Probabilmente posso ottenere alcuni parametri di modifica del ms che modificano.

Mentre questo è il tempo di soluzione OP sulla mia macchina:

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

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

Modificare:

Aggiungendo semplicemente openmp e cambiando l'ordine di for I ho un guadagno x3, portando a un miglioramento delle prestazioni x450 rispetto al codice OP. : D In questo caso l' leadingZeroarray deve essere atomico. I globali casuali ... sono casuali, saranno più casuali.

 #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--;
       }
    }
 }

è necessario aggiungere -fopenmpal flag del compilatore


Modifica: 2 Come suggeritore dell'utente71404 ho cambiato le funzioni sumOnes e sumArray e ora è super veloce.

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

Con openmp è più lento, perché l'atomica aggiunge un sovraccarico eccessivo.

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

Senza atomica è ancora più veloce, ma ottengo risultati sbagliati.

2137992 1147218 619297 321243 155815 70946 32919 15579

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

Per capire sumArray considerare che 16 bit rappresentano e un array di 8 numeri.
00 non ha 1 e rappresentano -1
01 e 10 ne hanno 1 e rappresentano 0
11 hanno due 1 e rappresentano 1
Quindi quel built-in conta il numero di bit impostato su 1 [ http://en.wikipedia.org/wiki/ Hamming_weight] e per ogni gruppo rimuoviamo 1. Cool.

sumOnes è solo magia nera.

Qui gli ultimi flag di compilazione e codice.

gcc -std = c ++ 11 -mfpmath = sse -O3 -flto -march = native -funroll-loops -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;
}

Ora non vedo l'ora di provarlo! Purtroppo questo non sarà per alcune ore.

1
Quanto segue era in una modifica suggerita, ma penso che potrebbe adattarsi meglio come commento. Puoi sostituire il tuo sumOnes, sumArray con il seguente (sembra darmi una velocità 2x rispetto alla versione 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; }questo è stato suggerito da @ user71404
ace_HongKongIndependence

@ user71404: il profiler ha detto che il programma ha trascorso tutto il tempo in quelle due funzioni, ma ieri ero davvero stanco di pensare a qualcosa di meglio. Ci proverò stasera (UTC). Grazie.
ilmale,

Ti dispiacerebbe cambiare il secondo snippet di codice per renderlo copia completa e codice pastable? Devo fare qualcosa di sbagliato nei miei tentativi di far funzionare il tuo codice openmp in modo che possa aiutare molto.

Bello. Ho pensato che questo potesse essere fatto con operazioni a bit.
Guy Sirton,

10

Julia: 0,7 secondi, 120 volte più veloce

Come dimostrato da user20768, una porta semplice del codice per Julia è circa due volte più veloce di PyPy. Ma possiamo fare molto meglio di così.

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

Puoi eseguirlo usando julia -p 8 -e 'require("golf.jl");main()' (l'8 è il numero di processi, potresti voler giocare con esso). Nell'ultimo prerelease di Julia questo richiede 0,7s contro 1m22s per PyPy.

Se hai abbastanza core sul tuo computer, e forse fai girare alcune istanze di AWS, dovresti riuscire a radere un po 'di più :)


Sono abbastanza sicuro che stai misurando i tempi sbagliati. Python con Pypy è anche un linguaggio basato su JIT, ma i tempi fatti dall'OP includono il tempo di compilazione JIT. Lo stai escludendo. Ho installato l'ultima versione di Julia git e testato il tuo codice, e sulla mia macchina il tuo comando con 8 processi richiede 6,6 secondi per finire, ma stampa "tempo trascorso 0,588 .. secondi".
semi-estrinseco il

I tempi di Python includono l'avvio PyPy e il riscaldamento JIT, ma ciò richiede al massimo pochi secondi: la differenza in un minuto di runtime è trascurabile. Sono contento se l'OP cambia il modo in cui il Python è temporizzato (non farà alcuna differenza), ma includere il tempo di avvio di Julia non sarebbe ragionevole.
un minuto in più il

Ho chiesto al PO nei commenti alla domanda originale, e ha detto "I tempi dovrebbero includere tutto per le lingue JIT". Ha anche dichiarato che creerà una nuova sfida in cui le soluzioni impiegheranno molto più tempo di 1 secondo, lasciando Julia in competizione.
semi-estrinseco

In tal caso, la soluzione ottimale è utilizzare un algoritmo seriale, che richiede circa 2 secondi. Pubblicherei il codice, ma questa competizione è ora in qualche modo ridondante, poiché tutti sanno già che il C ++ si avvia più velocemente di tutto il resto.
un minuto in più il

Ho appena pubblicato la mia soluzione Fortran, quindi non vedo perché non dovresti pubblicare la Julia più veloce (se hai già il codice).
semi-estrinseco

5

C, 1.210s

Con il codice OP in esecuzione 1m45.729s sulla mia macchina.

Compilazione:

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

Un ringraziamento speciale: @dyp per flag di compilazione e idee per ottimizzazioni.

#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;
}

Uscita campione:

6334411 2527506 1042239 439328 192914 87005 40847 19728

1
Interessante davvero, posso fare osservazioni simili quando faccio cadere tutte quelle bandiere di ottimizzazione. Prova -march=native -fwhole-program -fstrict-aliasing -ftree-vectorizeBtw. Sono arrivato a <4 s usando alcuni C ++ 11 tra cui un MT19937 più un uniform_int_distribution.
dyp,

1
1.119s con una velocità di circa 59!

1
@ace Sì, volevo solo sottolineare questo. Per me è stato più semplice provare alcuni dei PRNG della libreria standard in C ++. Si noti che è possibile utilizzare un risultato intero a 32 bit da un PRNG per produrre 8 voci per F.
dyp,

1
Dato che nè uguale a 8, probabilmente puoi usare AVX (o 2 * SSE) per calcolare il dotproduct con una Smemoria adeguata .
Michael M.

2
Versione SSE, piccola accelerazione: gist.github.com/anonymous/11394210 (non dimenticare di includere smmintrin.h)
Michael M.

5

Perl

Questo non è affatto veloce come la soluzione C, ma è piuttosto veloce per un linguaggio interpretato di alto livello, credo. Rade circa il 40% di sconto sul tempo di esecuzione dell'implementazione di 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;

The Algorithm :: Combinatorics è disponibile in Ubuntu ( sudo apt-get install libalgorithm-combinatorics-perl). Gli altri moduli utilizzati sono moduli Perl core, quindi dovrebbero già essere installati come parte dell'installazione di base di Ubuntu.


1
Non influenzerà la velocità, ma è il 0..N-1range nell'ultimo map, giusto? Hai dimenticato di use warnings? :-) Sebbene la logica in OP sia confusa, la finestra scorrevole non arriva mai all'ultimo elemento di S.
user2846289

Ah. Ho appena immaginato che le matrici fossero di dimensioni non corrispondenti, quindi ho disabilitato warningspermettendo agli elementi mancanti di essere trattati come zero. N-1migliora questo. E in realtà migliora leggermente la velocità: ora è circa il 40% più veloce dell'implementazione di Python.
tobyink

Penso che il tuo codice richieda una versione molto moderna di List :: Util. Su Ubuntu 14.04 ottengo che "any" non viene esportato dal modulo List :: Util

Oh sì, è vero - probabilmente dovrai installare List :: Util off CPAN. anyin alternativa può essere trovato in List :: MoreUtils, che sebbene non sia un modulo core è uno dei moduli CPAN più comunemente usati.
tobyink

4

Julia: 4.66x più lenta!

Sto davvero iniziando a dubitare delle statistiche sul loro sito web ...

Si noti che il seguente codice Julia è effettivamente una trascrizione diretta del codice Python dell'OP senza alcuna ottimizzazione. Uso la time()funzione per escludere il lento tempo di avvio di 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

Codice OP in PyPy: 1 m 11.506 s

Uscita Julia:

6332170
2525472
1041522
438761
193119
86873
40705
19662

7
+1 per la tua <s> spudoratezza </s> sportività.
ace_HongKongIndipendenza

Le variabili globali, le importazioni e la comprensione dell'array sono lenti. Questo non è il modo in cui si scriverebbe un programma Julia per le prestazioni.
Alex A.

4

RPython 0.187s (258x più veloce)

Fonte originale w / PyPy2.2.1: 1m 6.718s

Ora con il threading è stato abbandonato il supporto posteriore per Python standard. Il numero di thread di lavoro può essere specificato come parametro della riga di comando, il valore predefinito è due.

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

RPython è un sottoinsieme limitato di Python, che può essere tradotto in C e quindi compilato usando RPython Toolchain . Il suo scopo espresso è di aiutare nella creazione di interpreti linguistici, ma può anche essere usato per compilare programmi semplici come quello sopra. La maggior parte delle funzionalità "più elaborate" di Python, come itertoolso anchemap non sono disponibili.

Per compilare, creare un clone locale del repository pypy corrente ed eseguire quanto segue:

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

L'eseguibile risultante verrà nominato convolution-c o simile nella directory di lavoro corrente.

Ho parametrizzato le variabili di input, quindi il programma dovrebbe essere eseguito come:

convolution-c 8 8 1000

per abbinare il codice di esempio.


Note di implementazione

S in itertools.product([-1,1], repeat = n+m-1)diventa S in xrange(1<<n+m-1), interpretando Scome una bit map: [ 0, 1] → [ -1, 1]

Allo stesso modo, Fè anche una mappa di bit, con ogni due bit che rappresentano un singolo valore:
[ 00, 01, 10, 11] → [ -1, 0, 0, 1]

Una tabella di verità viene utilizzata per cercare il prodotto, piuttosto che eseguire una mulitplicazione.

Poiché vengono utilizzati numeri interi con segno a 32 bit, non npuò essere maggiore di 15 e n+mnon superiore a 31. Il supporto di numero intero arbitrario può essere ottenuto conrpython.rlib.rbigint Se necessario, è modulo.

La prima iterazione del ciclo punto-prodotto viene srotolata e combinata con il test di nullità di F.

Viene utilizzato un PRNG homebrew, elencato alla fonte. L'autore dell'articolo dimostra un periodo di 2 32 -1 e afferma di aver superato tutti i test di Diehard, tranne uno, sebbene io non l'abbia confermato personalmente.

Il seme casuale cambia ogni millisecondo, il che è altrettanto buono usando un timestamp consentirà. Inoltre, ogni thread di lavoro ha xoril proprio ID di processo con questo valore, per assicurarsi che ognuno abbia un seme diverso.


Tempi di campionamento

2 thread di lavoro:

$ 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 thread di lavoro:

$ 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 thread di lavoro:

$ 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 originale di 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

Tempi per 100000 iterazioni:

$ 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

Non ho mai visto un programma rpython prima d'ora. Questo è fantastico Esiste un programma Python puro equivalente che Pypy può eseguire in 1.03s?

@Lembik Mi piacerebbe vederne uno. Ho pensato che 4.7s fosse abbastanza buono, considerando che il mio primo tentativo di pitone puro era ~ 15s.
primo

Sì, scusa per il ritardo. Non ho ancora il codice in esecuzione ma lo farò al più presto.

Dovresti provare ad aggiungere un JIT. Ora che sarebbe veloce!
kirbyfan64sos,

@Lembik grazie per la menzione;) Per curiosità, ha funzionato più velocemente con 4 thread di lavoro o 8?
primo

3

Julia: 1 min 21.4s (2.2x più veloce) (modifica del codice di Arman)

Codice operativo in PyPy: 3 min 1.4 s

Entrambi eseguiti nel REPL, escluso il tempo per caricare i pacchetti.

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 

Ci sono alcuni problemi con il codice di Arman che lo rende molto lento: utilizza inutilmente molte funzioni anonime e funzioni di ordine superiore. Per verificare se tutto un vettore F è zero, perché non scrivere solo tutto (F == 0) invece di tutto (x-> x == 0, F)? È più corto e letteralmente mille volte più veloce.

Utilizza anche sum (map (*, x, y)) come prodotto punto anziché semplicemente punto (x, y). La prima versione 650 volte più lenta per un vettore di 10k raddoppia. E la funzione del prodotto punto è implementata come un ciclo for in Julia pura.

Inoltre, la comprensione dell'array è lenta. È meglio scrivere [0,1,0, -1] [rand (1: 4, n)] anziché [[-1 0 0 1] [rand (1: 4)] per j = 1: n] .

Infine, le variabili globali sono un pessimo juju a Julia. Julia è veloce solo se scrivi il codice in modo tale da consentire alla JIT e al tipo di inferenza di funzionare. Gran parte di questo è la stabilità del tipo: il compilatore deve essere in grado di essere sicuro che il tipo di una variabile non cambierà durante un ciclo, ad esempio.


Grazie! Vedo che ho ancora molto da imparare sulla Julia Language prima di poter fare affermazioni sulla sua velocità :) Sono davvero felice di vedere che alcune banali correzioni al mio codice aumentano il tempo di esecuzione di diverse volte.
agar agile

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()

Esempio di output:

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

Nimrod si compila in C, quindi anche la scelta del compilatore C per il backend è importante.

Utilizzando clang, compilare con:

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

Utilizzando gcc, compilare con:

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

Omettere --passc:-fltose si dispone di un compilatore C più vecchio che non supporta LTO. Ometti l' --cc=...opzione se stai bene con la scelta predefinita per il compilatore C. Il codice richiede Nimrod 0.9.4 o 0.9.5 .

Sul mio quad core iMac (2,66 GHz core i5), il codice gira in circa .15 secondi con gcc 4.9, .16 secondi con clang, rispetto agli 88 secondi per PyPy 2.2.1 (ovvero una velocità 500+ volte). Sfortunatamente, non ho accesso a una macchina con più di quattro core su cui è installato anche PyPy o dove potrei facilmente installare PyPy, anche se ottengo circa 1 secondo (con molto rumore di misurazione) su un AMD a 64 core Opteron 6376 1,4 GHz (secondo / proc / cpuinfo) con gcc 4.4.6.

L'implementazione cerca di essere fedele all'originale anziché ottimizzare il codice a costo della leggibilità, senza rinunciare a ovvie ottimizzazioni. È interessante notare che la ricorsione della coda initVecRand()è un po 'più veloce di un ciclo con un'istruzione break con gcc e clang. Anche srotolare manualmente una iterazione del convolveloop di test all'interno del loop principale ha prodotto un aumento di velocità, presumibilmente a causa di una migliore previsione del ramo.


Come si ottiene nimrod per Ubuntu?

@Lembik Una rapida ricerca su Google ti darebbe nimrod-lang.org/download.html
ace_HongKongIndependence

@ace Avevo anche incluso il link nel mio post (anche se è difficile vedere con il blu sul nero ora che lo guardo).
Reimer Behrends

Potresti accelerare ulteriormente aumentando la dimensione del seme a 128 bit: xorshift.di.unimi.it
user60561

2

Giava

Ho tradotto la precedente soluzione C ++ in 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));
  }
}

Sul mio computer ottengo il seguente output per il programma 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

Il programma OPs esegue circa 53 secondi sulla mia macchina:

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

Il programma c ++ è stato eseguito solo circa 0,15 secondi:

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

Questo è circa 2,5 volte più veloce della corrispondente soluzione Java (non ho escluso l'avvio della VM). Questa soluzione Java è circa 142 volte più veloce del programma eseguito con PyPy.

Dato che ero personalmente interessato, ho impostato iters 100_000 per Java e C ++, ma il fattore 2.5 non è diminuito a favore di Java se qualcosa è diventato più grande.

EDIT: ho eseguito i programmi su un PC Arch Linux a 64 bit.

EDIT2: Voglio aggiungere che ho iniziato con una traduzione approssimativa del codice 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));
    }
}

Questo programma ha funzionato per circa 3,6 secondi:

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

Che è circa 14 volte più veloce della soluzione PyPy. (La scelta della funzione casuale standard rispetto alla funzione fastRandom porta a un tempo di esecuzione di 5 secondi)


2

Python 3.5 + numpy 1.10.1, 3.76 secondi

I test sono stati eseguiti sul mio Macbook Pro. Il codice OP ha impiegato circa 6 minuti sulla stessa macchina.

Il motivo per cui sto rispondendo a questa domanda in realtà è perché non ho 10 reputazioni e non posso rispondere alla Parte I :-p

Negli ultimi giorni, ho cercato di capire come eseguire convoluzioni enormi in modo efficiente con numpy (senza fare affidamento su un pacchetto di terze parti, anche scipy). Mentre mi sono imbattuto in questa serie di sfide durante la mia ricerca, ho deciso di provarlo. Potrei essere arrivato a questo gioco tardi, ma ecco il mio tentativo di usare Python 3.5 e 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)

Ho pre-calcolato gli array S e F e appiattito l'array S mentre eseguivo la convoluzione, che (sulla base dei miei esperimenti) poteva sfruttare la velocità di np.convolve. In altre parole, poiché non ho trovato una routine di convoluzione vettorializzata, ho falsificato il codice vettoriale appiattendo l'intero array e sperando che np.convolute avrebbe fatto la vettorizzazione sotto il cofano per me, che sembrava funzionare. Nota ho usato mode = 'same' e ho tagliato gli elementi iniziali e finali inutili.

Sul mio Macbook Pro, i risultati del test danno 3,76 secondi . Quando ho eseguito il codice OP (modificato in Python 3.5), ho ottenuto circa 6 minuti . L'accelerazione è di circa 100 volte.

Uno svantaggio è che, poiché le matrici S e F devono essere archiviate, il requisito di memoria può essere un problema se le dimensioni sono troppo grandi.

Ho usato lo stesso metodo per la parte I e ho ottenuto una velocità ~ 60-100x sul mio laptop.

Dato che ho fatto tutto sul mio Macbook Pro, se qualcuno potesse testare il mio codice e farmi sapere come va sul tuo computer, lo apprezzerei molto!


1

J, 130x ~ 50x speedup?

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''

Volte su un debian casuale:

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

Penso che ci siano margini di miglioramento.


Lo script Python dovrebbe essere eseguito usando pypy, non python, motivo per cui il tuo script sembra dare una velocità di 130x.
ace_HongKongIndipendenza

@ace sì, l'ho notato ma non riesco a installare pypy: - / Penso che l'ordine di grandezza rimarrà comunque.
Eelvex,


Anzi, non necessariamente.
Eelvex,

Che problema hai con l'installazione di pypy?

1

C ++: x200 (i7 a 4 core, dovrebbe ridimensionare a x400 su 8 core)

Cercando una soluzione C ++ 11 (testata con VS 2012, gcc e clang) più semplice con parallelizzazione.

Per farlo compilare ed eseguire sotto Linux con gcc 4.8.1:

g ++ -O3 -msse -msse2 -msse3 -march = native -std = c ++ 11 -pthread -Wl, - golf.cpp non necessario

Sotto Linux dobbiamo anche std::launch::asyncforzare più thread. Mi mancava quello in una versione precedente.

In Visual Studio (2012+) questo dovrebbe funzionare ma creare una versione di rilascio per i tempi ...

Sul mio vecchio dual core i3 questo funziona in ~ 0.9 secondi. Sul mio quad core i7 questo è 0,319s contro pypy 66 secondi.

Su un i7 a 8 core, ciò dovrebbe rientrare nell'intervallo di velocità x400. Passare alle matrici in stile C lo accelererebbe ma ero interessato a stare con i contenitori C ++. Per me è interessante vedere lo speedup che puoi ottenere rimanendo relativamente vicino al dominio del problema e ad un livello relativamente alto, qualcosa in cui penso che C ++ sia davvero bravo. Da notare anche la relativa facilità di paralleizzazione usando costrutti C ++ 11.

La soluzione bit di @ ilmale è molto interessante e funziona per -1/1/0. Si potrebbe anche lanciare SSE a questo e forse ottenere un significativo aumento di velocità.

Oltre la parallelizzazione c'è un altro "trucco" che sta riducendo il numero di sommazioni. Risultati del campione: 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: ce l'ho fino a 106x 155x 160x 316x quando utilizzo un Xorshift RNG e OpenMP su una CPU i7 a 4 core. A parte questo, non ci sono grandi trucchi. Affinché l'iteratore costruisca S, utilizzo semplicemente la rappresentazione binaria dell'intero a 16 bit i. Noterai che a parte l'RNG in linea e il "iteratore" / mapping da i a S, il codice è di alto livello come il codice Python.

Modifica: rimosso "if" in Xorshift, ora usando "r = abs (w / ...)" invece di "r = w / ...". Va da 106x a 155x.

Edit2: questo genera 15x tanti numeri casuali quanti la soluzione C ++. Se qualcuno ha una soluzione overhead zero per convertire un int casuale in un array di 0 e 1 in Fortran, sono tutto a posto. Quindi potremmo battere C ++ :)

Edit3: La prima modifica ha introdotto un bug, come ha sottolineato Lembik. Questo problema è stato risolto ora, con un leggero miglioramento della velocità. Proverò a utilizzare il suggerimento di Eelvex per ottenere maggiore velocità.

Edit4: La profilatura indicava che la conversione in reale e di nuovo in intero con nint () era lenta. Ho sostituito questo con una divisione intera facendo sia ridimensionamento che arrotondamento, passando da 160x a 316x speedup.

Compilare con:

gfortran -O3 -march = native -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

Esempio di output:

$ time ./a.out
6329624 2524831 1039787 438809 193044 6860 40486
19517 ./a.out 1.45s utente sistema 0.00s 746% cpu 0.192 totale

Codice OP:

$ time pypy golf.py
pypy golf.py 60.68s user 0.04s system 99% cpu 1: 00.74 total


Quello che ho usato in J era un elenco prefabbricato di 4 ^ n numeri in base-4, quindi convertito in triadico ed escluso 0. L'RNG ha appena scelto da questo elenco.
Eelvex,

Non sono sicuro che il tuo codice sia corretto. Per 100.000 iterazioni ottengo 633140285 271390368 118307997 52751245 23725837 10744292 4944464 2388125 ma penso che dovrebbe essere più vicino al 633170604 252560981 104156146 43911426 19316309 8713324 4073378 1959440. Questa è una differenza costante.

1
Ah, grazie, @Lembik, la mia modifica per accelerare (rimuovendo l'istruzione if) era davvero un bug. Ho aggiornato il mio codice, quindi ora dovrebbe essere corretto. Proverò a pubblicare una versione usando il suggerimento di Eelvex in seguito.
semi-estrinseco

Sembra che abbia accelerato anche quello!

Sì, credo che una leggera accelerazione. Mi sono reso conto che stavo aggiungendo 1.0 e quindi sottraendo 1.0 in un ciclo stretto.
semi-estrinseco
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.