Wie langsam ist Python wirklich (Teil II)?


52

Dies ist eine Fortsetzung von Wie langsam ist Python wirklich? (Oder wie schnell ist Ihre Sprache?) .

Es stellte sich heraus, dass es ein bisschen zu einfach war, für meine letzte Frage ein x100-Speedup zu bekommen. Für diejenigen, die die Herausforderung genossen haben, aber etwas Schwierigeres wollen, bei dem sie ihre geringen Fähigkeiten wirklich einsetzen können, ist hier Teil II. Die Herausforderung besteht darin, eine x100-Beschleunigung für den folgenden Python-Code zu erhalten, der auf meinem Computer getestet wurde.

Um es schwieriger zu machen, verwende ich diesmal Pypy. Das aktuelle Timing für mich ist 1 Minute und 7 Sekunden mit Pypy 2.2.1.

Regeln

  1. Die erste Person, die Code einreicht, den ich ausführen kann, der korrekt ist und der auf meinem Computer 100-mal schneller ist, erhält eine Prämie von 50 Punkten.
  2. Ich werde den Gewinn nach einer Woche an den schnellsten Code vergeben.
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

Die Ausgabe sollte ähnlich sein

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

Sie müssen einen zufälligen Startwert in Ihrem Code verwenden, und jeder Zufallszahlengenerator, der gut genug ist, um Antworten in der Nähe der oben genannten zu geben, wird akzeptiert.

Mein Computer Die Timings werden auf meinem Computer ausgeführt. Dies ist eine Ubuntu-Standardinstallation auf einem AMD FX-8350 Eight-Core-Prozessor. Dies bedeutet auch, dass ich in der Lage sein muss, Ihren Code auszuführen.

Erklärung des Codes

Dieser Code durchläuft alle Arrays S der Länge n + m-1, die sich aus -1s und 1s zusammensetzen. Für jedes Array S werden 1000 von Null verschiedene zufällige Arrays F der Länge n abgetastet, die aus -1,0 oder 1 bestehen, mit einer Wahrscheinlichkeit von 1/4, 1/2, / 14, jeden Wert zu nehmen. Dann berechnet es die inneren Produkte zwischen F und jedem Fenster von S der Länge n, bis es ein inneres Produkt ungleich Null findet. Es addiert 1 zu leadingzerocountsjeder Position, an der es ein inneres Produkt von Null gefunden hat.

Status

  • Perl . 2,7-fache Verlangsamung durch @tobyink. (Im Vergleich zu Pypy nicht Cpython.)

  • J . 39-fache Beschleunigung durch @Eelvex.

  • C . 59-fache Beschleunigung durch @ace.
  • Julia . 197-mal schneller, ohne Startzeit von @ one-more-minute. 8,5-fache Geschwindigkeit einschließlich Startzeit (in diesem Fall sind 4 Prozessoren schneller als 8).
  • Fortran . 438-fache Beschleunigung durch @ semi-extrinsic.
  • Rpython . 258-fache Beschleunigung durch @primo.
  • C ++ . 508-fache Beschleunigung durch @ilmale.

(Ich habe aufgehört, die neuen Verbesserungen zeitlich festzulegen, weil sie zu schnell und die Iterationen zu klein waren.)


Es wurde darauf hingewiesen, dass Timings unter einer Sekunde unzuverlässig sind und auch einige Sprachen Startkosten verursachen. Das Argument ist, dass, wenn Sie einschließen möchten, Sie auch die Kompilierungszeit von C / C ++ usw. einschließen sollten. Hier sind die Zeiten für den schnellsten Code mit der Anzahl der Iterationen, die auf 100.000 erhöht wurden.

  • Julia . 42 Sekunden von @ one-more-minute.
  • C ++ . 14 Sekunden von @GuySirton.
  • Fortran . 14s von @ semi-extrinsic.
  • C ++ . 12s von @ilmale.
  • Rpython . 18s von @primo.
  • C ++ . 5s von @Stefan.

Der Gewinner ist .. Stefan!

Follow-up-Challenge veröffentlicht. Wie hoch kannst du gehen? (Eine Codierung + Algorithmen Herausforderung) . Dieser ist schwieriger.


3
Eine Erklärung dessen, was der Code erreichen soll, wäre nett, damit wir ihn umschreiben und nicht einfach portieren können
Einacio

6
" Die erste Person, die Code einreicht, den ich ausführen kann, der korrekt ist und der auf meinem Computer 100-mal schneller ist, gewinnt sofort und der Wettbewerb endet. " Was ist der Zweck, um den Wettbewerb so zu schließen? Warum nicht einen Termin wie die meisten anderen verwenden, um ihn in anderen Sprachen noch weiter zu verkürzen?
HainenNL

5
@Einacio Das ist eine schöne Idee. Ich habe die Regeln geändert, von denen ich hoffe, dass es niemanden stört.

1
@Lembik Ich habe meine Fortran-Version verbessert und sie auf meinem Computer 2x schneller gemacht. Könntest du es nochmal probieren? :)
semi-extrinsic

1
@ semi-extrinsic Fertig.

Antworten:


12

C ++ bisschen Magie

~ 16 ms Multithreading, 56 ms Singlethreading. ~ 4000 Beschleunigung.

(Die Beschleunigung basiert auf Multithread-Code auf meinem i7-2820QM und den in der Frage genannten 1 min 9 Sekunden. Da das OP-System eine schlechtere Single-Thread-Leistung als meine CPU, aber eine bessere Multi-Thread-Leistung aufweist, erwarte ich, dass diese Zahl korrekt ist.)

Der Multithread-Teil ist aufgrund des Ablaichens von Threads ziemlich ineffizient. Ich könnte es wahrscheinlich besser machen, indem ich meine benutzerdefinierte Jobbibliothek nutze, aber diese hat Fehler unter Unix-Systemen. Eine Erklärung und fast identischen Code ohne Threading finden Sie unter https://codegolf.stackexchange.com/a/26485/20965 .

bearbeiten

Ich gab jedem Thread ein eigenes RNG und reduzierte die Bitlänge auf 32, was die Laufzeit um einige ms verkürzte.

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

Beispielausgabe:

   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,

Die Ausgabe stimmt meiner Meinung nach nicht, vielleicht liegt ein Fehler vor? Vergleichen Sie mit dem, was in der Frage steht. Insbesondere sollte die letzte Spalte eine Zahl nahe 19180 sein.

@Lembik Ich kann sehen, was du meinst. Ich denke, dass die zufällige Ausgabe nicht zufällig genug ist, was manchmal zu einer flippigen Ausgabe führt. Mit dem C ++ 11 Zufallsgenerator funktioniert es gut. Ich werde den Code später heute beheben.
Stefan

Ich erhalte Stefan.cpp: 104: 101: Fehler: 'memcpy' wurde in diesem Bereich nicht deklariert memcpy (out.data () + (threadId * m), leadingZeros.data (), sizeof (leadingZeros [0]) * m );

Ich denke, dass ich string.h einschließen muss. Versuche es noch einmal.
Stefan

Sie kompilieren dies mit g ++ -O3 -std = c ++ 0x -pthread -Wl, - no-as-needed Stefan.cpp -o Stefan

16

C ++ x 150 x 450 x 530

Anstelle eines Arrays habe ich Bits (und dunkle Magie) verwendet.
Danke @ace für die schnellere Zufallsfunktion.

So funktioniert es: Die ersten 15 Bits der Ganzzahl sstellen das Array dar S[15]; Die Nullen stehen für -1, die Einsen für +1. Das Array Fist auf ähnliche Weise aufgebaut. Aber mit zwei Bits für jedes Symbol.

  • 00 steht für -1
  • 01 und 10 stehen für 0
  • 11 stellen 1 dar

Verursache Sund Fhabe eine andere Darstellung, mit der ich mich verschachteln Smuss, um vergleichbar zu sein F.

  • 0 (-1) wurde 00 (-1 in der Darstellung von F)
  • Aus 1 (+1) wurde 11 (+1 in der Darstellung von F)

Jetzt können wir einfach Carnot verwenden, um das innere Produkt zu berechnen. Beachten Sie, dass eine Variable nur den Wert 00 oder 11 annehmen kann

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)

Sieht aus wie ein Nicht-Xor für mich. :)

Alles in allem ist es nur ein Spiel mit Verschiebung und Maske, nichts wirklich Komplexes.

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

Hier eine Beispielausgabe:

6332350 2525218 1041716 438741 192917 87159 41023 19908 

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

Das Programm wurde kompiliert mit:

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

auf Fedora 20 mit gcc 4.8.2 Die CPU ist ein i7 8core.

Wahrscheinlich kann ich einige ms erhalten, die Compilerparameter zwicken.

Während dies die OP-Lösungszeit auf meinem Computer ist:

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

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

Bearbeiten:

Füge einfach openmp hinzu und ändere die Reihenfolge von, denn ich habe eine x3-Verstärkung, was zu einer x450-Leistungsverbesserung gegenüber dem OP-Code führt. : D In diesem Fall leadingZeromuss das Array atomar sein. Die zufälligen globalen ... sind zufällig, sie werden zufälliger sein.

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

müssen zum -fopenmpCompiler-Flag hinzugefügt werden


Edit: 2 Als Suggester von user71404 habe ich die Funktionen sumOnes und sumArray geändert und jetzt ist es superschnell.

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

Mit OpenMP ist langsamer, weil die Atomics zu viel Overhead hinzufügen.

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

Ohne Atomics geht es noch schneller, aber ich bekomme falsches Ergebnis.

2137992 1147218 619297 321243 155815 70946 32919 15579

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

Um sumArray zu verstehen, muss berücksichtigt werden, dass 16 Bit ein Array von 8 Zahlen darstellen.
00 habe keine 1 und repräsentiere -1
01 und 10 habe eine 1 und repräsentiere 0
11 habe zwei 1 und repräsentiere 1
Damit die eingebaute Anzahl der auf 1 gesetzten Bits zählt [ http://en.wikipedia.org/wiki/ Hamming_weight] und zu jeder Gruppe entfernen wir 1. Cool.

sumOnes ist nur schwarze Magie.

Hier kompilieren die neuesten Flags und Code.

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

Jetzt kann ich es kaum erwarten, das zu testen! Leider wird dies nicht für ein paar Stunden sein.

1
Das Folgende war in einer vorgeschlagenen Bearbeitung, aber ich denke, dass es als Kommentar besser passen könnte. Sie können Ihre sumOnes, sumArray durch folgende ersetzen (scheint mir eine doppelte Geschwindigkeit gegenüber der openmp-Version zu geben). 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; }Dies wurde von @ user71404
ace_HongKongIndependence 30.04.14

@ user71404: profiler sagte, dass das programm all seine zeit in diesen beiden funktionen verbracht hat, aber ich war gestern wirklich müde, denke etwas besseres darüber nach. Ich werde es heute Abend versuchen (UTC). Vielen Dank.
Ilmale

Würde es Ihnen etwas ausmachen, das zweite Code-Snippet so zu ändern, dass es sich um den vollständigen und einfügbaren Code handelt? Ich muss etwas falsch machen, wenn ich versuche, Ihren OpenMP-Code zum Laufen zu bringen, damit dies viel hilft.

Nett. Ich dachte, das könnte mit Bitoperationen gemacht werden.
Guy Sirton

10

Julia: 0,7s, 120x schneller

Wie user20768 gezeigt hat, ist ein unkomplizierter Port des Codes für Julia ungefähr doppelt so schnell wie PyPy. Aber wir können noch viel besser machen.

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

Sie können dies mit ausführen julia -p 8 -e 'require("golf.jl");main()'(die 8 ist die Anzahl der Prozesse, mit denen Sie möglicherweise herumspielen möchten). In der letzten Julia-Vorabversion dauert dies 0,7s gegenüber 1m22s für PyPy.

Wenn Sie genug Kerne auf Ihrem Computer haben und vielleicht ein paar AWS-Instanzen hochfahren, sollten Sie in der Lage sein, weitere zu rasieren :)


Ich bin mir ziemlich sicher, dass Sie das Timing falsch messen. Python mit Pypy ist ebenfalls eine JIT-basierte Sprache, aber die vom OP vorgenommenen Timings enthalten die JIT-Kompilierungszeit. Sie schließen es aus. Ich habe die neueste Julia-Git-Version installiert und Ihren Code getestet. Auf meinem Computer dauert es 6,6 Sekunden, bis Ihr Befehl mit 8 Prozessen ausgeführt ist. Es wird jedoch "Verstrichene Zeit 0,588 Sekunden" ausgegeben.
semi-extrinsische

Das Python-Timing beinhaltet zwar den PyPy-Start und das JIT-Aufwärmen, dies dauert jedoch höchstens Sekunden - der Unterschied über eine Minute Laufzeit ist vernachlässigbar. Ich bin froh, wenn das OP die Python-Zeit ändert (es macht keinen Unterschied), aber es wäre nicht vernünftig, Julias Startzeit zu berücksichtigen.
Eine weitere Minute am

Ich habe das OP in den Kommentaren zur ursprünglichen Frage gefragt und er sagte: "Der Zeitplan sollte alles für JIT-Sprachen enthalten." Er erklärte auch, er werde eine neue Herausforderung schaffen, bei der die Lösungen viel länger als eine Sekunde dauern und Julia im Wettbewerb zurückbleiben werde.
semi-extrinsic

In diesem Fall ist die optimale Lösung die Verwendung eines seriellen Algorithmus - das dauert ungefähr 2 Sekunden. Ich würde den Code veröffentlichen, aber diese Konkurrenz ist jetzt etwas überflüssig, da jeder bereits weiß, dass C ++ schneller bootet als alles andere.
Eine weitere Minute

Ich habe gerade meine Fortran-Lösung gepostet, daher verstehe ich nicht, warum Sie nicht die schnellste Julia-Lösung posten sollten (wenn Sie den Code bereits haben).
Semi-Extrinsic

5

C, 1,210 s

Mit OPs Code läuft 1m45.729s auf meinem Rechner.

Zusammenstellung:

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

Besonderer Dank geht an @dyp für die Zusammenstellung von Flags und Optimierungsideen.

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

Beispielausgabe:

6334411 2527506 1042239 439328 192914 87005 40847 19728

1
Interessanterweise kann ich ähnliche Beobachtungen machen, wenn ich all diese Optimierungsflags fallen lasse. Versuchen Sie -march=native -fwhole-program -fstrict-aliasing -ftree-vectorizeBtw. Ich bin mit etwas C ++ 11 auf <4 s gekommen, einschließlich eines MT19937 plus eines uniform_int_distribution.
Typ

1
1.119s machen eine Beschleunigung von etwa 59!

1
@ace Ja, ich wollte nur darauf hinweisen. Es war für mich einfacher, nur einige der PRNGs der Standardbibliothek in C ++ auszuprobieren. Beachten Sie, dass Sie ein 32-Bit-Integer-Ergebnis aus einem PRNG verwenden können, um 8 Einträge für zu erzeugen F.
Typ

1
Da nist gleich, 8können Sie wahrscheinlich AVX (oder 2 * SSE) verwenden, um das Punktprodukt mit einem richtigen SSpeicher zu berechnen .
Michael M.

2
SSE-Version, kleine Beschleunigung: gist.github.com/anonymous/11394210 (vergessen Sie nicht einzuschließensmmintrin.h )
Michael M.

5

Perl

Dies ist bei weitem nicht so schnell wie die C-Lösung, aber für eine hochinterpretierte Sprache, denke ich, ziemlich schnell. Es spart etwa 40% der Laufzeit der Python-Implementierung.

#!/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;

Der Algorithmus :: Combinatorics ist in Ubuntu ( sudo apt-get install libalgorithm-combinatorics-perl) verfügbar . Die anderen verwendeten Module sind Perl-Kernmodule und sollten daher bereits im Rahmen der Ubuntu-Basisinstallation installiert werden.


1
Es wird die Geschwindigkeit nicht beeinflussen, aber die 0..N-1Reichweite im letzten map, oder? Hast du es vergessen use warnings? :-) Obwohl die Logik in OP verwirrend ist, gelangt das Schiebefenster nie zum letzten Element von S.
user2846289

Ah. Ich habe gerade herausgefunden, dass die Arrays nicht übereinstimmende Größen haben, also habe ich es deaktiviert, warningsdass die fehlenden Elemente als Null behandelt werden. N-1verbessert dies. Tatsächlich verbessert es die Geschwindigkeit geringfügig - es ist jetzt ungefähr 40% schneller als die Python-Implementierung.
Tobyink

Ich denke, Ihr Code erfordert eine sehr moderne Version von List :: Util. Am Ubuntu 14.04 bekomme ich "any" wird nicht vom List :: Util-Modul exportiert

Oh ja, das stimmt - wahrscheinlich müssen Sie List :: Util außerhalb von CPAN installieren. anyfinden Sie alternativ in List :: MoreUtils, einem der am häufigsten verwendeten CPAN-Module, obwohl es sich nicht um ein Kernmodul handelt.
Tobyink

4

Julia: 4.66x langsamer!

Ich fange wirklich an, die Statistiken auf ihrer Website zu bezweifeln ...

Beachten Sie, dass der folgende Julia-Code effektiv eine direkte Transkription des Python-Codes des OP ohne Optimierungen ist. Ich benutze die time()Funktion, um Julias langsame Startzeit auszuschließen ...

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

OP-Code in PyPy: 1 m 11.506 s

Julia Ausgabe:

6332170
2525472
1041522
438761
193119
86873
40705
19662

7
+1 für Ihre <s> Schamlosigkeit </ s> Sportlichkeit.
ace_HongKongIndependence

Globale Variablen, Importe und Array-Verständnis sind langsam. So schreibt man normalerweise kein Julia-Programm für Aufführungen.
Alex A.

4

RPython 0.187s (258x schneller)

Originalquelle mit PyPy2.2.1: 1m 6.718s

Mit Threading wurde die Unterstützung für Standard-Python aufgegeben. Die Anzahl der Worker-Threads kann als Befehlszeilenparameter angegeben werden. Der Standardwert ist zwei.

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 ist eine eingeschränkte Teilmenge von Python, die in C übersetzt und dann mit der RPython-Toolchain kompiliert werden kann . Ihr ausdrücklicher Zweck ist es, die Erstellung von Sprachinterpreten zu unterstützen, aber sie können auch dazu verwendet werden, einfache Programme wie das oben beschriebene zu kompilieren. Die meisten der "schickeren" Funktionen von Python, wie z. B. itertoolsoder sogar mapnicht verfügbar.

Erstellen Sie zum Kompilieren einen lokalen Klon des aktuellen Pypy-Repositorys und führen Sie Folgendes aus:

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

Die resultierende ausführbare Datei wird convolution-cim aktuellen Arbeitsverzeichnis benannt oder ähnlich.

Ich habe die Eingabevariablen parametrisiert, daher sollte das Programm wie folgt ausgeführt werden:

convolution-c 8 8 1000

den Beispielcode zu entsprechen.


Implementierungshinweise

S in itertools.product([-1,1], repeat = n+m-1)wird S in xrange(1<<n+m-1), interpretiert Sals Bitmap: [ 0, 1] → [ -1, 1]

Ebenso Fist auch ein Bit - Map, mit jeweils zwei Bits einen einzelnen Wert repräsentiert:
[ 00, 01, 10, 11] → [ -1, 0, 0, 1]

Eine Wahrheitstabelle wird zum Nachschlagen des Produkts verwendet, anstatt eine Multiplikation durchzuführen.

Da vorzeichenbehaftete 32-Bit-Ganzzahlen verwendet werden, ndürfen diese nicht größer als 15 und n+mnicht größer als 31 sein. Mit dem rpython.rlib.rbigintModul kann bei Bedarf eine beliebige Ganzzahlunterstützung erzielt werden .

Die erste Iteration der Skalarproduktschleife wird abgewickelt und mit dem Nullitätstest von kombiniert F.

Es wird ein Homebrew-PRNG verwendet, der als Quelle aufgeführt ist. Der Autor des Papiers weist einen Zeitraum von 2 32 -1 nach und behauptet, dass er alle Diehard-Tests mit Ausnahme eines bestand, obwohl ich dies nicht persönlich bestätigt habe.

Der zufällige Startwert ändert sich jede Millisekunde, was bei Verwendung eines Zeitstempels so gut wie möglich ist. Darüber hinaus weist jeder Arbeitsthread xorseine Prozess-ID mit diesem Wert auf, um sicherzustellen, dass jeder einen anderen Startwert hat.


Beispiel-Timings

2 Arbeitsthreads:

$ 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 Arbeitsthreads:

$ 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 Arbeitsthreads:

$ 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

Originalquelle von 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

Timing für 100000 Iterationen:

$ 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

Ich habe noch nie ein Rpython-Programm gesehen. Das ist toll. Gibt es jetzt ein gleichwertiges reines Python-Programm, das pypy in 1.03s ausführen kann?

@Lembik Ich würde gerne einen sehen. Ich fand 4.7s ziemlich gut, wenn man bedenkt, dass mein erster Versuch mit reinem Python ~ 15s war.
Primo

Ja, entschuldigen Sie die Verspätung. Ich habe den Code noch nicht am Laufen, werde ihn aber so bald wie möglich veröffentlichen.

Sie sollten versuchen, eine JIT hinzuzufügen. Nun, das wäre schnell!
kirbyfan64sos

@Lembik danke für die Erwähnung;) Aus Neugier lief es am schnellsten mit 4 Arbeitsthreads oder 8?
Primo

3

Julia: 1 min 21.4s (2.2x schneller) (Änderung von Armans Code)

Ops Code in PyPy: 3 min 1,4 s

Beides erfolgt in der REPL, ohne Zeit zum Laden von Paketen.

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 

Es gibt einige Probleme damit, dass Armans Code sehr langsam ist: Er verwendet unnötigerweise viele anonyme Funktionen und Funktionen höherer Ordnung. Um zu testen, ob der gesamte Vektor F Null ist, schreiben Sie einfach alle (F == 0) anstelle aller (x-> x == 0, F). Es ist kürzer und buchstäblich tausendmal schneller.

Es wird auch sum (map (*, x, y)) als Skalarprodukt verwendet, anstatt nur Skalar (x, y). Die erste Version ist 650-mal langsamer für einen Vektor von 10k-Doubles. Und die Skalarproduktfunktion ist als for-Schleife in pure Julia implementiert.

Auch das Array-Verständnis ist langsam. Es ist besser, [0,1,0, -1] [rand (1: 4, n)] anstelle von [[-1 0 0 1] [rand (1: 4)] für j = 1: n zu schreiben. .

Schließlich sind globale Variablen in Julia schlechte Juju. Julia ist nur dann schnell, wenn Sie Code so schreiben, dass die JIT- und Typinferenz funktioniert. Ein großer Teil davon ist die Typstabilität: Der Compiler muss sicherstellen können, dass sich der Typ einer Variablen beispielsweise innerhalb einer Schleife nicht ändert.


Vielen Dank! Ich sehe, dass ich noch einiges über die Julia-Sprache lernen muss, bevor ich ihre Geschwindigkeit in Anspruch nehmen kann :) Ich bin wirklich froh zu sehen, dass ein paar geringfügige Korrekturen an meinem Code die Ausführungszeit um ein Vielfaches verlängern.
Flinker Agar

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

Beispielausgabe:

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

Nimrod kompiliert nach C, daher ist die Wahl des C-Compilers auch für das Backend von Bedeutung.

Kompilieren Sie mit clang mit:

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

Kompilieren Sie mit gcc mit:

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

Lassen --passc:-fltoSie es aus, wenn Sie einen älteren C-Compiler haben, der LTO nicht unterstützt. Lassen Sie die --cc=...Option aus, wenn Sie mit der Standardauswahl für den C-Compiler zufrieden sind. Der Code erfordert Nimrod 0.9.4 oder 0.9.5 .

Auf meinem Quadcore-iMac (2,66-GHz-Core i5) läuft der Code mit gcc 4.9 in ungefähr 0,15 Sekunden, mit clang in 0,16 Sekunden, verglichen mit 88 Sekunden für PyPy 2.2.1 (dh eine Beschleunigung um mehr als das Doppelte). Leider habe ich keinen Zugriff auf eine Maschine mit mehr als vier Kernen, auf der auch PyPy installiert ist oder auf der PyPy problemlos installiert werden kann, obwohl ich auf einem 64-Kern-AMD etwa 0,1 Sekunden (mit viel Messrauschen) erhalte Opteron 6376 1.4 GHz (nach / proc / cpuinfo) mit gcc 4.4.6.

Die Implementierung versucht, dem Original treu zu bleiben, anstatt den Code auf Kosten der Lesbarkeit zu optimieren, ohne auf offensichtliche Optimierungen zu verzichten. Interessanterweise ist die Schwanzrekursion in initVecRand()etwas schneller als eine Schleife mit einer break-Anweisung mit gcc und clang. Das manuelle Abrollen einer Iteration der convolveTestschleife innerhalb der Hauptschleife führte ebenfalls zu einer Beschleunigung, vermutlich aufgrund einer besseren Verzweigungsvorhersage.


Wie bekommt man Nimrod für Ubuntu?

@Lembik Eine schnelle Google-Suche würde Ihnen nimrod-lang.org/download.html
ace_HongKongIndependence

@ace Ich hatte auch den Link in meinen Beitrag aufgenommen (obwohl es bei Blau auf Schwarz schwer zu erkennen ist, wenn ich es mir jetzt ansehe).
Reimer Behrends

Sie könnten dies noch weiter beschleunigen, indem Sie die Startgröße
user60561

2

Java

Ich habe die obige C ++ - Lösung nach Java übersetzt:

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

Auf meinem Computer erhalte ich folgende Ausgabe für das Java-Programm:

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

Das OP-Programm läuft auf meinem Rechner ca. 53 Sekunden:

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

Das c ++ Programm führte nur ca. 0,15 Sekunden aus:

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

Das ist ungefähr 2,5x schneller als die entsprechende Java-Lösung (ich habe den VM-Start nicht ausgeschlossen). Diese Java-Lösung ist ca. 142x schneller als das mit PyPy ausgeführte Programm.

Da ich persönlich interessiert war, habe ich itersfür Java und C ++ auf 100_000 gesetzt , aber der Faktor 2,5 hat nicht zugunsten von Java abgenommen, wenn etwas größer wurde.

BEARBEITEN: Ich habe die Programme auf einem 64-Bit-Arch-Linux-PC ausgeführt.

EDIT2: Ich möchte hinzufügen, dass ich mit einer groben Übersetzung des Python-Codes begonnen habe:

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

Dieses Programm lief etwa 3,6 Sekunden:

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

Welches ist etwa 14-mal schneller als die PyPy-Lösung. (Die Auswahl der Standard-Zufallsfunktion gegenüber der FastRandom-Funktion führt zu einer Ausführungszeit von 5 Sekunden.)


2

Python 3.5 + numpy 1.10.1, 3.76 Sekunden

Die Tests wurden auf meinem Macbook Pro ausgeführt. Der OP-Code benötigte auf demselben Computer ~ 6 Minuten.

Der Grund, warum ich diese Frage beantworte, ist, dass ich nicht über 10 Reputationen verfüge und Teil I nicht beantworten kann :-p

In den letzten Tagen habe ich versucht, herauszufinden, wie man mit numpy massive Windungen effizient ausführt (ohne auf ein Paket eines Drittanbieters angewiesen zu sein, selbst scipy). Als ich während meiner Recherche auf diese Reihe von Herausforderungen stieß, beschloss ich, es auszuprobieren. Ich bin vielleicht zu spät zu diesem Spiel gekommen, aber hier ist mein Versuch, Python 3.5 und Numpy 1.10.1 zu verwenden.

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)

Ich habe die S- und F-Arrays vorberechnet und das S-Array während der Durchführung der Faltung abgeflacht, was (basierend auf meinen Experimenten) die Geschwindigkeit von np.convolve ausnutzen könnte. Mit anderen Worten, da ich keine vektorisierte Faltungsroutine gefunden habe, habe ich den Code fälschlicherweise vektorisiert, indem ich das gesamte Array abgeflacht habe und gehofft, dass np.convolved die Vektorisierung unter der Haube für mich erledigen würde, was anscheinend funktioniert. Hinweis: Ich habe mode = 'same' verwendet und die führenden und nachfolgenden Elemente, die nutzlos waren, abgeschnitten.

Auf meinem Macbook Pro ergeben die Testergebnisse 3,76 Sekunden . Als ich den auf Python 3.5 geänderten OP-Code ausführte, hatte ich ungefähr 6 Minuten Zeit . Die Beschleunigung beträgt etwa das 100-fache.

Ein Nachteil ist, dass der Speicherbedarf ein Problem sein kann, wenn die Größen zu groß sind, da die S- und F-Arrays gespeichert werden sollen.

Ich habe die gleiche Methode für Teil I verwendet und auf meinem Laptop eine ~ 60-100-fache Geschwindigkeit erreicht.

Wenn jemand meinen Code testen und mir mitteilen könnte, wie er auf Ihrem Computer abläuft, würde ich mich sehr freuen, wenn ich alles auf meinem Macbook Pro tun würde!


1

J, 130x ~ 50x Beschleunigung?

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

Mal auf einem zufälligen Debian:

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

Ich denke, es gibt Raum für Verbesserungen.


Das Python-Skript soll nicht mit ausgeführt werden pypy, pythonweshalb Ihr Skript 130x schneller zu sein scheint.
ace_HongKongIndependence

@ace habe ich ja gemerkt aber ich kann pypy nicht installieren: - / ich denke die größenordnung bleibt aber.
Eelvex


In der Tat nicht unbedingt.
Eelvex

Was für ein Problem hast du bei der Installation von pypy?

1

C ++: x200 (4-Core i7, sollte auf 8-Core auf x400 skaliert werden)

Versuch einer einfacheren C ++ 11- Lösung (getestet mit VS 2012, gcc und clang) mit Parallelisierung.

Um dies zu kompilieren und unter Linux mit gcc 4.8.1 auszuführen:

g ++ -O3 -msse -msse2 -msse3 -march = native -std = c ++ 11 -pthread -Wl, - nicht benötigte golf.cpp

Unter Linux müssen wir auch std::launch::asyncmehrere Threads erzwingen. Mir hat das in einer früheren Version gefehlt.

In Visual Studio (2012+) sollte dies funktionieren, aber ein Release für das Timing erstellen ...

Auf meinem alten Dual Core i3 dauert das ~ 0,9 Sekunden. Auf meinem i7 Quad Core sind das 0,319s vs. 66 Sekunden.

Auf einem 8-Core i7 sollte dies im x400-Beschleunigungsbereich liegen. Das Wechseln zu Arrays im C-Stil würde das beschleunigen, aber ich war daran interessiert, bei C ++ - Containern zu bleiben. Für mich ist es interessant zu sehen, welche Beschleunigung Sie erzielen können, während Sie relativ nah an der Problemdomäne und auf einem relativ hohen Niveau bleiben. Ich denke, C ++ kann das wirklich gut. Bemerkenswert ist auch die relativ einfache Paralleisierung mit C ++ 11-Konstrukten.

Die Bit-Lösung von @ ilmale ist sehr cool und funktioniert für -1/1/0. Man könnte auch SSE auf diese werfen und vielleicht eine erhebliche Beschleunigung bekommen.

Über die Parallelisierung hinaus gibt es einen weiteren "Trick", der die Anzahl der Summierungen verringert. Beispielergebnisse: 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

Okay, Fortran: Ich habe eine Beschleunigung von bis zu 106 x 155 x 160 x 316 x, wenn ich ein Xorshift-RNG und OpenMP auf einer 4-Kern-i7-CPU verwende. Ansonsten gibt es keine großen Tricks. Damit der Iterator S konstruiert, verwende ich nur die binäre Darstellung der 16-Bit-Ganzzahl i. Sie werden feststellen, dass der Code, abgesehen vom Inline-RNG und dem "Iterator" / Mapping von i nach S, genauso hoch ist wie der Python-Code.

Bearbeiten: Das "Wenn" in Xorshift wurde entfernt, jetzt wird "r = abs (w / ...)" anstelle von "r = w / ..." verwendet. Geht von 106x bis 155x.

Edit2: Dies erzeugt 15x so viele Zufallszahlen wie die C ++ - Lösung. Wenn jemand eine Zero-Overhead-Lösung hat, um ein zufälliges int in ein Array von 0s und 1s in Fortran zu konvertieren, bin ich ganz Ohr. Dann könnten wir C ++ schlagen :)

Edit3: Die erste Bearbeitung führte zu einem Fehler, wie Lembik betonte. Dies ist jetzt behoben, mit einer winzigen Verbesserung der Geschwindigkeit. Ich werde versuchen, den Vorschlag von Eelvex zu nutzen, um die Geschwindigkeit zu erhöhen.

Edit4: Die Profilerstellung zeigte an, dass die Konvertierung in real und zurück in integer mit nint () langsam war. Ich habe dies durch eine Ganzzahldivision ersetzt, die sowohl skaliert als auch rundet und dabei von 160x auf 316x beschleunigt.

Kompilieren mit:

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

Beispielausgabe:

$ time ./a.out
6329624 2524831 1039787 438809 193044 6860 40486 19517
./a.out 1,45s user 0.00s system 746% cpu 0.192 total

OP-Code:

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


Was ich in J verwendet habe, war eine vorgefertigte Liste von 4 ^ n Zahlen in Basis 4, die dann in Triade und ohne 0 umgewandelt wurde. Der RNG wählt nur aus dieser Liste aus.
Eelvex

Ich bin nicht sicher, ob Ihr Code korrekt ist. Für 100.000 Iterationen erhalte ich 633140285 271390368 118307997 52751245 23725837 10744292 4944464 2388125, aber ich denke, es sollte näher an 633170604 252560981 104156146 43911426 19316309 8713324 4073378 1959440 liegen.

1
Ah, danke, @Lembik, meine Bearbeitung zum Beschleunigen (Entfernen der if-Anweisung) war in der Tat ein Fehler. Ich habe meinen Code aktualisiert, sodass er jetzt korrekt sein sollte. Ich werde später versuchen, eine Version mit dem Vorschlag von Eelvex zu veröffentlichen.
Semi-Extrinsic

Das hat es anscheinend auch beschleunigt!

Ja, eine leichte Beschleunigung, denke ich. Mir wurde klar, dass ich 1.0 addierte und dann 1.0 innerhalb einer engen Schleife subtrahierte.
Semi-Extrinsic
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.