深イイ意味など全くない並列処理 in Python


あるサイトに載っていたコードパズルを解く際に,全く解法が思いつかず,禁断の総当たりに手を染めることにした.

対象となる問題は,与えられた関数f,定数cに対してf(m) == cとなるmを見つけるというもの.

# まずはシンプルに

ひとまず何も考えずに実装した結果が以下のコードになる.

```python:solution1
import sys

def solve(m):
    # f, cは定義済みとする
    return f(m) == c

def solution1():
    limit = int(sys.argv[1])
    ans = [m for m in range(limit) if solve(m)]
    print ans
```

処理時間がどれくらいかかるかな,と思い,プロファイラを仕込んだ.

```python:profile
import cProfile

if __name__ == '__main__':
    profiler = cProfile.Profile()
    profiler.runcall(solution1)
    profiler.print_stats()
```

で,実行.

```bash:solution1_profile_result
% python mp.py 100000000

(解は省略)

200000003 function calls in 333.009 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
100000000   78.739    0.000  299.934    0.000 mp.py:12(solve)
        1   30.856   30.856  333.009  333.009 mp.py:31(solution1)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
100000000  221.195    0.000  221.195    0.000 {pow}
        1    2.219    2.219    2.219    2.219 {range}
```

# 煽り,並列,そして...

とりあえず答えは分かったかと一息ついていると,「なんとwww並列化しないでござるかwwwブフェフェwww」という声が横から聞こえてきた(注:発言は筆者の悪意により歪められています). 
「そういえばPythonの並列処理は調べたことがないな」と思いつつ,並列化したバージョンを実装してみる.

```python:solution2
import math
import sys
from multiprocessing import Process, Queue


def task(q, num_range):
    # キュー経由でデータをmain側に渡すことに,このプログラムでは本質的な意味はない.
    # ﾔﾘﾀｶｯﾀﾀﾞｹｰ
    q.put([m for m in num_range if solve(m)])


def solution2():
    queue = Queue()
    limit = int(sys.argv[1])
    worker_num = int(sys.argv[2])
    task_size = int(math.ceil(float(limit) / float(worker_num)))

    # 処理するリストを分割して,各プロセスに渡す.
    # ref. http://iogi.hatenablog.com/entry/split-list-into-sublist
    num_ranges = izip_longest(*[iter(range(limit))]*task_size)
    processes = [Process(group=None, target=task, args=(queue, num_range))
                 for num_range in num_ranges]

    for process in processes:
        process.start()

    for process in processes:
        print queue.get()
        process.join()
```

で,実行. 処理時間は半分近くになっている.

```bash:solution2_profile_result
% python mp.py 100000000 4

(解は省略)

1021 function calls (1018 primitive calls) in 182.498 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.000    0.000 __future__.py:48(<module>)
        1    0.000    0.000    0.000    0.000 __future__.py:74(_Feature)
        7    0.000    0.000    0.000    0.000 __future__.py:75(__init__)
        1    0.005    0.005    0.042    0.042 __init__.py:102(Pipe)
        1    0.004    0.004    0.084    0.084 __init__.py:213(Queue)
        1    0.000    0.000    0.000    0.000 connection.py:117(Listener)
        1    0.000    0.000    0.000    0.000 connection.py:183(Pipe)
        1    0.000    0.000    0.000    0.000 connection.py:246(SocketListener)
        1    0.013    0.013    0.037    0.037 connection.py:35(<module>)
        1    0.000    0.000    0.000    0.000 connection.py:426(ConnectionWrapper)
        1    0.000    0.000    0.000    0.000 connection.py:448(XmlListener)
        1    0.000    0.000    0.000    0.000 forking.py:113(Popen)
        4    0.000    0.000    0.027    0.007 forking.py:115(__init__)
       (略)
```

# 進撃のメモリ

さきほどの実装は,リストの分割処理の際,range関数を使ってlistを生成している.
そのため,メモリ使用量がえらい事になる.

![solution2.png](https://qiita-image-store.s3.amazonaws.com/0/1835/8540b65d-2e46-775e-8188-fc4adafcb612.png "solution2.png")


range関数ではなく,xrange関数を用いることで,この問題は解決できた.
xrange関数はリストではなくiteratorを返すため,メモリを一度に消費しない.

```python:solution3
import math
import sys
from multiprocessing import Process, Queue


def solution3():
    queue = Queue()
    limit = int(sys.argv[1])
    worker_num = int(sys.argv[2])
    task_size = int(math.ceil(float(limit) / float(worker_num)))

    gen = xrange(limit)
    processes = [Process(
                 group=None,
                 target=task,
                 args=(queue, islice(gen, task_size * i, task_size * (i + 1))))
                 for i in xrange(worker_num)]

    for process in processes:
        process.start()

    for process in processes:
        print queue.get()
        process.join()
```

で,実行 + その際のメモリ消費量. 実行時間は殆ど変わっていないが,劇的にメモリ消費量が減った

```bash:solution3_profile_result
% python mp.py 100000000 4

(解は省略)

1019 function calls (1016 primitive calls) in 187.432 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.000    0.000 __future__.py:48(<module>)
        1    0.000    0.000    0.000    0.000 __future__.py:74(_Feature)
        7    0.000    0.000    0.000    0.000 __future__.py:75(__init__)
        1    0.006    0.006    0.047    0.047 __init__.py:102(Pipe)
        1    0.005    0.005    0.082    0.082 __init__.py:213(Queue)
        1    0.000    0.000    0.000    0.000 connection.py:117(Listener)
        1    0.000    0.000    0.000    0.000 connection.py:183(Pipe)
        1    0.000    0.000    0.000    0.000 connection.py:246(SocketListener)
        1    0.015    0.015    0.041    0.041 connection.py:35(<module>)
        1    0.000    0.000    0.000    0.000 connection.py:426(ConnectionWrapper)
        1    0.000    0.000    0.000    0.000 connection.py:448(XmlListener)
        1    0.000    0.000    0.000    0.000 forking.py:113(Popen)
        4    0.000    0.000    0.001    0.000 forking.py:115(__init__)
       10    0.000    0.000    0.000    0.000 forking.py:130(poll)
        4    0.000    0.000    0.000    0.000 forking.py:146(wait)
        1    0.014    0.014    0.019    0.019 forking.py:35(<module>)
       (略)
```

![solution3.png](https://qiita-image-store.s3.amazonaws.com/0/1835/a5419b9b-526f-8fb5-85bb-d63e794fa586.png "solution3.png")

## (補足)Python2/3でのrange関数の違い

Python3では,range関数はrangeクラスというイテレータを返すため,上記の対応は不要.
というより, xrange関数が廃止されている.

```
// Python3
% python3
  Python 3.3.0 (default, Mar  2 2013, 11:44:00)
  [GCC 4.2.1 Compatible Apple LLVM 4.2 (clang-425.0.24)] on darwin
  Type "help", "copyright", "credits" or "license" for more information.
  >>> a = range(5)
  >>> a
  range(0, 5)
  >>> type(a)
  <class 'range'>
  >>>
  >>> xrange(5)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    NameError: name 'xrange' is not defined

// Python2
% python
  Python 2.7.3 (default, May 14 2012, 22:41:33)
  [GCC 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.1.00)] on darwin
  Type "help", "copyright", "credits" or "license" for more information.
  >>> a = range(5)
  >>> a
  [0, 1, 2, 3, 4]
  >>> type(a)
  <type 'list'>
  >>>
```