5

私は恥ずかしいほど並列問題に Python の Multiprocessing パッケージを使用することを学んでいるので、自然数n以下の素数を決定するための直列バージョンと並列バージョンを作成しました。ブログ投稿スタック オーバーフローの質問から読んだ内容に基づいて、次のコードを思いつきました。

シリアル

import math
import time

def is_prime(start, end):
    """determine how many primes within given range"""
    numPrime = 0
    for n in range(start, end+1):
        isPrime = True
        for i in range(2, math.floor(math.sqrt(n))+1):
            if n % i == 0:
                isPrime = False
                break
        if isPrime:
            numPrime += 1
    if start == 1:
        numPrime -= 1  # since 1 is not prime
    return numPrime

if __name__ == "__main__":
    natNum = 0
    while natNum < 2:
        natNum = int(input('Enter a natural number greater than 1: '))
    startTime = time.time()
    finalResult = is_prime(1, natNum)
    print('Elapsed time:', time.time()-startTime, 'seconds')
    print('The number of primes <=', natNum, 'is', finalResult)

平行

import math
import multiprocessing as mp
import numpy
import time


def is_prime(vec, output):
    """determine how many primes in vector"""
    numPrime = 0
    for n in vec:
        isPrime = True
        for i in range(2, math.floor(math.sqrt(n))+1):
            if n % i == 0:
                isPrime = False
                break
        if isPrime:
            numPrime += 1
    if vec[0] == 1:
        numPrime -= 1  # since 1 is not prime
    output.put(numPrime)


def chunks(vec, n):
    """evenly divide list into n chunks"""
    for i in range(0, len(vec), n):
        yield vec[i:i+n]

if __name__ == "__main__":
    natNum = 0
    while natNum < 2:
        natNum = int(input('Enter a natural number greater than 1: '))
    numProc = 0
    while numProc < 1:
        numProc = int(input('Enter the number of desired parallel processes: '))
    startTime = time.time()
    numSplits = math.ceil(natNum/numProc)
    splitList = list(chunks(tuple(range(1, natNum+1)), numSplits))
    output = mp.Queue()
    processes = [mp.Process(target=is_prime, args=(splitList[jobID], output))
                 for jobID in range(numProc)]
    for p in processes:
        p.start()
    for p in processes:
        p.join()
    print('Elapsed time:', time.time()-startTime, 'seconds')
    procResults = [output.get() for p in processes]
    finalResult = numpy.sum(numpy.array(procResults))
    print('Results from each process:\n', procResults)
    print('The number of primes <=', natNum, 'is', finalResult)

n = 10000000に対して得られるものは次のとおりです (並列の場合、8 つのプロセスを要求します)。

$ python serial_prime_test.py 
Enter a natural number greater than 1: 10000000
Elapsed time: 162.1960825920105 seconds
The number of primes <= 10000000 is 664579
$ python parallel_prime_test.py
Enter a natural number greater than 1: 10000000
Enter the number of desired parallel processes: 8
Elapsed time: 49.41204643249512 seconds
Results from each process:
[96469, 86603, 83645, 80303, 81796, 79445, 78589, 77729]
The number of primes <= 10000000 is 664579

それで、3倍強のスピードアップが得られるようです。ここに私の質問があります:

  1. 明らかに、これは直線的なスピードアップではありません。では、どれだけ改善できるでしょうか (または、現実的にどのようなスピードアップを期待すべきでしょうか)。
  2. アムダールの法則がこれに答えているように見えますが、プログラムのどの部分が厳密にシリアルであるかを判断する方法がわかりません。

どんな助けでも大歓迎です。

編集:ハイパースレッディングが可能な4つの物理コアがあります。

4

2 に答える 2

6

仕事を別の方法で分割したいと思います。

プログラムは候補整数の範囲をコア間で均等に分割しますが、各範囲の作業は均等ではない可能性があります。つまり、一部のコアは早期に終了し、何もすることがなく、他のコアはまだ実行中です。それは並列効率を失います

要点を説明するために、1000 個のコアがあると想像してください。最初のコアは非常に小さな候補数を認識し、それらを素因数分解するのに時間がかからず、アイドル状態になります。最後の (1000 番目の) コアは、非常に大きな候補数のみを認識し、それらを分解するのにはるかに長い時間がかかります。したがって、最初のコアがアイドル状態になっている間、実行されます。無駄なサイクル。4コアも同様。

コアに渡される作業の量が不明な場合にやりたいことは、コアの数よりも多くの適度なサイズのチャンクをすべてのコアに渡すことです。その後、コアは不均一な速度で終了する可能性があり、各コアは戻って、もう少しやるべき仕事を見つけます。これは本質的にワークリスト アルゴリズムです。最後にむらができますが、それは小さなチャンクにすぎないので、あまり無駄にはなりません。

私は Python プログラマーではないので、代わりに Parlanse でソリューションをコーディングしました。

(includeunique `Console.par')
(includeunique `Timer.par')

(define upper_limit 10000000)

(define candidates_per_segment 10)
(define candidates_per_segment2 (constant (* candidates_per_segment 2)))

(= [prime_count natural] 0)
[prime_finding_team team]

(define primes_in_segment
(action (procedure [lower natural] [upper natural])
   (;;
      (do [candidate natural] lower upper 2
      (block test_primality
        (local (= [divisor natural] 3)
           (;;
              (while (< (* divisor divisor) candidate)
                  (ifthenelse (== (modulo candidate divisor) 0)
                     (exitblock test_primality)
                     (+= divisor 2)
                  )ifthenelse
              )while
              (ifthen (~= (* divisor divisor) candidate)
                 (consume (atomic+= prime_count))
              )ifthen
           );;
        )local
      )block
      )do
  );;
  )action
)define

(define main
(action (procedure void)
   (local [timer Timer:Timer]
     (;;
     (Console:Put (. `Number of primes found: '))
     (Timer:Reset (. timer))
     (do [i natural] 1 upper_limit candidates_per_segment2
        (consume (draft prime_finding_team primes_in_segment
                     `lower':i
                     `upper':(minimum upper_limit (- (+ i candidates_per_segment2) 2))))
     )do
     (consume (wait (@ (event prime_finding_team))))
     (Timer:Stop (. timer))
     (Console:PutNatural prime_count)
     (Console:PutNewline)
     (Timer:PrintElapsedTime (. timer) (. `Parallel computed in '))
     (Console:PutNewline)
     );;
  )local
)action
)define

Parlanse は LISP のように見えますが、C のように機能し、コンパイルします。

ワーカーはprimes_in_segmentです。パラメータlowerおよびupperによって定義された候補値の範囲を取ります。その範囲内の各候補を試し、その候補が素数である場合は、合計prime_countを (アトミックに) 増やします。

mainの do ループによって、全範囲が範囲の小さなパケット (奇数のシーケンス) に分割されます。並列処理は、draftコマンドで発生します。これは、(Windows スレッドではなく) 計算の並列実行グレインを作成し、それをすべての素因数分解を表す作業の集合セットであるprime_finding_teamに追加します。(チームの目的は、このすべての作業を 1 つの単位として管理できるようにすることです。たとえば、必要に応じて破棄し、このプログラムでは必要ありません)。ドラフトへの引数フォークされたグレインによって実行される関数とそのパラメーターです。この作業は、ワークスティーリング アルゴリズムを使用して、Parlanse が管理する (Windows) スレッドのセットによって実行されます。作業が多すぎる場合、Parlanse は作業を生成するグレインを抑制し、そのエネルギーを純粋な計算であるグレインの実行に費やします。

各グレインに 1 つの候補値のみを渡すこともできますが、その場合、候補ごとの fork オーバーヘッドが増え、それに応じて全体の実行時間が悪化します。候補の範囲ごとのフォークのオーバーヘッドが小さくなるように、経験的に10を選択しました。セグメントごとの候補を 1000 に設定しても、それほど高速化されません。

doループは、可能な限り高速に作業を行うだけです。Parlanseは、有用な並列性が十分にある場合、ドラフトステップを抑制します。チームイベントの待機により、メインプログラムはすべてのチームメンバーが完了するまで待機します。

HP hex-core AMD Phenom II X6 1090T 3.2 GHz でこれを実行しました。実行の実行は以下のとおりです。最初に 1 CPU の場合:

 >run -p1 -v ..\teamprimes
PARLANSE RTS: Version 19.1.53
# Processors = 1
Number of primes found: 664579
Parallel computed in 13.443294 seconds
---- PARLANSE RTS: Performance Statistics
  Duration = 13.527557 seconds.
  CPU Time Statistics:
  Kernel CPU Time: 0.031s
  User   CPU Time: 13.432s
  Memory Statistics:
Peak Memory Usage    : 4.02 MBytes
  Steals: 0  Attempts: 0  Success rate: 0.0%  Work Rediscovered: 0
Exiting with final status 0.

次に、6 つの CPU の場合 (適切にスケーリングされます):

>run -p6 -v ..\teamprimes
PARLANSE RTS: Version 19.1.53
# Processors = 6
Number of primes found: 664579
Parallel computed in 2.443123 seconds
---- PARLANSE RTS: Performance Statistics
  Duration = 2.538972 seconds.
  CPU Time Statistics:
Kernel CPU Time: 0.000s
User   CPU Time: 14.102s
Total  CPU Time: 14.102s
  Memory Statistics:
Peak Memory Usage    : 4.28 MBytes
  Steals: 459817  Attempts: 487334  Success rate: 94.4%  Work Rediscovered: 153

並列バージョンの合計 CPU 時間は、シリアル バージョンとほぼ同じであることに注意してください。これは、彼らが同じ仕事をしているからです。

Python の "fork" および "join" 操作を考えると、簡単にコーディングできる同等の Python があると確信しています。同時にフォークが多すぎる可能性があるため、スペースまたはスレッドが不足する可能性があります。( candidates_per_segment10 では、Parlanse の下で最大 100 万個のライブ グレインが実行されます)。これは、作業の生成を自動的に調整することが良いことです。代わりにcandidates_per_segment、たとえば 10000 など、より大きな数を設定することもできます。これは、最悪の場合でも 1000 スレッドしか得られないことを意味します。(Python の解釈的な性質のために、依然として高い代償を払うことになると思います)。セグメントごとの候補を 1e7/4 に近づけるにつれて、現在の Python コードでの正確な動作に近づきます。

于 2014-10-27T06:08:32.917 に答える
1

CPU のコア/スレッドの数以上の並列処理は得られません。4 コアのマシンで 3 倍の速度が得られれば、それはかなり良いことです。わずかなオーバーヘッドしかありません。4 コアのマシンでは、「並列プロセスの数」を 4 に設定してオーバーヘッドを削減することをお勧めします。これを 16 コアのマシンで実行している場合、わずか 3 倍の速度向上は低いように見えます。特にスレッド(プロセス?)の実行方法については、Python マルチプロセッシング ライブラリを参照してください。

での結果はどうでしたnumProc == 4か?

ここではアムダールの法則が適用されますが、作業が整数範囲に均等に並列化されるため、並列プログラムの非常に小さな部分 (基本的には主要部分) のみが順次的になります。

于 2014-10-24T20:39:58.160 に答える