Насколько медлен Python на самом деле (часть II)?

52

Это продолжение того, насколько медленно работает Python? (Или как быстро ваш язык?) .

Оказывается, было слишком легко получить ускорение x100 для моего последнего вопроса. Для тех, кто испытал трудности, но хочет чего-то сложнее, когда они действительно могут использовать свои навыки низкого уровня, вот часть II. Задача состоит в том, чтобы получить ускорение x100 для следующего кода Python, протестированного на моем компьютере.

Чтобы сделать это более сложным, я использую Pypy на этот раз. Текущее время для меня составляет 1 минуту и ​​7 секунд, используя pypy 2.2.1.

правила

  1. Первый, кто предоставит код, который я смогу выполнить, правильный и в 100 раз быстрее на моей машине, получит награду в 50 баллов.
  2. Я награжу победу самым быстрым кодом через неделю.
import itertools 
import operator 
import random

n = 8 
m  = 8 
iters = 1000  

# creates an array of 0s with length m
# [0, 0, 0, 0, 0, 0, 0, 0]
leadingzerocounts = [0]*m

# itertools.product creates an array of all possible combinations of the 
# args passed to it.
#
# Ex:
#   itertools.product("ABCD", "xy") --> Ax Ay Bx By Cx Cy Dx Dy
#   itertools.product("AB", repeat=5) --> [
#    ('A', 'A', 'A', 'A', 'A'),
#    ('A', 'A', 'A', 'A', 'B'),
#    ('A', 'A', 'A', 'B', 'A'),
#    ('A', 'A', 'A', 'B', 'B'),
#    etc.
#   ]
for S in itertools.product([-1,1], repeat = n+m-1):
    for i in xrange(iters):
        F = [random.choice([-1,0,0,1]) for j in xrange(n)]

        # if the array is made up of only zeros keep recreating it until
        # there is at least one nonzero value.
        while not any(F):
            F = [random.choice([-1,0,0,1]) for j in xrange(n)]

        j = 0
        while (j < m and sum(map(operator.mul, F, S[j:j+n])) == 0):
            leadingzerocounts[j] +=1
            j += 1
print leadingzerocounts

Вывод должен быть похож на

[6335185, 2526840, 1041967, 439735, 193391, 87083, 40635, 19694]

Вы должны использовать случайное начальное число в своем коде, и любой генератор случайных чисел, который достаточно хорош, чтобы дать ответы, близкие к приведенным выше, будет принят.

Моя машина Время будет запущено на моей машине. Это стандартная установка Ubuntu на восьмиъядерный процессор AMD FX-8350. Это также означает, что мне нужно иметь возможность запускать ваш код.

Объяснение кода

Этот код выполняет итерацию по всем массивам S длиной n + m-1, которые составлены на -1 с и 1 с. Для каждого массива S он выбирает 1000 ненулевых случайных массивов F длины n, составленных из -1,0 или 1 с вероятностью 1/4, 1/2, / 14 принятия каждого значения. Затем он вычисляет внутренние произведения между F и каждым окном S длины n, пока не найдет ненулевое внутреннее произведение. Он добавляет 1 к leadingzerocountsкаждому найденному положению с нулевым внутренним произведением.

Статус

  • Perl . Замедление в 2,7 раза от @tobyink. (По сравнению с pypy, а не cpython.)

  • Дж . Ускорение в 39 раз @Eelvex.

  • C . Ускорение в 59 раз
  • Джулия . В 197 раз быстрее, не включая время запуска на одну минуту больше. Ускорение в 8,5 раз, включая время запуска (в данном случае быстрее при использовании 4 процессоров, чем 8).
  • Фортран . Ускорение в 438 раз при @ полуэкстремальном.
  • Rpython . Ускорение в 258 раз благодаря @primo.
  • С ++ . @Ilmale ускорил в 508 раз.

(Я перестал рассчитывать новые улучшения, потому что они слишком быстрые, а iters были слишком малы.)


Было отмечено, что время ниже секунды ненадежно, а также некоторые языки имеют начальную стоимость. Аргументом является то, что если вы хотите включить это, вы должны также включить время компиляции C / C ++ и т. Д. Вот время для самого быстрого кода с числом итераций, увеличенным до 100 000.

  • Джулия . 42 секунды на @ еще одну минуту.
  • С ++ . 14 секунд @GuySirton.
  • Фортран . 14s от @ полу-внешности.
  • С ++ . 12s @ilmale.
  • Rpython . 18s @primo.
  • С ++ . 5s @Stefan.

Победитель .. Стефан!

Последующий вызов опубликован. Как высоко вы можете пойти? (Задача кодирования + алгоритмы) . Этот сложнее.

Сообщество
источник
3
было бы неплохо объяснить, чего должен достичь код, поэтому мы можем переписать его, а не просто перенести его
Einacio
6
« Первый, кто предоставит код, который я смогу выполнить, правильный и в 100 раз быстрее на моей машине, немедленно побеждает, и соревнование закрывается. » Какова цель закрытия соревнования таким образом? Почему бы не использовать крайний срок, как в большинстве других, чтобы мы могли видеть, что он сокращается на других языках?
grovesNL
5
@Einacio Это хорошая идея. Я изменил правила, которые, я надеюсь, никто не будет возражать.
1
@Lembik Я улучшил свою версию на Фортране, сделав ее в 2 раза быстрее на моей машине. Не могли бы вы время снова? :)
полу-посторонний
1
@ Полуэкстремальный Готово.

Ответы:

13

С ++ немного магии

~ 16мс многопоточный, 56мс однопоточный. ~ 4000 ускорений.

(Ускорение основано на многопоточном коде на моем i7-2820QM и 1 мин 9 секунд, упомянутых в вопросе. Поскольку у системы OP хуже однопоточная производительность, чем у моего процессора, но лучше многопоточная производительность, я ожидаю, что это число будет точным)

Многопоточная часть довольно неэффективна из-за нереста нитей. Я мог бы, вероятно, добиться большего успеха, используя свою пользовательскую библиотеку заданий, но в ней есть ошибки в системах Unix. Для объяснения и почти идентичного кода без многопоточности см. Https://codegolf.stackexchange.com/a/26485/20965 .

редактировать

Я назначил каждому потоку собственный RNG и сократил длину в битах до 32, что сократило время выполнения на несколько мс.

#include <iostream>
#include <bitset>
#include <random>
#include <chrono>
#include <stdint.h>
#include <cassert>
#include <array>
#include <tuple>
#include <memory>
#include <thread>
#include <future>
#include <string.h>


#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



void convolve()
{
    static const unsigned threadCount = 32;
    static const unsigned n = 8;
    static const unsigned m = 8;
    static const unsigned totalIters = 1000;
    static_assert( n <= 16, "packing of F fails when n > 16.");
    static uint32_t fmask = (1 << n) -1; fmask |= fmask << 16;

    std::array< uint32_t, m * threadCount > out;
    std::vector< std::future<void> > threads;

    for( int threadId = 0; threadId < threadCount; threadId++)
    {
        threads.emplace_back( std::async( [&, threadId]
        {
            std::random_device rd;
            std::knuth_b gen(rd());
            uint32_t nextRandomNumber = gen();

            const unsigned iters = totalIters / threadCount;

            std::array< uint32_t, m > leadingZeros;
            for( auto& x : leadingZeros )
                x = 0;

            for( unsigned i = 0; i < iters; i++ )
            {
                // generate random bit mess
                uint32_t F;
                do {
                    // this funky looking construction shortens the dependancy chain of F
                    F = nextRandomNumber & fmask;
                    nextRandomNumber = gen();
                } 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 + m -1));
                // generate all possible N+1 bit strings
                // 1 = +1
                // 0 = -1
                while ( S-- )
                {
                    for( int shift = 0; shift < m; shift++ )
                    {
                        uint32_t s = (S >> shift) % ( 1 << n );
                        auto firstBits = (s & mask) ^ adjF;

                        if ( desiredBits == popcnt( firstBits ) )
                        {
                            leadingZeros[shift] = leadingZeros[shift] + 1;
                        }
                        else
                        {
                            break;
                        }
                    }
                }
            }

            memcpy( out.data() + (threadId * m), leadingZeros.data(), sizeof( leadingZeros[0] ) * m );
        } ));

    };

    std::array< uint32_t, m > leadingZeros;
    for( auto& x : leadingZeros )
        x = 0;

    for( auto& thread : threads )
    {
        thread.wait();
    }

    for( int i = 0; i < (threadCount * m); i++ )
    {
        leadingZeros[i % m] += out[i];
    }


    for( auto x : leadingZeros )
        std::cout << x << ", ";

    std::cout << std::endl;
}

int main()
{
    typedef std::chrono::high_resolution_clock clock;
    int rounds = 100;

    // do some rounds to get the cpu up to speed..
    for( int i = 0; i < rounds / 10; i++ )
    {
        convolve();
    }


    auto start = clock::now();

    for( int i = 0; i < rounds; i++ )
        convolve();

    auto end = clock::now();
    double seconds = std::chrono::duration_cast< std::chrono::microseconds >( end - start ).count() / 1000000.0;

    std::cout << seconds/rounds*1000 << " msec/round" << std::endl;

    return 0;
}

Пример вывода:

   6317312, 2515072, 1034368, 434048, 190144, 85200, 39804, 19168,
   6226944, 2481408, 1031168, 438080, 192896, 86816, 40484, 19490,
   6321152, 2524672, 1045376, 442880, 195680, 88464, 41656, 20212,
   6330624, 2517504, 1031104, 430208, 187696, 83976, 38976, 18708,
   6304768, 2510336, 1030720, 433056, 190880, 86824, 40940, 19840,
   6272512, 2494720, 1028160, 432352, 189168, 84752, 39540, 19052,
   6233600, 2507520, 1046912, 447008, 198224, 89984, 42092, 20292,
Стефан
источник
Вывод не верный, я думаю, может быть, есть ошибка? Сравните с тем, что в вопросе. В частности, последний столбец должен быть числом, близким к 19180.
@Lembik я понимаю, что ты имеешь в виду. Я думаю, что случайный вывод не является достаточно случайным, что иногда создает эффектный вывод. С генератором случайных чисел в C ++ 11 он работает нормально. Я исправлю код позже сегодня.
Стефан
Я получаю Stefan.cpp: 104: 101: ошибка: 'memcpy' не был объявлен в этой области memcpy (out.data () + (threadId * m) ,adingZeros.data (), sizeof (adingZeros [0]) * m );
Я думаю, что мне нужно включить string.h. Попробуйте снова.
Стефан
Вы компилируете это с помощью g ++ -O3 -std = c ++ 0x -pthread -Wl, - без необходимости, Stefan.cpp -o Stefan
16

C ++ x150 x450 x530

Вместо массива я использовал биты (и темную магию).
Спасибо @ace за более быструю случайную функцию.

Как это работает: первые 15 бит целого числа sпредставляют массив S[15]; нули представляют -1, единицы представляют +1. Массив Fстроится аналогичным образом. Но с двумя битами для каждого символа.

  • 00 представляет -1
  • 01 и 10 представляют 0
  • 11 представляют 1

Потому что Sи у Fменя другое представление, которое я должен чередовать Sс самим собой, чтобы сопоставить его F.

  • 0 (-1) стал 00 (-1 в представлении F)
  • 1 (+1) стало 11 (+1 в представлении F)

Теперь мы можем просто использовать Карно для вычисления внутреннего продукта. Помните, что одна переменная может принимать только значение 00 или 11

0 00 = 11 (-1 * -1 = +1)
0. 01 = 10 (-1 * 0 = 0)
0. 10 = 01 (-1 * 0 = 0)
0. 11 = 00 (-1 * +1 = -1)
1. 00 = 00 (+1 * -1 = -1)
1. 10 = 10 (+1 * 0 = 0)
1. 01 = 01 (+1 * 0 = 0)
1. 11 = 11 (+1 * +1 = +1)

Похоже, не XOR для меня. :)

Суммируйте, что это просто игра смены и маски, ничего действительно сложного.

#include <array>
#include <ctime>

// From standford bithacks
// http://graphics.stanford.edu/~seander/bithacks.html
inline int32_t interleaveBit(int32_t x)
{
   static const uint32_t B[] = { 0x55555555, 0x33333333, 0x0F0F0F0F, 0x00FF00FF };
   x = (x | ( x << 8)) & B[3];
   x = (x | ( x << 4)) & B[2];
   x = (x | ( x << 2)) & B[1];
   x = (x | ( x << 1)) & B[0];
   return x | (x << 1);
}

inline int32_t sumOnes(int32_t v)
{
   static int b[] = { 1, 0, 0, 1};
   int s = 0;
   for( int i = 0; i < 8; ++i)
   {
      const int a = 3&(v>>(i*2));
      s += b[a];
   }
   return s;
}

inline int32_t sumArray(int32_t v)
{
   static int b[] = { -1, 0, 0, 1};
   int s = 0;
   for( int i = 0; i < 8; ++i)
   {
      const int a = 3&(v>>(i*2));
      s += b[a];
   }
   return s;
}

uint32_t x, y = 24252, z=57768, w=1564; //PRNG seeds

int32_t myRand()
{
   uint32_t t;
   t = x ^ (x<<1);
   x = y;
   y = z;
   z = w;
   w = w ^ ( w >> 19) ^ t ^ (t >> 8);
   return w;
}

int main()
{
   std::array<int, 8> leadingZero{0};
   x = static_cast<int32_t>(time(nullptr)); // seed PRNG
   const int maxS = 1 << 15;
   for(int s = 0; s < maxS; ++s)
   {
      const int32_t x = interleaveBit(s);
      for(int i = 0; i < 1000; ++i)
      {
         int32_t random;
         do
         {
            random = 0xFFFF & myRand();
         }while(sumOnes(random) == 0);
         int j = 7;
         while( j >= 0 )
         {
            const int32_t h = (x >> (j*2));
            const int32_t l = 0xFFFF & (~(random ^ h)); // inner product
            if(sumArray(l) == 0)
            {
               leadingZero[j]++;
            } else
            {
               break;
            }
            j--;
         }

      }
   }
   for(int i = 7; i >= 0; --i)
   {
      printf("%d ", leadingZero[i]);
   }
   printf("\n");
   return 0;
}

Вот пример вывода:

6332350 2525218 1041716 438741 192917 87159 41023 19908 

real 0m0.372s
user 0m0.371s
sys  0m0.001s

Программа была составлена ​​с:

gcc -std=c++11 -O3 -msse4.2 -Wall -lstdc++ 26371.cpp -o fastPy

на Fedora 20 с gcc 4.8.2 Процессор является i7 8core.

Вероятно, я могу получить некоторые параметры настройки компилятора мс.

Пока это время решения OP на моей машине:

time pypy 26371.py
[6330609, 2523914, 1040885, 439303, 192708, 86987, 40710, 19498]

real 0m53.061s
user 0m53.016s
sys  0m0.022s

Редактировать:

Просто добавив openmp и изменив порядок for, я получу выигрыш в x3, что приведет к улучшению производительности x450 по сравнению с OP-кодом. : D В этом случае leadingZeroмассив должен быть атомарным. Случайные глобальные ... случайны, они будут более случайными.

 #pragma omp parallel for
 for(int i = 0; i < 1000; ++i)
 {
    int32_t random;
    do
    {
       random = 0xFFFF & myRand();
    }while(sumOnes(random) == 0);
    for(int s = 0; s < maxS; ++s)
    {
       const int32_t x = interleaveBit(s);
       int j = 7;
       while( j >= 0 )
       {
          const int32_t h = (x >> (j*2));
          const int32_t l = 0xFFFF & (~(random ^ h)); // inner product
          if( sumArray(l) == 0 )
          {
             leadingZero[j]++;
          } else
          {
             break;
          }
          j--;
       }
    }
 }

нужно добавить -fopenmpв флаг компилятора


Редактировать: 2 В качестве подсказки user71404 я изменил функции sumOnes и sumArray, и теперь это очень быстро.

real  0m0.101s
user  0m0.101s
sys   0m0.000s

С openmp медленнее, потому что атомники добавляют слишком много накладных расходов.

real  0m0.253s
user  0m1.870s
sys   0m0.001s

Без атомики еще быстрее, но я получаю неправильный результат.

2137992 1147218 619297 321243 155815 70946 32919 15579

real   0m0.048s
user   0m0.338s
sys    0m0.001s

Чтобы понять sumArray, рассмотрим 16-битное представление и массив из 8 чисел.
00 не имеют 1 и представляют -1
01, а 10 имеют 1 и представляют 0
11, имеют 2 1 и представляют 1,
так что для встроенного подсчета число битов устанавливается в 1 [ http://en.wikipedia.org/wiki/ Hamming_weight] и каждой группе удаляем 1. Круто.

sumOnes - это просто черная магия.

Здесь самые последние флаги компиляции и код.

gcc -std = c ++ 11 -mfpmath = sse -O3 -flto -march = native -funroll-loops -Wall -lstdc ++

#include <cstdint>
#include <cstdio>
#include <ctime>

inline int32_t interleaveBit(int32_t x)
{
   static const uint32_t B[] = { 0x55555555, 0x33333333, 0x0F0F0F0F, 0x00FF00FF };
   x = (x | ( x << 8)) & B[3];
   x = (x | ( x << 4)) & B[2];
   x = (x | ( x << 2)) & B[1];
   x = (x | ( x << 1)) & B[0];
   return x | (x << 1);
}

inline int32_t sumOnes(int32_t v)
{
   /* 0xAAAA == 0b1010 1010 1010 1010 */
   return !!(0xAAAA & (v ^ ~(v << 1)));
}

inline int32_t sumArray(int32_t v)
{
   return __builtin_popcount(v) - 8;
}

uint32_t x, y = 24252, z = 57768, w = 1564; //PRNG seeds

int32_t myRand()
{
   uint32_t t;
   t = x ^ (x << 1);
   x = y;
   y = z;
   z = w;
   w = w ^ ( w >> 19) ^ t ^ (t >> 8);
   return w;
}

int main()
{
   int leadingZero[8] = { 0 };
   x = static_cast<int32_t>(time(nullptr)); // seed PRNG
   const int maxS = 1 << 15;
   for( int i = 0; i < 1000; ++i )
   {
      int32_t random;
      do
      {
         random = 0xFFFF & myRand();
      } while(sumOnes(random) == 0 );
      for( int s = 0; s < maxS; ++s )
      {
         const int32_t x = interleaveBit(s);
         int j = 7;
         while( j >= 0 )
         {
            const int32_t h = (x >> (j * 2));
            const int32_t l = 0xFFFF & (~(random ^ h)); // inner product
            if( sumArray(l) == 0 )
            {
               leadingZero[j]++;
            } else
            {
               break;
            }
            j--;
         }
      }
   }
   printf("[%d, %d, %d, %d, %d, %d, %d, %d]\n",
      leadingZero[7], leadingZero[6],
      leadingZero[5], leadingZero[4],
      leadingZero[3], leadingZero[2],
      leadingZero[1], leadingZero[0]);
   return 0;
}
ilmale
источник
Теперь я не могу дождаться, чтобы проверить это! К сожалению, это не будет в течение нескольких часов.
1
Следующее было в предложенном редактировании, но я думаю, что оно может лучше подойти в качестве комментария. Вы можете заменить свой sumOnes, sumArray следующим (кажется, дает мне двукратное ускорение по сравнению с версией openmp). inline int32_t sumOnes(int32_t v) { /* 0xAAAA == 0b1010 1010 1010 1010 */ return !! (0xAAAA & (v ^ ~(v << 1))); } inline int32_t sumArray(int32_t v) { return __builtin_popcount(v) - 8; }это было предложено @ user71404
ace_HongKongIndependence
@ user71404: профилировщик сказал, что программа потратила все свое время на эти две функции, но я вчера очень устал думать, что-то лучше, чем это. Я попробую этим вечером (UTC). Благодарю.
ilmale
Не могли бы вы изменить второй фрагмент кода, чтобы он стал полной копией и вставляемым кодом? Я, должно быть, делаю что-то не так в своих попытках заставить ваш openmp-код работать, так что это очень поможет.
Приятно. Я думал, что это можно сделать с помощью битовых операций.
Гай Сиртон
10

Юлия: 0,7 с, в 120 раз быстрее

Как показал пользователь 20768, простой порт кода для Julia примерно в два раза быстрее PyPy. Но мы можем сделать намного лучше, чем это.

function pleadingzerocounts(; n = 8,
                              m = 8,
                              iters = 1000)
  @parallel (+) for S = 1:2^(8+8-1)
    leading_counts = zeros(Int, m)
    F = Array(Int, n)
    for i = 1:iters
      flag = 0
      while flag == 0
        for i = 1:n
          v = (1-(rand(Int8)&3))%2
          @inbounds F[i] = v
          flag += v & 1
        end
      end
      for j = 1:m
        sum = 0
        for i = 1:n
          @inbounds sum += S & (1 << (j + i - 2)) > 0 ? F[i] : -F[i]
        end
        sum == 0 ?
          (leading_counts[j] += 1) :
          break
      end
    end
    leading_counts
  end
end

function main()
  # Warm up the JIT
  pleadingzerocounts()
  # Then go for real
  println(@time pleadingzerocounts())
end

Вы можете запустить это с помощью julia -p 8 -e 'require("golf.jl");main()'(8 - это количество процессов, вы можете поиграть с ним). В последнем пререлизе Julia это занимает 0,7 с против 1 м 22 для PyPy.

Если у вас достаточно ядер на вашем компьютере и, возможно, раскрутите несколько экземпляров AWS, вы сможете побриться еще немного :)

один-более минуты
источник
Я совершенно уверен, что вы неправильно измеряете время. Python с Pypy также является языком на основе JIT, но временные характеристики, сделанные OP, включают время компиляции JIT. Вы исключаете это. Я установил последнюю версию Julia git и проверил ваш код, и на моей машине ваша команда с 8 процессами завершается за 6,6 секунды, но выдает «истекшее время 0,588 ... секунд».
полу-постороннее
Время Python включает в себя запуск PyPy и разогрев JIT, но это занимает не более секунды - разница в минуту выполнения незначительна. Я рад, если OP изменит способ, которым питон рассчитан (это не будет иметь никакого значения), но включение времени запуска Джулии не будет разумным.
еще минут
Я спросил ОП в комментариях к исходному вопросу, и он сказал: «Время должно включать все для языков JIT». Он также заявил, что создаст новую задачу, в которой решение займет намного больше времени, чем 1 секунда, и Джулия останется в конкурсе.
полу-постороннее
В этом случае оптимальным решением является использование последовательного алгоритма - это занимает около 2 с. Я бы опубликовал код, но этот конкурс сейчас несколько избыточен, поскольку все уже знают, что C ++ загружается быстрее, чем все остальное.
одна минута
Я только что опубликовал свое решение на Фортране, так что я не понимаю, почему вы не должны публиковать самый быстрый Julia (если у вас уже есть код).
полу-посторонний
5

С, 1,210 с

С кодом OP, работающим на моей машине 1m45.729s.

Компиляция:

gcc -O3 -march=native -fwhole-program -fstrict-aliasing -ftree-vectorize -Wall ./test2.c -o ./test2

Отдельное спасибо: @dyp за флаги компиляции и идеи для оптимизации.

#include <stdio.h>
#include <time.h>

#define n (8)
#define m (8)
#define iters (1000)
int leadingzerocounts[m]; // declared as global so initialised to 0
unsigned int x,y=34353,z=57768,w=1564; //PRNG seeds

/* xorshift PRNG
 * Taken from https://en.wikipedia.org/wiki/Xorshift#Example_implementation
 * Used under CC-By-SA */
int myRand() {
    unsigned int t;
    t = x ^ (x << 11);
    x = y; y = z; z = w;
    return w = w ^ (w >> 19) ^ t ^ (t >> 8);
}

int dotproduct(int*F, int*S) {
    unsigned int i;
    int sum=0;
    for(i=0; i<n; i++) {
        sum+=F[i]*S[i];
    }
    return sum;
}

int main() {
    unsigned int i, j, tmp;
    x=(int)time(NULL); //seed PRNG

    int S[n+m-1];
    for(i=0; i<(1<<(n+m-1)); i++) {
        tmp=i;
        for(j=0; j<n+m-1; j++) {
            S[j]=(tmp&1)*(-2)+1;
            tmp>>=1;
        }
        for(j=0; j<iters; j++) {
            int F[n];
            unsigned int k, flag=0;
            do {
                for(k=0; k<n; k++) {
                    F[k]=(1-(myRand()&3))%2;
                    flag+=(F[k]&1);
                }
            } while(!flag);
            for(k=0; k<m&&!dotproduct(F, S+k); k++) {
                leadingzerocounts[k]++;
            }
        }
    }
    for(i=0; i<m; i++) printf("%d ", leadingzerocounts[i]);
    return 0;
}

Пример вывода:

6334411 2527506 1042239 439328 192914 87005 40847 19728
ace_HongKongIndependence
источник
1
Интересно, что я могу сделать аналогичные наблюдения при отбрасывании всех этих флагов оптимизации. Попробуйте -march=native -fwhole-program -fstrict-aliasing -ftree-vectorizeкстати. Я опустился до <4 с использованием C ++ 11, включая MT19937 плюс a uniform_int_distribution.
DYP
1
1.119 с ускорением примерно 59!
1
@ace Да, я просто хотел указать на это. Мне было проще попробовать некоторые из стандартных библиотечных PRNG в C ++. Обратите внимание, что вы можете использовать одно 32-разрядное целочисленное значение из PRNG для получения 8 записей F.
DYP
1
Поскольку nон равен 8, вы, вероятно, можете использовать AVX (или 2 * SSE) для вычисления точечного продукта с надлежащим Sхранилищем.
Майкл М.
2
Версия SSE, небольшое ускорение: gist.github.com/anonymous/11394210 (не забудьте включить smmintrin.h)
Майкл М.
5

Perl

Это далеко не так быстро, как решение C, но я думаю, что это довольно быстро для интерпретируемого языка высокого уровня. Это позволяет сэкономить около 40% времени работы реализации Python.

#!/usr/bin/env perl

use v5.10;
use strict;
use warnings;
use Algorithm::Combinatorics qw( variations_with_repetition );
use List::Util qw( any sum );

use constant {
  N        => 8,
  M        => 8,
  ITERS    => 1000,
};

my @leadingzerocounts;

my $variations = variations_with_repetition([-1, 1], N + M - 1);
while (my $S = $variations->next)
{
  for my $i (1 .. ITERS)
  {
    my @F;
    until (@F and any { $_ } @F)
    {
      @F = map +((-1,0,0,1)[rand 4]), 1..N;
    }

    my $j = 0;
    while ($j < M)
    {
      last if sum map $F[$_]*$S->[$j+$_], 0..N-1;
      $leadingzerocounts[$j++]++;
    }
  }
}

say join ", ", @leadingzerocounts;

Алгоритм :: Комбинаторика доступна в Ubuntu ( sudo apt-get install libalgorithm-combinatorics-perl). Другие используемые модули являются базовыми модулями Perl, поэтому их уже следует устанавливать как часть базовой установки Ubuntu.

tobyink
источник
1
Это не повлияет на скорость, но это 0..N-1диапазон в последнем map, верно? Вы забыли use warnings? :-) Хотя логика в OP сбивает с толку, скользящее окно никогда не доходит до последнего элемента S.
user2846289
Ах. Я просто подумал, что массивы имеют несоответствующие размеры, поэтому я отключил, warningsчтобы пропущенные элементы рассматривались как ноль. N-1улучшает это. И на самом деле это немного улучшает скорость - теперь она примерно на 40% быстрее, чем реализация Python.
tobyink
Я думаю, что ваш код требует очень современной версии List :: Util. На Ubuntu 14.04 я получаю "any" не экспортируется модулем List :: Util
Ах, да, это правда - вам, вероятно, нужно установить List :: Util с CPAN. anyАльтернативно можно найти в List :: MoreUtils, который, хотя и не является основным модулем, является одним из наиболее часто используемых модулей CPAN.
tobyink
4

Юлия: в 4,66 раза медленнее!

Я действительно начинаю сомневаться в статистике на их сайте ...

Обратите внимание, что следующий код Julia фактически является прямой транскрипцией кода Python OP без каких-либо оптимизаций. Я использую time()функцию, чтобы исключить медленное время запуска Юлии ...

srand(27182818284590)
t = time()

require("Iterators")

n = 8
m = 8
iters = 1000
bothzero = 0
leadingzerocounts = zeros(m)

for S in Iterators.product(fill([-1,1], n+m-1)...)
    for i = 1:iters
        F = [[-1 0 0 1][rand(1:4)] for j = 1:n]
        while all((x) -> x == 0, F)
            F = [[-1 0 0 1][rand(1:4)] for j = 1:n]
        end
        j = 1
        while j <= m && sum(map(*, F, S[j:j+n-1])) == 0
            leadingzerocounts[j] += 1
            j += 1
        end
    end
end

println(leadingzerocounts)

t = time() - t
println("$t seconds")

Юлия: 5 м 32,912 с

Код ОП в PyPy: 1 м 11.506 с

Юлия выходной:

6332170
2525472
1041522
438761
193119
86873
40705
19662
шустрый агар
источник
7
+1 за ваше <s> бесстыдство </ s> спортивного мастерства.
ace_HongKongIndependence
Глобальные переменные, импорт и массивы воспринимаются медленно. Это не то, как можно было бы написать программу Джулии для производительности.
Алекс А.
4

RPython 0.187s (в 258 раз быстрее)

Оригинальный источник с PyPy2.2.1: 1 м 6,718 с

Теперь с многопоточностью обратная поддержка для стандартного Python прекращена. Количество рабочих потоков может быть указано в качестве параметра командной строки, по умолчанию - два.

from time import time, sleep
from math import fmod

from rpython.rlib.rthread import start_new_thread, allocate_lock, get_ident
class Random:
  __slots__ = ['s']

  def __init__(self, s=1):
    self.s = s

  def init_genrand(self, seed):
    self.s = seed

  def genrand32(self):
    # xorshift PRNG with period 2^32-1
    # see http://core.kmi.open.ac.uk/download/pdf/6250138.pdf
    self.s ^= (self.s << 13)
    self.s ^= (self.s >> 17)
    self.s ^= (self.s << 5)
    return self.s

class ThreadEnv:
  __slots__ = ['n', 'm', 'iters', 'counts', 'running', 'lock']

  def __init__(self):
    self.n = 8
    self.m = 8
    self.iters = 1000
    self.counts = [0]*8
    self.running = 0
    self.lock = None

env = ThreadEnv()
truth = [-1,0,0,1]

def main(argv):
  argc = len(argv)
  if argc < 4 or argc > 5:
    print 'Usage: %s N M ITERS [NUM_THREADS=2]'%argv[0]
    return 1

  if argc == 5:
    num_threads = int(argv[4])
  else:
    num_threads = 2

  env.n = int(argv[1])
  env.m = int(argv[2])
  env.iters = int(argv[3]) // num_threads
  env.counts = [0]*env.m
  env.lock = allocate_lock()

  for i in xrange(num_threads-1):
    start_new_thread(run,())
    env.running += 1

  env.running += 1

  # use the main process as a worker
  run()

  # wait for any laggers
  while env.running:
    sleep(0.01)

  print env.counts
  return 0

def run():
  n, m, iters = env.n, env.m, env.iters
  counts = [0]*m
  sbits = [0]*(n+m-1)

  random = Random()
  seed = int(fmod(time(), 2147483.648)*1000) ^ get_ident()
  random.init_genrand(seed)

  for S in xrange(1<<n+m-1):
    i = 0
    sbit = 0
    while not sbit:
      sbits[i] ^= 3
      sbit = sbits[i]
      i += 1

    for i in xrange(iters):
      f = 0
      while not f:
        F = random.genrand32()

        G, x = F, 0
        for k in xrange(n):
          x += truth[(G&3)^sbits[k]]
          f |= x
          G >>= 2

      if not x:
        counts[0] += 1
        for j in xrange(1, m):
          G, x = F, 0
          for k in xrange(j, n+j):
            x += truth[(G&3)^sbits[k]]
            G >>= 2
          if x: break
          counts[j] += 1

  # passing True stalls until a lock can be obtained
  env.lock.acquire(True)

  for i in xrange(m):
    env.counts[i] += counts[i]
  env.running -= 1

  env.lock.release()

def target(*args):
  return main, None

RPython - это ограниченное подмножество Python, которое можно преобразовать в C и затем скомпилировать с помощью RPython Toolchain . Его выраженная цель - помочь в создании языковых переводчиков, но его также можно использовать для компиляции простых программ, подобных приведенной выше. Большинство «причудливых» функций Python, таких как itertoolsили даже mapнедоступны.

Для компиляции создайте локальный клон текущего pypy-репозитория и выполните следующее:

$ pypy %PYPY_REPO%/rpython/bin/rpython --thread convolution.py

Полученный исполняемый файл будет назван convolution-cили похож в текущем рабочем каталоге.

Я параметризовал входные переменные, поэтому программа должна запускаться как:

convolution-c 8 8 1000

чтобы соответствовать примеру кода.


Замечания по реализации

S in itertools.product([-1,1], repeat = n+m-1)становится S in xrange(1<<n+m-1), интерпретируя Sкак битовую карту: [ 0, 1] → [ -1, 1]

Кроме того, Fтакже немного карту, с каждых двух битов , представляющих одно значение:
[ 00, 01, 10, 11] → [ -1, 0, 0, 1]

Таблица истинности используется для поиска продукта, а не для выполнения мультипликации.

Поскольку используются 32-разрядные целые числа со знаком, они nмогут быть не больше 15 и n+mне больше 31. При необходимости с помощью rpython.rlib.rbigintмодуля можно добиться поддержки произвольного целого числа .

Первая итерация цикла скалярных произведений развернута и объединена с тестом на недействительность F.

Используется доморощенный PRNG, источник указан. Автор статьи демонстрирует период 2 32 с -1 и утверждает, что он проходит все тесты Диарда, кроме одного, хотя я лично не подтвердил это.

Случайное начальное число меняется каждую миллисекунду, что примерно так же хорошо, как использование временной метки. Кроме того, каждый рабочий поток имеет xorсвой идентификатор процесса с этим значением, чтобы каждый из них имел различное начальное число.


Пример времени

2 рабочих потока:

$ timeit convolution-c 8 8 1000 2
[6331845, 2526161, 1042330, 440018, 193724, 87147, 40943, 19603]

Elapsed Time:     0:00:00.375
Process Time:     0:00:00.687
System Calls:     6927

4 рабочих потока:

$ timeit convolution-c 8 8 1000 4
[6334565, 2527684, 1043502, 440216, 193225, 87398, 40799, 19338]

Elapsed Time:     0:00:00.218
Process Time:     0:00:00.796
System Calls:     3417

8 рабочих потоков:

$ timeit convolution-c 8 8 1000 8
[6327639, 2522483, 1039869, 437884, 192460, 86771, 40420, 19403]

Elapsed Time:     0:00:00.187
Process Time:     0:00:00.734
System Calls:     3165

Первоисточник ОП:

$ timeit pypy convolution-orig.py
[6330610, 2525644, 1041481, 438980, 193001, 86622, 40598, 19449]

Elapsed Time:     0:01:06.718
Process Time:     0:01:06.718
System Calls:     11599808

Сроки для 100000 итераций:

$ timeit convolution-c 8 8 100000 8
[633156171, 252540679, 104129386, 43903716, 19307215, 8709157, 4072133, 1959124]

Elapsed Time:     0:00:16.625
Process Time:     0:01:02.406
System Calls:     171341
Примо
источник
Я никогда не видел программу rpython раньше. Это здорово. Есть ли эквивалентная программа на чистом питоне, которую pypy может запустить в 1.03s?
@Lembik Я хотел бы увидеть один. Я думал, что 4.7s было довольно хорошо, учитывая, что моя первая попытка на чистом питоне была ~ 15с.
Прим
Да, извините за задержку. У меня еще не запущен код, но он будет запущен как можно скорее.
Вы должны попробовать добавить JIT. Теперь это будет быстро!
kirbyfan64sos
@Lembik спасибо за упоминание;) Из любопытства, он работал быстрее всего с 4 рабочими потоками или с 8?
Примо
3

Джулия: 1 мин 21,4 с (в 2,2 раза быстрее) (модификация кода Армана)

Код оп в PyPy: 3 мин. 1,4 с

Оба делаются в REPL, не считая времени на загрузку пакетов.

function foo()                                                                                                                                                             
    n = 8                                                                                                                                                                  
    m = 8                                                                                                                                                                  
    iters = 1000                                                                                                                                                           
    bothzero = 0                                                                                                                                                           
    leadingzerocounts = zeros(Int,m)                                                                                                                                       
    P=[-1,0,0,1]                                                                                                                                                           

    for S in Iterators.product(fill([-1,1], n+m-1)...)                                                                                                                     
        Sm=[S...]                                                                                                                                                          
        for i = 1:iters                                                                                                                                                    
            F = P[rand(1:4,n)]                                                                                                                                             
            while all(F==0)                                                                                                                                                
                F = P[rand(1:4,n)]                                                                                                                                         
            end                                                                                                                                                            
            j = 1                                                                                                                                                          

            while j <= m && dot(F,Sm[j:j+n-1]) == 0                                                                                                                        
                leadingzerocounts[j] += 1                                                                                                                                  
                j += 1                                                                                                                                                     
            end                                                                                                                                                            
        end                                                                                                                                                                
    end                                                                                                                                                                    

    println(leadingzerocounts)                                                                                                                                             
end 

Есть некоторые проблемы с кодом Армана, делающим его очень медленным: он использует множество анонимных функций и функций более высокого порядка без необходимости. Чтобы проверить, является ли весь вектор F нулевым, почему бы просто не написать все (F == 0) вместо всех (x-> x == 0, F)? Это короче, и буквально в тысячу раз быстрее.

Он также использует сумму (map (*, x, y)) в качестве точечного произведения вместо простой точки (x, y). Первая версия в 650 раз медленнее для вектора 10 Кб удваивается. И функция точечного произведения реализована как цикл for в чистой Джулии.

Кроме того, понимание массива медленное. Лучше написать [0,1,0, -1] [rand (1: 4, n)] вместо [[-1 0 0 1] [rand (1: 4)] для j = 1: n] ,

И, наконец, глобальные переменные - это плохо для Джулии. Джулия работает быстро, только если вы пишете код таким образом, чтобы JIT и вывод типов работали. Большая часть этого - стабильность типов: компилятор должен быть уверен, что тип переменной не изменится, например, внутри цикла.

user20768
источник
Спасибо! Я вижу, что мне еще предстоит немало узнать о языке Julia, прежде чем я смогу заявить о его скорости :) Действительно рад видеть, что несколько тривиальных исправлений в моем коде увеличивают время его выполнения в несколько раз.
шустрый агар
2

Нимрод

import times, locks, strutils, unsigned

const
  N = 8
  M = 8
  iters = 1000
  numThreads = 8

type
  SVec = array[0..N+M-1, int]
  FVec = array[0..N-1, int]
  ComputeThread = TThread[int]

var
  rngSeed = int(epochTime()*1000)
  totalLeadingZeros: array[0..M-1, int]
  lock: TLock

type
  RNGState = object
    x, y, z, w: uint32

proc newRNG(seed: int): RNGState =
  result.x = uint32(seed)

proc random(rng: var RNGState): int =
  let t = rng.x xor (rng.x shl 11)
  rng.x = rng.y; rng.y = rng.z; rng.z = rng.w
  rng.w = rng.w xor (rng.w shr 19) xor t xor (t shr 8)
  result = int(rng.w)

proc initVecRand(v: var FVec, rng: var RNGState) =
  const values = [ -1, 0, 0, 1 ]
  var rnd = rng.random
  var bitAcc = 0
  for i in 0 .. <len(v):
    let val = values[rnd and 3]
    rnd = rnd shr 2
    v[i] = val
    bitAcc = bitAcc or val
  if bitAcc == 0:
    initVecRand(v, rng)

proc convolve(s: SVec, f: FVec, offset: int): int =
  for i in 0 .. <len(f):
    result += s[i+offset]*f[i]

proc iterate(v: var SVec) =
  for i in 0 .. <len(v):
    if v[i] == -1:
      v[i] = 1
      return
    v[i] = -1

proc mainThread(id: int) {.thread.} =
  const numS = 1 shl (N+M-1)
  var
    s: SVec
    f: FVec
    leadingZeros: array[0..M-1, int]
    rng = newRNG(rngSeed + id)
  for k in 0 .. <len(s):
    s[k] = -1
  for i in 1..numS:
    for j in countUp(id, iters, numThreads):
      initVecRand(f, rng)
      if convolve(s, f, 0) == 0:
        leadingZeros[0] += 1
        for k in 1 .. <M:
          if convolve(s, f, k) == 0:
            leadingZeros[k] += 1
          else:
            break
    iterate(s)
  acquire(lock)
  for i in 0 .. <M:
    totalLeadingZeros[i] += leadingZeros[i]
  release(lock)

proc main =
  let startTime = epochTime()
  var threads: array[1..numThreads, ComputeThread]
  initLock(lock)
  for i in 1..numThreads:
    createThread(threads[i], mainThread, i)
  for i in 1..numThreads:
    joinThread(threads[i])
  echo("Leading zeros: ", @totalLeadingZeros)
  let endTime = epochTime()
  echo("Time taken:    ", formatFloat(endTime - startTime, ffDecimal, 3),
       " seconds")

main()

Пример вывода:

Leading zeros: @[6333025, 2525808, 1042466, 439138, 192391, 86751, 40671, 19525]
Time taken:    0.145 seconds

Nimrod компилируется в C, поэтому выбор C-компилятора для внутреннего интерфейса также имеет значение.

Используя clang, скомпилируйте с:

nimrod cc --threads:on --cc=clang --passc:-flto -d:release conv.nim

Используя gcc, скомпилируйте с:

nimrod cc --threads:on --cc=gcc --passc:-flto -d:release conv.nim

Опустите, --passc:-fltoесли у вас есть старый компилятор C, который не поддерживает LTO. Опустите --cc=...опцию, если вы в порядке с выбором по умолчанию для компилятора C. Код требует Nimrod 0.9.4 или 0.9.5 .

На моем четырехъядерном iMac (Core i5 с частотой 2,66 ГГц) код выполняется примерно за 0,15 секунды с gcc 4,9, с 0,16 секундами при Clang, по сравнению с 88 секундами для PyPy 2.2.1 (т.е. ускорение в 500 раз больше). К сожалению, у меня нет доступа к машине с более чем четырьмя ядрами, на которой также установлен PyPy или где я мог бы легко установить PyPy, хотя я получаю около 0,1 секунды (с большим количеством измерительного шума) на 64-ядерном AMD Opteron 6376 1,4 ГГц (согласно / proc / cpuinfo) с gcc 4.4.6.

Реализация старается быть верной оригиналу, а не оптимизировать код за счет читабельности, не отказываясь при этом от очевидных оптимизаций. Интересно, что хвостовая рекурсия initVecRand()немного быстрее, чем цикл с инструкцией break с gcc и clang. Развертывание вручную одной итерации цикла convolveтестирования внутри основного цикла также приводило к ускорению, вероятно, из-за лучшего предсказания ветвлений.

Реймер Берендс
источник
Как вы получаете Nimrod для Ubuntu?
@Lembik Быстрый поиск в Google даст вам nimrod-lang.org/download.html
ace_HongKongIndependence
@ace Я также включил ссылку в свой пост (хотя сейчас я вижу, что это плохо видно с синим по черному).
Реймер Берендс
Вы могли бы ускорить это еще больше, увеличив размер семени до 128 бит: xorshift.di.unimi.it
user60561
2

Ява

Я перевел вышеуказанное решение C ++ на Java:

import java.util.Random;
import java.util.Arrays;

public class Bench2 {
  public static int[] bits = { 0x55555555, 0x33333333, 0x0F0F0F0F, 0x00FF00FF };
  public static int[] oneValues = { 1, 0, 0, 1 };
  public static int[] values = { -1, 0, 0, 1 };
  public static int n = 8;
  public static int m = 8;
  public static int iters = 1000;

  private static int x,y=34353,z=57768,w=1564;

  public static void main( String[] args ) {
    x = (int) (System.currentTimeMillis()/1000l);

    int[] leadingzerocounts = new int[ m ];
    Arrays.fill( leadingzerocounts, 0 );

    int maxS = 1 << 15;

    for( int s = 0; s < maxS; s++ ) {
      int x = interleaveBit( s );

      for( int i=0; i<iters; i++ ) {
        int random;

        do {
          random = 0xFFFF & fastRandom( );
        } while( sumOnes( random ) == 0 );

        int j = 7;

        while( j >= 0 ) {
          int h = ( x >> (j*2) );
          int l = 0xFFFF & (~(random ^ h));

          if( sumArray( l ) == 0 ) {
            leadingzerocounts[ j ]++;
          } else {
            break;
          }

          j--;
        }
      }
    }

    for( int i = 7; i >= 0; --i ) {
      System.out.print( leadingzerocounts[ i ] + " " );
    }

    System.out.println( );
  }

  public static int interleaveBit( int x ) {
    x = (x | ( x << 8)) & bits[3];
    x = (x | ( x << 4)) & bits[2];
    x = (x | ( x << 2)) & bits[1];
    x = (x | ( x << 1)) & bits[0];
    return x | (x << 1);
  }

  public static int sumOnes( int v ) {
    return (0xAAAA & (v ^ ~(v << 1)));
    // int s = 0;

    // for( int i = 0; i < 8; ++i ) {
    //   int a = 3 & ( v >> (i*2) );
    //   s += oneValues[ a ];
    // }

    // return s;
  }

  public static int sumArray( int v ) {
    return Integer.bitCount( v ) - 8;
    // int s = 0;

    // for( int i=0; i<8; ++i ) {
    //   int a = 3 & ( v >> (i*2) );
    //   s += values[ a ];
    // }

    // return s;
  }

  public static int fastRandom( ) {
    long t;
    t = x ^ (x << 11);
    x = y; y = z; z = w;
    return w = (int)( w ^ (w >> 19) ^ t ^ (t >> 8));
  }
}

На моей машине я получаю следующий вывод для программы Java:

time java Bench2
6330616 2524569 1040372 439615 193290 87131 40651 19607 
java Bench2  0.36s user 0.02s system 102% cpu 0.371 total

Программа OP работает на моем компьютере около 53 секунд:

time pypy start.py
[6330944, 2524897, 1040621, 439317, 192731, 86850, 40830, 19555]
pypy start.py  52.96s user 0.06s system 99% cpu 53.271 total

Программа на С ++ выполняется всего около 0,15 секунды:

time ./benchcc
[6112256, 2461184, 1025152, 435584, 193376, 87400, 40924, 19700]
./benchcc  0.15s user 0.00s system 99% cpu 0.151 total

Это примерно в 2,5 раза быстрее, чем соответствующее решение Java (я не исключал запуск виртуальной машины). Это Java-решение примерно в 142 раза быстрее, чем программа, выполняемая с PyPy.

Поскольку я лично был заинтересован, я установил iters100_000 для Java и C ++, но коэффициент 2,5 не уменьшился в пользу Java, если что-нибудь станет больше.

РЕДАКТИРОВАТЬ: Я запустил программы на 64-битном компьютере Arch Linux.

РЕДАКТИРОВАТЬ 2: Я хочу добавить, что я начал с грубого перевода кода Python:

import java.util.Random;
import java.util.Arrays;

public class Bench {
    public static int[] values = { -1, 0, 0, 1 };
    public static int n = 8;
    public static int m = 8;
    public static int iters = 1000;

    private static int x,y=34353,z=57768,w=1564; 

    public static void main( String[] args ) {
        x = (int) (System.currentTimeMillis()/1000l);

        int[] leadingzerocounts = new int[ m ];
        Arrays.fill( leadingzerocounts, 0 );

        int[] S = new int[ n+m-1 ];
        Arrays.fill( S, -1 );

        do {
            for( int i=0; i<iters; i++ ) {
                int[] F = new int[ n ];

                do {
                    randomArray( F );
                } while( containsOnlyZeros( F ) );

                for( int j=0; j < m && check( F, S, j ); j++ ) {
                    leadingzerocounts[ j ] += 1;
                }
            }
        } while( next( S ) );

        System.out.println( Arrays.toString( leadingzerocounts ) );
    }

    public static void randomArray( int[] F ) {
        for( int i = 0; i<F.length; i++ ) {
            F[ i ] = (1-(fastRandom()&3))%2;
        }
    }

    public static boolean containsOnlyZeros( int[] F ) {
        for( int x : F ) {
            if( x != 0 ) {
                return false;
            }
        }

        return true;
    }

    public static boolean next( int[] S ) {
        for( int i=0; i<S.length; i++ ) {
            if( ( S[ i ] = -S[ i ] ) == 1 ) {
                return true;    
            }
        }

        return false;
    }

    public static boolean check( int[] F, int[] S, int j ) {
      int sum = 0;

      for( int i=0; i<n; i++ ) {
          sum += F[ i ] * S[ j + i ];
      }

      return sum == 0;
    }

    public static int fastRandom( ) {
        long t;
        t = x ^ (x << 11);
        x = y; y = z; z = w;
        return w = (int)( w ^ (w >> 19) ^ t ^ (t >> 8));
    }
}

Эта программа работала около 3,6 секунд:

time java Bench   
[6330034, 2524369, 1040723, 439261, 193673, 87338, 40840, 19567]
java Bench  3.64s user 0.01s system 101% cpu 3.600 total

Что примерно в 14 раз быстрее, чем решение PyPy. (Выбор стандартной случайной функции вместо функции fastRandom приводит к времени выполнения 5 секунд)

dinfuehr
источник
2

Python 3.5 + numpy 1.10.1, 3.76 секунды

Тесты проводились на моем Macbook Pro. Код ОП занял ~ 6 минут на той же машине.

Причина, по которой я отвечаю на этот вопрос, на самом деле в том, что у меня нет 10 репутаций и я не могу ответить на часть I :-p

Последние несколько дней я пытался понять, как эффективно выполнять массивные свертки с помощью numpy (не полагаясь на сторонний пакет, даже на scipy). Когда я столкнулся с этой серией проблем во время исследования, я решил попробовать. Возможно, я пришел в эту игру поздно, но вот моя попытка использовать Python 3.5 и numpy 1.10.1.

def test_convolv():
    n = 8 
    m  = 8 
    iters = 1000
    ilow = np.ceil(0+n/2).astype(int)
    ihigh = np.ceil(m+n/2).astype(int)

    leadingzerocounts = np.zeros(m)

    # Pre-compute S & F
    S = np.array(list(itertools.product([-1,1], repeat = n+m-1)))
    choicesF = np.random.choice(np.array([-1, 0, 0, 1], dtype=np.int8), size=n*iters).reshape(iters,n)
    imask = ~np.any(choicesF, axis=1)
    while np.any(imask):
        imasksize = np.count_nonzero(imask)
        choicesF[imask,:] = np.random.choice(np.array([-1, 0, 0, 1], dtype=np.int8), size=n*imasksize).reshape(imasksize, n)
        imask = ~np.any(choicesF, axis=1)

    for i in np.arange(iters):
        F = choicesF[i, :]
        # This is where the magic is: by flattening the S array, 
        # I try to take advantage of speed of the np.convolve 
        # (really numpy.multiarray.correlate). 
        FS = (np.convolve(S.reshape(-1), F, 'same').reshape(S.shape))[:, ilow:ihigh]
        jmask_not = (FS[:, 0] != 0)
        leadingzerocounts[0] = leadingzerocounts[0]+np.count_nonzero(~jmask_not)
        for j in np.arange(n-1)+1:
            jmask = (FS[jmask_not, j] != 0)
            leadingzerocounts[j] = leadingzerocounts[j] + np.count_nonzero(~jmask)
            jmask_not[(jmask_not.nonzero()[0])[jmask]] = False

    print(leadingzerocounts)

Я предварительно вычислил массивы S и F и сгладил массив S при выполнении свертки, которая (основываясь на моих экспериментах) могла бы использовать скорость np.convolve. Другими словами, поскольку я не нашел подпрограммы векторизованной свертки, я фальшиво векторизовал код, сгладив весь массив, и надеялся, что np.convolved сделает векторизацию под капотом для меня, что, похоже, работает. Обратите внимание, что я использовал mode = 'same' и обрезал начальные и конечные элементы, которые были бесполезны.

На моем Macbook Pro результаты теста дают 3,76 секунды . Когда я запустил код OP (модифицированный до Python 3.5), я получил около 6 минут . Ускорение примерно в 100 раз.

Один недостаток состоит в том, что, поскольку массивы S и F должны храниться, требование к памяти может быть проблемой, если размеры слишком велики.

Я использовал тот же метод для первой части, и я получил ускорение ~ 60-100x на моем ноутбуке.

Поскольку я делал все на своем Macbook Pro, если бы кто-нибудь мог проверить мой код и сообщить мне, как он работает на вашем компьютере, я был бы очень признателен!

Trying.Too.Hard
источник
1

J, ускорение 130x ~ 50x ?

n =: m =: 8
len =: 1000
S =: (] - 0 = ])S0=: #:i.2^<:+/n,m
k =: (n#0) -.~ (_1 0 0 1) {~ (n#4) #: i.4^n
sn =: (]-0=])#:i.2^n
ku =: ~. k
M =: 0=+/"1 sn *"1/ ku
fs =: (ku&i.)"1 k
snum =: n #.\"1 S0

run =: 3 : 0
 r =: n#0
 for_t. (snum) do.
   rn =: fs{~? len # #k
   r =: r + +/"1*/\rn{"1 t{M
 end.
 r
)
echo run 0
exit''

Раз на случайном дебиане:

u#>time j slowpy.ijs
6334123 2526955 1041600 440039 193567 87321 40754 19714

real    0m2.453s
user    0m2.368s
sys     0m0.084s


u#>time python slow_pyth.py
[6331017, 2524166, 1041731, 438731, 193599, 87578, 40919, 19705]

real    5m25.541s
user    5m25.548s
sys     0m0.012s

Я думаю, что есть место для улучшения.

Eelvex
источник
Предполагается, что сценарий Python выполняется с использованием pypy, а не pythonпотому, что ваш сценарий ускоряется в 130 раз.
ace_HongKongIndependence
@ace да, я заметил, но я не могу установить pypy: - / Я думаю, что порядок останется, хотя.
Eelvex
Не обязательно ... i.imgur.com/n566hzw.png
ace_HongKongIndependence
Действительно, не обязательно.
Eelvex
Какая проблема у вас при установке pypy?
1

C ++: x200 (4-ядерный i7, должен масштабироваться до x400 на 8-ядерном)

В поисках более простого решения C ++ 11 (протестировано с VS 2012, gcc и clang) с распараллеливанием.

Чтобы это скомпилировать и запустить под Linux с помощью gcc 4.8.1:

g ++ -O3 -msse -msse2 -msse3 -march = native -std = c ++ 11 -pthread -Wl, - no-as-Необходимый golf.cpp

Под Linux нам также нужно std::launch::asyncфорсировать несколько потоков. Мне не хватало этого в более ранней версии.

В Visual Studio (2012+) это должно просто работать, но делать сборку релиза для синхронизации ...

На моем старом двухъядерном i3 это выполняется за ~ 0,9 секунды. На моем четырехъядерном процессоре i7 это 0.319 с против pypy 66 секунд.

На 8-ядерном i7 это должно быть в диапазоне ускорения x400. Переключение на массивы в стиле C ускорило бы это, но мне было интересно остаться с контейнерами C ++. Для меня интересно увидеть ускорение, которое вы можете получить, оставаясь относительно близко к проблемной области и на относительно высоком уровне, что я думаю, C ++ действительно хорош. Также следует отметить относительную простоту распараллеливания с использованием конструкций C ++ 11.

Битовое решение @ ilmale очень круто и работает на -1/1/0. Можно также бросить SSE на это и, возможно, получить значительное ускорение.

Помимо распараллеливания, есть еще одна «хитрость», которая заключается в уменьшении количества сумм. Пример результатов: 6332947 2525357 1041957 438353 193024 87331 40902 19649

#include <vector>
#include <iostream>
#include <thread>
#include <future>
#include <time.h>
#include <ctime>
#include <algorithm>

using namespace std;

// Bring some of these constants out to share
const size_t m = 8;
const int nthreads = 16;
const size_t cn = 15;
const int two_to_cn = 32768;

static unsigned int seed = 35;

int my_random() // not thread safe but let's call that more random!
{
   seed = seed*1664525UL + 1013904223UL; // numberical recipes, 32 bit
   return ((seed>>30)&1)-!!((seed>>30)&2); // Credit to Dave!
}

bool allzero(const vector<int>& T)
{
   for(auto x : T)
   {
      if(x!=0)
      {
         return false;
      }
   }
   return true;
}

// Return the position of the first non-zero element
size_t convolve_until_nonzero(size_t max_n, const vector<int>& v1, const vector<int>& v2)
{
   for(size_t i = 0; i<max_n; ++i)
   {
      int result = 0;
      for(size_t j = 0; j<v2.size(); ++j)
      {
         result += v1[i+j]*v2[j];
      }
      if(result!=0)
      {
         return i;
      }
   }
   return max_n;
}

void advance(vector<int>& v)
{
   for(auto &x : v)
   {
      if(x==-1)
      {
         x = 1;
         return;
      }
      x = -1;
   }
}

vector<int> convolve_random_arrays(vector<int> S, int range)
{
   const int iters = 1000;
   int bothzero = 0;
   int firstzero = 0;

   time_t current_time;
   time(&current_time);
   seed = current_time;


   vector<int> F(m);
   vector<int> leadingzerocounts(m+1);

   for(auto &x: leadingzerocounts)
   {
      x = 0;
   }

   for(int i=0; i<range; ++i)
   {
      for(int j=0; j<iters; ++j)
      {
         do
         {
            for(auto &x : F)
            {
               x = my_random();
            }
         } while(allzero(F));
         leadingzerocounts[convolve_until_nonzero(m, S, F)]++;
      }
      advance(S);
   }

   // Finish adding things up...
   for(int i=m-1; i>0; --i)
   {
      leadingzerocounts[i] += leadingzerocounts[i+1];
   }

   vector<int> withoutfirst(leadingzerocounts.begin()+1, leadingzerocounts.end());
   return withoutfirst;
}

int main(int argc, char* argv[])
{

   vector<int> leadingzerocounts(m);

   for(auto &x: leadingzerocounts)
   {
      x = 0;
   }

   clock_t start = clock();

   vector<int> S(cn);
   for(auto &x : S)
   {
      x = -1;
   }

   vector< future< vector< int > > > fs; // The future results of the threads

   // Go make threads to work on parts of the problem
   for(int i=0; i<nthreads; ++i)
   {
      vector<int> S_reversed = S; // S counts using LSBs but we want the thread start to be in MSBs
      reverse(S_reversed.begin(), S_reversed.end());
      fs.push_back(async(std::launch::async, convolve_random_arrays, S_reversed, two_to_cn/nthreads));
      advance(S);
   }
   // And now collect the data
   for(auto &f : fs)
   {
      vector<int> result = f.get();
      for(int i=0; i<result.size(); ++i)
      {
         leadingzerocounts[i] += result[i];
      }
   }

   for(auto count : leadingzerocounts)
   {
      cout << count << endl;
   }

   return 0;
}
Гай Сиртон
источник
1

Фортран: 316x

Ладно, Фортран: Я получил ускорение до 106x 155x 160x 316x при использовании Xorshift RNG и OpenMP на 4-ядерном процессоре i7. Кроме этого, нет больших хитростей. Для итератора для построения S я просто использую двоичное представление 16-разрядного целого числа i. Вы заметите, что помимо встроенного RNG и «итератора» / отображения от i до S, код является таким же высокоуровневым, как и код Python.

Редактировать: убрал «если» в Xorshift, теперь используя «r = abs (w / ...)» вместо «r = w / ...». Идет от 106х до 155х.

Edit2: генерирует в 15 раз больше случайных чисел, чем решение C ++. Если у кого-то есть решение с нулевыми издержками для преобразования случайного целого в массив из 0 и 1 в Фортране, я весь в ушах. Тогда мы могли бы победить C ++ :)

Edit3: первое редактирование представило ошибку, как указал Лембик. Это исправлено сейчас, с небольшим улучшением в ускорении. Я постараюсь использовать предложение Eelvex, чтобы ускорить процесс.

Edit4: Профилирование показало, что преобразование в вещественное и обратно в целое число с помощью nint () было медленным. Я заменил это одним целочисленным делением, выполняющим как масштабирование, так и округление, с ускорением 160x до 316x.

Компилировать с:

gfortran -O3 -march = native -fopenmp golf.f90

program golf
implicit none
integer, parameter :: m=8, n=8
integer :: F(n), S(m+n-1), leadingzerocounts(m)
integer :: j,k,bindec,enc,tmp,x=123456789,y=362436069,z=521288629,w=88675123
integer*2 :: i
real :: r

leadingzerocounts=0

!$OMP parallel do private(i,enc,j,bindec,S,F,k,tmp,x,y,z,w,r) reduction(+:leadingzerocounts) schedule(dynamic)
do i=0,32766
  enc=i
  ! Short loop to convert i into the array S with -1s and 1s
  do j=16,2,-1
    bindec=2**(j-1)
    if (enc-bindec .ge. 0) then
      S(j-1)=1
      enc=enc-bindec
    else
      S(j-1)=-1
    endif
  end do
  do j=1,1000
    F=0
    do while (.not. any(F /= 0))
      do k=1,n
        ! Start Xorshift RNG
        tmp = ieor(x,ishft(x,11))
        x = y
        y = z
        z = w
        w = ieor(ieor(w,ishft(w,-19)),ieor(tmp,ishft(tmp,-8)))
        ! End Xorshift RNG
        ! Just scale it inside the nint:
        !F(k)=nint(w/2147483648.0)
        ! Scaling by integer division is faster, but then we need the random 
        ! number to be in (-2,2) instead of [-1,1]:
        F(k)=w/1073741824

      end do
    end do
    do k=1,m
      if (dot_product(F,S(k:k+n-1)) /= 0) exit
      leadingzerocounts(k)=leadingzerocounts(k)+1
    end do
  end do
end do
!$OMP end parallel do

print *, leadingzerocounts

end

Пример вывода:

$ time ./a.out
6329624 2524831 1039787 438809 193044 6860 40486 19517
./a.out 1.45s пользователь 0.00s система 746% процессор 0.192 всего

Код ОП:

$ time pypy golf.py
pypy golf.py 60.68s пользователь 0.04s система 99% процессор 1: 00.74 всего

пол-внешний
источник
То, что я использовал в J, было предварительно скомпилированным списком из 4 ^ n чисел в base-4, затем преобразованным в триаду и исключающим 0. ГСЧ просто выбирает из этого списка.
Eelvex
Я не уверен, что ваш код правильный. Для 100 000 итераций я получаю 633140285 271390368 118307997 52751245 23725837 10744292 4944464 2388125, но я думаю, что это должно быть ближе к 633170604 252560981 104156146 43911426 19316309 8713324 4073378 1959440, что является последовательной разницей между.
1
Ах, спасибо, @Lembik, моя правка в ускорении (удаление оператора if) действительно была ошибкой. Я обновил свой код, так что теперь он должен быть правильным. Я постараюсь опубликовать версию, используя предложение Eelvex позже.
полу-посторонний
Это также ускорило это, кажется!
Да, небольшое ускорение, я думаю. Я понял, что добавляю 1.0, а затем вычитаю 1.0 внутри узкого цикла.
полу-посторонний