C ++ bit magic
0.84ms avec RNG simple, 1.67ms avec c ++ 11 std :: knuth
0,16 ms avec une légère modification algorithmique (voir édition ci-dessous)
L'implémentation en python s'exécute en 7,97 secondes sur ma plate-forme. Donc, ceci est 9488 à 4772 fois plus rapide en fonction du choix de RNG que vous avez choisi.
#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;
}
Compiler en 64 bits pour des registres supplémentaires. Lorsque vous utilisez le générateur aléatoire simple, les boucles de convolve () s'exécutent sans accès mémoire, toutes les variables sont stockées dans les registres.
Fonctionnement: plutôt que de stocker S
et en F
tant que tableaux en mémoire, il est stocké sous forme de bits dans un uint32_t.
En effet S
, les n
bits les moins significatifs sont utilisés lorsqu'un bit défini désigne un +1 et un bit non défini désigne un -1.
F
nécessite au moins 2 bits pour créer une distribution de [-1, 0, 0, 1]. Cela se fait en générant des bits aléatoires et en examinant les 16 bits les moins significatifs (appelés r
) et les 16 bits les plus significatifs (appelés l
). Si l & ~r
nous supposons que F est +1, si ~l & r
nous supposons que F
c'est -1. Sinon, la valeur F
est 0. Ceci génère la distribution que nous recherchons.
Nous avons maintenant S
, posBits
avec un bit défini à chaque emplacement où F == 1 et negBits
un bit défini à chaque emplacement où F == -1.
Nous pouvons prouver que F * S
(où * désigne la multiplication) est évalué à +1 dans la condition (S & posBits) | (~S & negBits)
. Nous pouvons également générer une logique similaire pour tous les cas où F * S
évalue à -1. Et enfin, nous savons quesum(F * S)
s’évalue à 0 si et seulement s’il existe une quantité égale de -1 et de +1 dans le résultat. Ceci est très facile à calculer en comparant simplement le nombre de +1 bits et -1 bits.
Cette implémentation utilise des bits 32 bits, et le maximum n
accepté est de 16. Il est possible d’échelonner l’implémentation à 31 bits en modifiant le code de génération aléatoire et à 63 bits en utilisant uint64_t au lieu de uint32_t.
modifier
La fonction de convolution suivante:
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);
}
coupe le temps d'exécution à 0.160-0.161ms. Le déroulement manuel de la boucle (non illustré ci-dessus) correspond à 0,150. Le moins trivial n = 10, iter = 100000 cas s’exécute en dessous de 250 ms. Je suis sûr que je peux obtenir moins de 50 ms en exploitant des cœurs supplémentaires, mais c'est trop facile.
Ceci est fait en libérant la branche de la boucle interne et en permutant les boucles F et S.
Si ce bothZero
n’est pas nécessaire, je peux réduire le temps d’exécution à 0,02 ms en effectuant une boucle parcimonieuse sur tous les tableaux S possibles.