Pythonは本当に遅い(パートII)


52

これは、Pythonが実際どれくらい遅いかというフォローアップです (または、どのくらいの速あなたの言語で?)

最後の質問で100倍のスピードアップを得るのは少し簡単すぎることがわかりました。チャレンジを楽しんでいるが、低レベルのスキルを実際に使用できるより難しいものが必要な場合は、パートIIをご覧ください。課題は、私のコンピューターでテストされた次のpythonコードのx100の高速化を得ることです。

さらに難しくするために、今回はpypyを使用しています。私の現在のタイミングは、pypy 2.2.1を使用して1分7秒です。

ルール

  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]

コード内でランダムシードを使用する必要があり、上記に近い回答を提供するのに十分な乱数ジェネレーターが受け入れられます。

私のマシンタイミングは私のマシンで実行されます。これは、AMD FX-8350 8コアプロセッサへの標準のUbuntuインストールです。これは、コードを実行できる必要があることも意味します。

コードの説明

このコードは、-1と1で構成される長さn + m-1のすべての配列Sを反復処理します。各配列Sに対して、長さnが-1、0または1で構成される1000個の非ゼロランダム配列Fをサンプリングし、各値を取得する確率を1 / 4、1 / 2、/ 14にします。次に、Fと、長さnのSの各ウィンドウとの間の内積を計算し、非ゼロの内積を見つけます。leadingzerocountsゼロの内積が見つかった各位置に1を加算します。

状態

  • Perlの。@tobyinkによる2.7倍のスローダウン。(cpythonではなくpypyと比較。)

  • J。@Eelvexによる39倍の高速化。

  • C。@aceで59倍高速化します。
  • ジュリア。@ one-more-分による起動時間を含まずに197倍高速。起動時間を含めて8.5倍に高速化されます(この場合、8個よりも4個のプロセッサを使用した方が高速です)。
  • Fortranの。@ semi-extrinsicにより438倍に高速化されます。
  • Rpython。@primoによって258倍の速度になります。
  • C ++。@ilmaleにより508倍の速度になります。

(新しい改善が速すぎてitersが小さすぎたため、新しい改善のタイミングを止めました。)


1秒未満のタイミングは信頼性が低く、一部の言語には起動コストがかかることが指摘されました。引数は、含める場合はC / C ++などのコンパイル時間も含める必要があるということです。ここに、反復回数が100,000に増加した最速のコードのタイミングを示します。

  • ジュリア。@ one-more-minuteで42秒。
  • C ++。@GuySirtonによる14秒。
  • Fortranの。@ semi-extrinsicによる14秒。
  • C ++。@ilmaleによる12秒。
  • Rpython。@primoによる18秒。
  • C ++。@Stefanによる5秒。

勝者は..ステファンです!

フォローアップの課題が投稿されました。あなたはどれくらい高く行くことができますか?(コーディング+アルゴリズムの挑戦)。これは難しいです。


3
我々はそれだけでポートを書き換えるとすることはできませんので、コードを達成するために仮定されているものの説明は、いいだろう
Einacio

6
私が実行できるコードを最初に送信した人は正しいです。私のマシンで100倍高速であるため、すぐに勝ち、競争が終了します。」そのような競争を終了する目的は何ですか?他のほとんどの日付のように日付の期限を使用して、他の言語でさらに期限を短縮できるのはなぜですか?
grovesNL

5
@Einacioそれはいいアイデアです。誰も気にしないことを願うルールを変更しました。

1
@Lembik Fortranバージョンを改善し、マシンで2倍高速になりました。もう一度時間をいただけますか?:)
半外因

1
@ semi-extrinsic完了。

回答:


12

C ++ビットマジック

〜16msのマルチスレッド、56msのシングルスレッド。〜4000の高速化。

(スピードアップはi7-2820QMのマルチスレッドコードと質問で言及された1分9秒に基づいています。OPのシステムはCPUよりもシングルスレッドのパフォーマンスが劣りますが、マルチスレッドのパフォーマンスが優れているため、この数値は正確です)

マルチスレッド部分は、スレッドの生成のために非常に非効率的です。私はおそらくカスタムジョブライブラリを活用することでより良い結果を得ることができましたが、Unixシステムではバグがあります

編集する

各スレッドに独自の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)、leadingZeros.data()、sizeof(leadingZeros [0])* m );

string.hを含める必要があると思います。もう一回やってみよう。
ステファン

これをg ++ -O3 -std = c ++ 0x -pthread -Wl、-no-as-needed Stefan.cpp -o Stefan

16

C ++ x150 x450 x530

配列の代わりに、ビット(およびダークマジック)を使用しました。
@aceの高速ランダム関数に感謝します。

仕組み:整数の最初の15ビットはs配列を表しS[15]ます。ゼロは-1を表し、ゼロは+1を表します。配列Fも同様の方法で構築されます。ただし、各シンボルに2ビットあります。

  • 00は-1を表します
  • 01と10は0を表します
  • 11は1を表します

原因SFは異なる表現を持っている私はSそれと比較するために自分自身とインターリーブする必要がありFます。

  • 0(-1)00になった(-1の表現でF
  • 1(+1)(の表現で+1 11なりましたF

これで、Carnotを使用して内積を計算できます。1つの変数は値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を使用CPUはi7 8コアです。

おそらく、コンパイラーのパラメーターを調整するmsを得ることができます。

これは私のマシンでの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のゲインが得られ、OPコードに対するx450のパフォーマンスが向上します。: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 1を有し、0表す
11を2 1Sを有し、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;
}

これをテストするのが待ちきれません!悲しいことに、これは数時間はかかりません。

1
以下は編集案として提案されたものですが、コメントとしてはより適していると思います。sumOnes、sumArrayを次のものに置き換えることができます(openmpバージョンの2倍の速度を実現しているようです)。 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:プロファイラーは、プログラムがこれらの2つの機能にすべての時間を費やしたと言っていましたが、昨日はそれより良いことを考えると本当に疲れていました。今晩(UTC)に挑戦します。ありがとう。
イルマーレ

2番目のコードスニペットを完全なコピーおよび貼り付け可能なコードに変更してもよろしいですか?あなたのopenmpコードを機能させるために、私は何か間違ったことをしなければなりません。

いいね これはビット演算でできると思いました。
ガイサートン14

10

ジュリア:0.7秒、120倍高速

user20768が示したように、Juliaへのコードの単純な移植は、PyPyの約2倍の速度です。しかし、私たちはそれよりもはるかに良いことができます。

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プレリリースでは、PyPyの場合は0.7秒と1分22秒かかります。

コンピューターに十分なコアがあり、おそらくいくつかのAWSインスタンスをスピンアップしている場合は、もう少し削ることができるはずです:)


タイミングを間違って測定していると確信しています。Python with PypyもJITベースの言語ですが、OPによって行われるタイミングはJITコンパイル時間が含まれます。あなたはそれを除外しています。最新のJulia gitバージョンをインストールし、コードをテストしました。私のマシンでは、8プロセスのコマンドを完了するのに6.6秒かかりますが、「elapsed time 0.588 .. seconds」と出力されます。
半外因性14

PythonのタイミングにはPyPyの起動とJITのウォームアップが含まれますが、これにはせいぜい数秒しかかかりません。1分間の実行時間の違いは無視できます。OPがPythonのタイミングを変更する(それは何の違いもありません)が、うれしいですが、Juliaの起動時間を含めることは合理的ではありません。
1分以上、14

元の質問へのコメントでOPを尋ねたところ、彼は「タイミングにはJIT言語のすべてを含めるべきだ」と言った。彼はまた、解決策が1秒よりもはるかに長くかかるという新たな課題を生み出し、ジュリアを競争に追い込むと述べました。
性14

その場合、最適な解決策はシリアルアルゴリズムを使用することです。これには約2秒かかります。私はコードを公開しますが、C ++が他のすべてよりも速く起動することを誰もがすでに知っているため、この競争は今やや冗長になっています。

Fortranソリューションを投稿したばかりなので、最速のJuliaを投稿しない理由はわかりません(既にコードを持っている場合)。
性14

5

C、1.210秒

私のマシンで1m45.729sを実行するOPのコードを使用します。

コンパイル:

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

1
興味深いことに、これらの最適化フラグをすべて削除するときに、同様の観察を行うことができます。-march=native -fwhole-program -fstrict-aliasing -ftree-vectorizeBtwを試してください。MT19937を含むいくつかのC ++ 11を使用して、4秒未満になりましたuniform_int_distribution
dyp 14

1
1.119秒で約59倍のスピードアップ!

1
@aceはい、私はこれを指摘したかっただけです。C ++の標準ライブラリPRNGをいくつか試すだけで簡単になりました。PRNGの1つの32ビット整数結果を使用して、の8つのエントリを生成できることに注意してくださいF
dyp 14

1
はとn等しいので8、おそらくAVX(または2 * SSE)を使用して、適切なSストレージで内積を計算できます。
マイケルM. 14

2
SSEバージョン、小さな高速化:gist.github.com/anonymous/11394210(含めることを忘れないでくださいsmmintrin.h
マイケルM.

5

Perl

これはCソリューションほど高速ではありませんが、高レベルのインタプリタ言語としてはかなり高速です。Python実装の実行時間を約40%短縮します。

#!/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;

Algorithm :: CombinatoricsはUbuntu(sudo apt-get install libalgorithm-combinatorics-perl)で利用可能です。使用される他のモジュールはコアPerlモジュールであるため、基本のUbuntuインストールの一部として既にインストールされている必要があります。


1
速度には影響しませんが0..N-1、最後の範囲mapですよね?忘れましたuse warningsか?:-) OPのロジックはわかりにくいですが、スライディングウィンドウがの最後の要素に到達することはありませんS
user2846289

あ。配列のサイズが一致しないと考えたためwarnings、欠落している要素をゼロとして扱うことを無効にしました。N-1これを改善します。また、実際には速度がわずかに向上します。Pythonの実装よりも約40%高速です。
tobyink

コードには、List :: Utilの最新バージョンが必要だと思います。Ubuntuの14.04で私は、「任意の」リストでエクスポートされません:: Utilのモジュールを取得

そうそう、それは本当です-CPANからList :: Utilをインストールする必要があるでしょう。anyまたは、List :: MoreUtilsにあります。これは、コアモジュールではありませんが、最も一般的に使用されるCPANモジュールの1つです。
tobyink

4

ジュリア:4.66倍遅い!

私は本当に彼らのウェブサイトの統計を疑い始めています...

次のJuliaコードは、最適化なしでOPのPythonコードを直接転写したものであることに注意してください。この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 m 32.912 s

PyPyでのOPのコード:1 m 11.506 s

ジュリアの出力:

6332170
2525472
1041522
438761
193119
86873
40705
19662

7
<s>恥知らず</ s>のスポーツマンシップのために+1。
ace_HongKongIndependence

グローバル変数、インポート、および配列内包表記は遅いです。これは、パフォーマンスのためにJuliaプログラムを通常記述する方法ではありません。
アレックスA.

4

RPython 0.187s(258x高速)

PyPy2.2.1を使用した元のソース:1分6.718秒

スレッド化により、標準Pythonのバックサポートは廃止されました。ワーカースレッドの数は、コマンドラインパラメーターとして指定できます。デフォルトは2です。

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を使用してコンパイルできます。その表現された目的は、言語インタープリターの作成を支援することですが、上記のような単純なプログラムのコンパイルにも使用できます。パイソンの「手の込んだ」機能、などのほとんど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ビットマップとして解釈されます:[ 01]→[ -11]

同様に、Fビットマップは単一の値を表す各2ビットでもあります:
[ 00011011]→[ 、-1、、0 ]01

乗算を実行するのではなく、真理値表を使用して製品を検索します。

32ビットの符号付き整数が使用されるため、n15 n+m以下、31以下rpython.rlib.rbigintである場合があります。必要に応じて、モジュールで任意の整数サポートを実現できます。

内積ループの最初の反復は展開され、のnullityテストと結合されFます。

自作PRNGが使用され、ソースがリストされています。この論文の著者は2 32 -1の期間を示しており、Diehardのすべてのテストに合格すると主張しますが、私はこれを個人的に確認していません。

ランダムシードはミリ秒ごとに変化します。これは、タイムスタンプの使用が許す限りほぼ同じです。さらに、各ワーカースレッドxorはこの値でプロセスIDを使用して、それぞれが異なるシードを持つようにします。


サンプルのタイミング

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

OPの元のソース:

$ 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で実行できる同等の純粋なPythonプログラムがありますか?

@Lembik見たいです。純粋なpythonでの私の最初の試みが15秒以下だったので、4.7秒はかなり良いと思いました。
primo 14

はい、遅れて申し訳ありません。まだコードを実行していませんが、できるだけ早く実行します。

JITを追加してみてください。これで高速になります!
kirbyfan64sos 14年

@Lembikは言及に感謝します;)好奇心から、4つのワーカースレッド、または8つのワーカースレッドで最も速く実行されましたか?
primo

3

ジュリア:1分21.4秒(2.2倍高速)(Armanのコードの変更)

PyPyでのOpのコード: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 

Armanのコードにはいくつかの問題があり、非常に遅くなります。多くの匿名関数と高次関数を不必要に使用します。ベクトルFのすべてがゼロかどうかをテストするには、all(x-> x == 0、F)ではなくall(F == 0)だけを書いてみませんか?それは短く、文字通り千倍高速です。

また、単純にdot(x、y)の代わりにsum(map(*、x、y))をドット積として使用します。最初のバージョンは、10k倍のベクトルに対して650倍遅くなります。また、純積ジュリアでは、内積関数がforループとして実装されています。

また、配列の理解は遅いです。j = 1:nの場合、[[-1 0 0 1] [rand(1:4)]]の代わりに[0,1,0、-1] [rand(1:4、n)]と書く方が良い。

最後に、グローバル変数はジュリアの悪いジュジュです。JITは、JITと型推論が機能するような方法でコードを記述した場合にのみ高速です。これの大部分は型の安定性です。たとえば、コンパイラーは、ループ内で変数の型が変更されないようにする必要があります。


ありがとう!速度について主張する前に、ジュリア言語について学ぶべきことはまだたくさんあると思います:)コードにいくつかの些細な修正を加えるだけで、実行時間が数倍に増加することを実に嬉しく思います。
軽快な寒天

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:-fltoLTOをサポートしない古いCコンパイラがある場合は省略します。--cc=...Cコンパイラのデフォルトの選択で問題ない場合は、オプションを省略します。コードにはNimrod 0.9.4または0.9.5が必要です。

私のクアッドコアiMac(2.66 GHzコアi5)では、コードはgcc 4.9で約.15秒、clangで.16秒で実行されますが、PyPy 2.2.1では88秒(500倍以上の高速化)です。残念ながら、64コアAMDで約0.1秒(測定ノイズが多い)になりますが、PyPyがインストールされているか、PyPyを簡単にインストールできる4コア以上のマシンにはアクセスできません。 Opteron 6376 1.4 GHz(/ proc / cpuinfoによる)、gcc 4.4.6。

実装は、明白な最適化を忘れずに、読みやすさを犠牲にしてコードを最適化するのではなく、元のものに忠実にしようとします。興味深いことにinitVecRand()、gccとclangの両方を使用したbreak命令を使用したループよりも、末尾再帰の方が少し高速です。convolveメインループ内でテストループの1つの反復を手動で展開すると、おそらくより良い分岐予測のために速度が向上しました。


UbuntuのNimrodを取得する方法

@Lembik Googleで簡単に検索すると、nimrod-lang.org / download.html
ace_HongKongIndependence

@aceリンクも投稿に含めていました(ただし、今では黒を青で見たときに見づらくなっています)。
ライマーBehrends

シードサイズを128ビットに増やすことで、これをさらに高速化できます。xorshift.di.unimi.it
user60561

2

Java

上記の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

c ++プログラムは約0.15秒しか実行しませんでした。

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

これは、対応するJavaソリューションよりも約2.5倍高速です(VMの起動を除外しませんでした)。このJavaソリューションは、PyPyで実行されるプログラムよりも約142倍高速です。

個人的には興味があったのでiters、JavaとC ++の場合は100_000 に設定しましたが、何かが大きくなっても、2.5の係数はJavaを優先して減少しませんでした。

編集:64ビットArch Linux PCでプログラムを実行しました。

EDIT2:私は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

これはPyPyソリューションよりも約14倍高速です。(fastRandom関数よりも標準のランダム関数を選択すると、実行時間が5秒になります)


2

Python 3.5 + numpy 1.10.1、3.76秒

テストは私のMacbook Proで実行されました。OPのコードは、同じマシン上で約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倍です。

欠点の1つは、SおよびF配列が格納されるため、サイズが大きすぎる場合にメモリ要件が問題になる可能性があることです。

パートIでも同じ方法を使用しましたが、ラップトップで60〜100倍の高速化を実現しました。

Macbook Proですべてを行ったので、誰かが私のコードをテストして、それがあなたのマシンでどのように動作するかを教えてくれたら、とても感謝しています!


1

J、130倍から50倍高速化?

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''

ランダムdebianの時間:

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

改善の余地があると思います。


Pythonスクリプトはではpypyなくを使用して実行されることになっpythonているため、スクリプトは130倍の高速化を実現しているようです。
ace_HongKongIndependence 14

@ace yesはい、気づきましたが、pypyをインストールできません。
エルベックス14

必ずしもではありません... i.imgur.com/n566hzw.png
ace_HongKongIndependence

確かに、必ずしもそうではありません。
エルベックス14

pypyのインストールでどんな問題がありますか?

1

C ++:x200(4コアi7、8コアでx400にスケーリングする必要があります)

並列化を使用した、より簡単なC ++ 11(VS 2012、gcc、clangでテスト済み)ソリューションを試してください。

これをgcc 4.8.1でLinuxでコンパイルおよび実行するには:

g ++ -O3 -msse -msse2 -msse3 -march = native -std = c ++ 11 -pthread -Wl、-no-as-needed golf.cpp

Linuxでは、std::launch::async複数のスレッドを強制する必要もあります。以前のバージョンではそれがありませんでした。

Visual Studio(2012+)ではこれは機能するはずですが、タイミングを考慮してリリースビルドを作成します...

私の古いデュアルコアi3では、これは約0.9秒で実行されます。私のi7クアッドコアでは、pypy 66秒に対して0.319秒です。

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

Fortran:316x

わかりました、Fortran:4コアi7 CPUでXorshift RNGとOpenMPを使用すると、最大106 倍、 155 160 倍、316倍に高速化されました。それ以外には、大きなトリックはありません。イテレータがSを構成するために、16ビット整数iのバイナリ表現を使用します。インラインRNGおよびiからSへの「イテレータ」/マッピング以外は、コードはPythonコードと同じ高レベルであることに注意してください。

編集:Xorshiftの「if」を削除し、「r = w / ...」の代わりに「r = abs(w / ...)」を使用するようになりました。106xから155xになります。

Edit2:これにより、C ++ソリューションの15倍の乱数が生成されます。誰かが、Fortranでランダムなintを0と1の配列に変換するためのオーバーヘッドのないソリューションを持っているなら、私はすべて耳です。それから私達はC ++を打つことができます:)

Edit3:Lembikが指摘したように、最初の編集ではバグが発生しました。この問題は修正され、スピードアップがわずかに改善されました。Eelvexの提案を使用して、さらに高速化を試みます。

Edit4:プロファイリングは、nint()で実数に変換して整数に戻すのが遅いことを示しました。これをスケーリングと丸めの両方を行う1つの整数除算に置き換え、160倍から316倍に高速化しました。

コンパイル:

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%cpu 0.192合計

OPのコード:

$ time pypy golf.py
pypy golf.py 60.68sユーザー0.04sシステム99%cpu 1:00.74合計


Jで使用したのは、ベース4の4 ^ n数のビルド前リストで、3進数に変換され、0を除外しました。RNGはこのリストから選択するだけです。
エルベックス14

あなたのコードが正しいかどうかわかりません。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を引くことに気付きました。
半外因性
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.