C ++ bisschen Magie
0,84 ms mit einfachem RNG, 1,67 ms mit c ++ 11 std :: knuth
0,16 ms mit geringfügiger algorithmischer Änderung (siehe Bearbeitung unten)
Die Python-Implementierung läuft auf meinem Rig in 7,97 Sekunden. Das ist also 9488- bis 4772-mal schneller, je nachdem, welches RNG Sie auswählen.
#include <iostream>
#include <bitset>
#include <random>
#include <chrono>
#include <stdint.h>
#include <cassert>
#include <tuple>
#if 0
// C++11 random
std::random_device rd;
std::knuth_b gen(rd());
uint32_t genRandom()
{
return gen();
}
#else
// bad, fast, random.
uint32_t genRandom()
{
static uint32_t seed = std::random_device()();
auto oldSeed = seed;
seed = seed*1664525UL + 1013904223UL; // numerical recipes, 32 bit
return oldSeed;
}
#endif
#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
std::pair<unsigned, unsigned> convolve()
{
const uint32_t n = 6;
const uint32_t iters = 1000;
unsigned firstZero = 0;
unsigned bothZero = 0;
uint32_t S = (1 << (n+1));
// generate all possible N+1 bit strings
// 1 = +1
// 0 = -1
while ( S-- )
{
uint32_t s1 = S % ( 1 << n );
uint32_t s2 = (S >> 1) % ( 1 << n );
uint32_t fmask = (1 << n) -1; fmask |= fmask << 16;
static_assert( n < 16, "packing of F fails when n > 16.");
for( unsigned i = 0; i < iters; i++ )
{
// generate random bit mess
uint32_t F;
do {
F = genRandom() & fmask;
} 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 );
// calculate which bits in the expression S * F evaluate to +1
unsigned firstPosBits = ((s1 & posBits) | (~s1 & negBits));
// idem for -1
unsigned firstNegBits = ((~s1 & posBits) | (s1 & negBits));
if ( popcnt( firstPosBits ) == popcnt( firstNegBits ) )
{
firstZero++;
unsigned secondPosBits = ((s2 & posBits) | (~s2 & negBits));
unsigned secondNegBits = ((~s2 & posBits) | (s2 & negBits));
if ( popcnt( secondPosBits ) == popcnt( secondNegBits ) )
{
bothZero++;
}
}
}
}
return std::make_pair(firstZero, bothZero);
}
int main()
{
typedef std::chrono::high_resolution_clock clock;
int rounds = 1000;
std::vector< std::pair<unsigned, unsigned> > out(rounds);
// do 100 rounds to get the cpu up to speed..
for( int i = 0; i < 10000; i++ )
{
convolve();
}
auto start = clock::now();
for( int i = 0; i < rounds; i++ )
{
out[i] = convolve();
}
auto end = clock::now();
double seconds = std::chrono::duration_cast< std::chrono::microseconds >( end - start ).count() / 1000000.0;
#if 0
for( auto pair : out )
std::cout << pair.first << ", " << pair.second << std::endl;
#endif
std::cout << seconds/rounds*1000 << " msec/round" << std::endl;
return 0;
}
Kompilieren Sie in 64-Bit für zusätzliche Register. Bei Verwendung des einfachen Zufallsgenerators laufen die Schleifen in convolve () ohne Speicherzugriff, alle Variablen werden in den Registern gespeichert.
Wie es funktioniert: anstatt zu speichern S
und F
als in-Speicherarrays, als Bits in einem uint32_t gespeichert ist.
Für S
werden die n
niedrigstwertigen Bits verwendet, wobei ein gesetztes Bit +1 und ein nicht gesetztes Bit -1 bezeichnet.
F
Benötigt mindestens 2 Bits, um eine Verteilung von [-1, 0, 0, 1] zu erstellen. Dies erfolgt durch Erzeugen von Zufallsbits und Untersuchen der 16 niedrigstwertigen (aufgerufen r
) und 16 höchstwertigen (aufgerufen l
) Bits . Wenn l & ~r
wir annehmen, dass F +1 ist, ~l & r
nehmen wir an, dass dies F
-1 ist. Ansonsten F
ist es 0. Dies erzeugt die gesuchte Distribution.
Jetzt haben wir S
, posBits
mit einem gesetzten Bit an jedem Ort , an dem F == 1 und negBits
mit einem gesetzten Bit an jedem Ort , an dem F == -1.
Wir können beweisen, dass F * S
(wobei * für Multiplikation steht) unter der Bedingung +1 ergibt (S & posBits) | (~S & negBits)
. Wir können auch eine ähnliche Logik für alle Fälle generieren, in denen F * S
-1 ausgewertet wird. Und schließlich wissen wir, dass sum(F * S)
genau dann 0 ergibt, wenn das Ergebnis die gleiche Anzahl von -1 und +1 enthält. Dies ist sehr einfach zu berechnen, indem einfach die Anzahl von +1 Bits und -1 Bits verglichen werden.
Diese Implementierung verwendet 32-Bit-Ints, und das n
akzeptierte Maximum ist 16. Es ist möglich, die Implementierung durch Ändern des Zufallsgenerierungscodes auf 31 Bit und durch Verwenden von uint64_t anstelle von uint32_t auf 63 Bit zu skalieren.
bearbeiten
Die folgende Faltungsfunktion:
std::pair<unsigned, unsigned> convolve()
{
const uint32_t n = 6;
const uint32_t iters = 1000;
unsigned firstZero = 0;
unsigned bothZero = 0;
uint32_t fmask = (1 << n) -1; fmask |= fmask << 16;
static_assert( n < 16, "packing of F fails when n > 16.");
for( unsigned i = 0; i < iters; i++ )
{
// generate random bit mess
uint32_t F;
do {
F = genRandom() & fmask;
} 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+1));
// generate all possible N+1 bit strings
// 1 = +1
// 0 = -1
while ( S-- )
{
// calculate which bits in the expression S * F evaluate to +1
auto firstBits = (S & mask) ^ adjF;
auto secondBits = (S & ( mask << 1 ) ) ^ ( adjF << 1 );
bool a = desiredBits == popcnt( firstBits );
bool b = desiredBits == popcnt( secondBits );
firstZero += a;
bothZero += a & b;
}
}
return std::make_pair(firstZero, bothZero);
}
verkürzt die Laufzeit auf 0.160-0.161ms. Durch manuelles Abrollen der Schlaufe (oben nicht abgebildet) ergibt sich ein Wert von 0,150. Der weniger triviale Fall n = 10, iter = 100000 läuft unter 250ms. Ich bin mir sicher, dass ich es unter 50 ms schaffen kann, wenn ich zusätzliche Kerne nutze, aber das ist zu einfach.
Dies geschieht, indem der innere Loop-Zweig frei gemacht und der F- und S-Loop vertauscht werden.
Wenn bothZero
es nicht erforderlich ist, kann ich die Laufzeit auf 0,02 ms reduzieren, indem ich alle möglichen S-Arrays sparsam durchschleife.