C ++ bit magic
0.84ms con RNG simple, 1.67ms con c ++ 11 std :: knuth
0,16 ms con una ligera modificación algorítmica (ver edición a continuación)
La implementación de Python se ejecuta en 7.97 segundos en mi plataforma. Entonces, esto es de 9488 a 4772 veces más rápido dependiendo de qué RNG elijas.
#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 en 64 bits para registros adicionales. Cuando se usa el generador aleatorio simple, los bucles en convolve () se ejecutan sin acceso a la memoria, todas las variables se almacenan en los registros.
Cómo funciona: en lugar de almacenar S
y F
como matrices en memoria, se almacena como bits en un uint32_t.
Para S
, los n
bits menos significativos se utilizan donde un bit establecido denota un +1 y un bit no establecido denota un -1.
F
requiere al menos 2 bits para crear una distribución de [-1, 0, 0, 1]. Esto se realiza generando bits aleatorios y examinando los 16 bits menos significativos (llamados r
) y los 16 bits más significativos (llamados l
). Si l & ~r
suponemos que F es +1, si ~l & r
suponemos que F
es -1. De F
lo contrario, es 0. Esto genera la distribución que estamos buscando.
Ahora tenemos S
, posBits
con un bit establecido en cada ubicación donde F == 1 y negBits
con un bit establecido en cada ubicación donde F == -1.
Podemos demostrar que F * S
(donde * denota multiplicación) se evalúa a +1 bajo la condición (S & posBits) | (~S & negBits)
. También podemos generar una lógica similar para todos los casos donde se F * S
evalúa a -1. Y finalmente, sabemos que se sum(F * S)
evalúa a 0 si y solo si hay una cantidad igual de -1 y + 1 en el resultado. Esto es muy fácil de calcular simplemente comparando el número de +1 bits y -1 bits.
Esta implementación usa entradas de 32 bits, y el máximo n
aceptado es 16. Es posible escalar la implementación a 31 bits modificando el código de generación aleatorio, y a 63 bits usando uint64_t en lugar de uint32_t.
editar
La siguiente función de convolución:
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);
}
corta el tiempo de ejecución a 0.160-0.161ms. El desenrollado manual del bucle (no se muestra arriba) hace que 0.150. El menos trivial n = 10, iter = 100000 casos se ejecuta por debajo de 250 ms. Estoy seguro de que puedo obtener menos de 50 ms aprovechando núcleos adicionales, pero eso es demasiado fácil.
Esto se hace liberando la rama del bucle interno e intercambiando el bucle F y S.
Si bothZero
no es necesario, puedo reducir el tiempo de ejecución a 0.02 ms haciendo un bucle escaso sobre todas las matrices S posibles.