C ++ bit magic
0,84 ms con RNG semplice, 1,67 ms con c ++ 11 std :: knuth
0.16ms con leggera modifica algoritmica (vedi modifica sotto)
L'implementazione di Python viene eseguita in 7,97 secondi sul mio rig. Quindi, questo è da 9488 a 4772 volte più veloce a seconda di quale RNG scegli.
#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;
}
Compilare in 64 bit per registri extra. Quando si utilizza il semplice generatore casuale i loop in convolve () vengono eseguiti senza alcun accesso alla memoria, tutte le variabili vengono memorizzate nei registri.
Come funziona: piuttosto che archiviare S
e F
come array in memoria, viene memorizzato come bit in un uint32_t.
Per S
, i n
bit meno significativi sono usati dove un bit impostato indica un +1 e un bit non impostato indica un -1.
F
richiede almeno 2 bit per creare una distribuzione di [-1, 0, 0, 1]. Questo viene fatto generando bit casuali ed esaminando i 16 bit meno significativi (chiamati r
) e 16 più significativi (chiamati l
). Se l & ~r
assumiamo che F sia +1, se ~l & r
assumiamo che F
sia -1. Altrimenti F
è 0. Questo genera la distribuzione che stiamo cercando.
Ora abbiamo S
, posBits
con un bit impostato in ogni posizione in cui F == 1 e negBits
con un bit impostato in ogni posizione in cui F == -1.
Possiamo dimostrare che F * S
(dove * indica la moltiplicazione) viene valutato a +1 nella condizione (S & posBits) | (~S & negBits)
. Possiamo anche generare una logica simile per tutti i casi in cui viene F * S
valutato -1. E infine, sappiamo che viene sum(F * S)
valutato a 0 se e solo se nel risultato vi è una quantità uguale di -1 e + 1. Questo è molto facile da calcolare semplicemente confrontando il numero di +1 bit e -1 bit.
Questa implementazione utilizza 32 bit ints e il massimo n
accettato è 16. È possibile ridimensionare l'implementazione su 31 bit modificando il codice di generazione casuale e su 63 bit utilizzando uint64_t anziché uint32_t.
modificare
La folle funzione concava:
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);
}
riduce il tempo di esecuzione a 0,160-0,161 ms. Lo svolgimento manuale del loop (non nella foto sopra) rende 0,150. Il meno banale n = 10, iter = 100000 case funziona sotto i 250ms. Sono sicuro di riuscire a ottenere meno di 50 ms sfruttando core aggiuntivi, ma è troppo facile.
Questo viene fatto rendendo libero il ramo del loop interno e scambiando i loop F e S.
Se bothZero
non è necessario, posso ridurre il tempo di esecuzione a 0,02 ms eseguendo un ciclo scarsamente su tutti i possibili array S.