C ++ бітова магія
0,84 мс з простим RNG, 1,67 мс з c ++ 11 ст :: кнут
0,16 мс із незначною алгоритмічною модифікацією (див. Редагування нижче)
Реалізація пітона працює на моїй установці за 7,97 секунд. Отже, це в 9488 - 4772 рази швидше, залежно від того, який RNG ви виберете.
#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;
}
Компілюйте в 64-розрядні для додаткових регістрів. При використанні простого випадкового генератора петлі в convolve () працюють без доступу до пам'яті, всі змінні зберігаються в регістрах.
Як це працює: замість того, щоб зберігати S
і F
як масиви в пам'яті, він зберігається як біти в uint32_t.
Для S
цього використовуються n
найменш значущі біти, коли набір бітів позначає +1, а невідомий біт позначає -1.
F
для створення розподілу [-1, 0, 0, 1] потрібно щонайменше 2 біта. Це робиться шляхом генерації випадкових бітів та вивчення 16 найменш значущих (називаних r
) та 16 найбільш значущих бітів (названих l
). Якщо l & ~r
припустити, що F дорівнює +1, якщо ~l & r
припустити, що F
це -1. Інакше F
дорівнює 0. Це генерує дистрибуцію, яку ми шукаємо.
Тепер у нас є S
, posBits
з набором біт на кожному місці , де F == 1 і negBits
з набором біт на кожному місці , де F == -1.
Ми можемо довести, що F * S
(де * позначає множення) оцінюється на +1 за умовою (S & posBits) | (~S & negBits)
. Ми також можемо створити подібну логіку для всіх випадків, коли F * S
оцінюється до -1. І нарешті, ми знаємо, що sum(F * S)
оцінюється на 0, якщо і лише тоді, коли в результаті є рівна кількість -1 та + 1. Це дуже просто обчислити, просто порівнявши кількість +1 біт і -1 біт.
Ця реалізація використовує 32 бітні вставки, а максимально n
прийняте - 16. Можна масштабувати реалізацію до 31 біта, змінивши випадковий код генерування, і до 63 біта, використовуючи uint64_t замість uint32_t.
редагувати
Функція згортання:
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);
}
скорочує час виконання до 0,160-0,161 мс. Ручне розкручування циклу (не на фотографії вище) складає 0,150. Менш тривіальний n = 10, iter = 100000 випадок працює за 250 мс. Я впевнений, що я можу отримати його за 50 мс, використовуючи додаткові сердечники, але це занадто просто.
Це робиться, зробивши внутрішню гілку петлі вільною і замінивши петлю F і S.
Якщо bothZero
цього не потрібно, я можу скоротити час виконання до 0,02 мс, рідко перебираючи всі можливі S масиви.