C ++ bit magic
0.84ms com RNG simples, 1.67ms com c ++ 11 std :: knuth
0.16ms com ligeira modificação algorítmica (veja editar abaixo)
A implementação do python é executada em 7,97 segundos no meu equipamento. Portanto, isso é 9488 a 4772 vezes mais rápido, dependendo do RNG que você escolher.
#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;
}
Compile em 64 bits para registros extras. Ao usar o gerador aleatório simples, os loops em convolve () são executados sem nenhum acesso à memória, todas as variáveis são armazenadas nos registradores.
Como funciona: em vez de armazenar S
e F
como matrizes na memória, ele é armazenado como bits em um uint32_t.
Pois S
, os n
bits menos significativos são usados onde um bit definido indica um +1 e um bit não definido indica -1.
F
requer pelo menos 2 bits para criar uma distribuição de [-1, 0, 0, 1]. Isso é feito gerando bits aleatórios e examinando os 16 bits menos significativos (chamados r
) e os 16 bits mais significativos (chamados l
). Se l & ~r
assumirmos que F é +1, se ~l & r
assumirmos que F
é -1. Caso contrário, F
é 0. Isso gera a distribuição que estamos procurando.
Agora temos S
, posBits
com um bit definido em todos os locais onde F == 1 e negBits
com um bit definido em todos os locais onde F == -1.
Podemos provar que F * S
(onde * denota multiplicação) é avaliado como +1 sob a condição (S & posBits) | (~S & negBits)
. Também podemos gerar lógica semelhante para todos os casos em que é F * S
avaliado como -1. E, finalmente, sabemos quesum(F * S)
avalia como 0 se e somente se houver uma quantidade igual de -1 e + 1 no resultado. É muito fácil calcular isso simplesmente comparando o número de +1 e -1 bits.
Esta implementação usa 32 bits de ints, e o valor máximo n
aceito é 16. É possível escalar a implementação para 31 bits modificando o código de geração aleatória e para 63 bits usando uint64_t em vez de uint32_t.
editar
A seguinte função convolve:
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);
}
reduz o tempo de execução para 0,160-0,161ms. O desenrolar manual do loop (não na foto acima) faz com que 0,150. O caso menos trivial de n = 10, iter = 100000 é executado em 250ms. Tenho certeza de que posso obtê-lo abaixo de 50ms usando núcleos adicionais, mas isso é muito fácil.
Isso é feito liberando o ramo do loop interno e trocando o loop F e S.
Se bothZero
não for necessário, eu posso reduzir o tempo de execução para 0,02 ms, fazendo um loop esparso sobre todas as matrizes S possíveis.