Sortアルゴリズムの消費メモリについて

Sortアルゴリズムの消費メモリについて(続) - Tech random memoranda

Sortアルゴリズムを自分で書いてみるとそれぞれの特性が良く分かってくる。メモリを最も消費しないのは、Bubbleソートのようにソート対象の配列を直接触っていけるような、時間がかかるが単純なアルゴリズムの場合。

ただ、そのようなアルゴリズムを優先すべきアプリケーションはなかな思いつかない(配列が小さければ消費メモリを気にすることはないだろうし、消費メモリを気にすることがないなら計算量を考慮するから)。組み込み系で、消費メモリを小さくすることが最優先されるような場合は、必要になるかもしれない。

では、QuickソートとMergeソート、どのくらいメモリを使うのだろうか。前回の安直なプログラムと、与えられたソート対象の配列のメモリ領域をなるべく直接変更しながらメモリ量を抑えるプログラム、を書いて比較。 mallocをラップして、確保したメモリ量の総和と、mallocを呼んだ回数を、それぞれのアルゴリズムでカウントして比較する。

mallocのラップ関数は以下。

unsigned long mem_size;
unsigned int malloc_cnt;

void* _malloc(size_t size){
    mem_size += size;
    malloc_cnt ++; 
    return malloc(size);
}

Macで実行した結果がこちら。

$ ./three_sorts 10000000
Quick sort : 8.67 sec.
Sorted check OK.
mem_size = 4024MB, malloc_cnt = 30000000
Merge sort : 7.25 sec.
Sorted check OK.
mem_size = 2669MB, malloc_cnt = 29999997

$ ./two_sorts_low_memory 10000000
Quick sort : 6.56 sec.
Sorted check OK.
mem_size = 1563MB, malloc_cnt = 10000000
Merge sort : 3.91 sec.
Sorted check OK.
mem_size = 889MB, malloc_cnt = 9999999

メモリ割り当て(mallocを呼ぶ回数)が少なくなるとその分速くなる。メモリ割り当てにはそれ相当の時間がかかるからだ。

Mergeソートの場合だと、malloc_cntが一回少ないので、その分メモリ量が抑えられている。 良くわからないが、配列が非常に大きいので、その一回が使用するメモリ量を大きく引き離す結果になっているのだろうと予測。結局この一回が、速度差に影響している気がしてならないが。

→ Quick sortにおいて、ソート対象の配列数が1の場合、関数を抜けるべきところを実行してしまっているため、malloc_cntが多くなってしまっている。

ソート対象のサイズが100,000,000でも今回はMergeソートの方が遅くなる現象は発生していない。

$ ./two_sorts_low_memory 100000000
Quick sort : 77.14 sec.
Sorted check OK.
mem_size = 17042MB, malloc_cnt = 100000000
Merge sort : 42.81 sec.
Sorted check OK.
mem_size = 10169MB, malloc_cnt = 99999999

実装はこちら Optimized for memory usage · GitHub

Sortアルゴリズムについて

Bubbleソート、Quickソート、Mergeソート、をpythonとCで実装してみた。何年ぶりだろうか。なお、pythonはバグがあるので、Cを参照のこと。

Quickソートさえ分かっていればよくて、qsortとか標準ライブラリを使えば良いと思っていたけれど、Mergeソートに負けてしまった。 最悪の計算量でいうと、QuickソートはO(n2)になってしまうが、MergeソートはO(n log n)で済む。 ソート対象の配列が非常に大きい場合は、リスクを回避という観点で言えば、QuickソートよりMergeソートの方が良さそう。あとは、どのくらいソート対象の配列にランダム性があるのかどうか、という点も考える必要があるかもしれない。 pythonの方では、指定した数の倍のシーケンシャルなリストを作った後で、指定した数だけサンプリングし、その順序をランダマイズしたリストをソート対象としている。Cの方では、重複した値をテストするために、データをサンプリングする過程で100回に1回データの重複が発生するようにしている。

Cで試したところ、mergeソートの方がquickソートよりも常に速いが、あるデータサイズを超えると、quickソートの方が速くなる。というか、mergeソートがかなり遅くなる。これはおそらく、mergeソートの実装の方がメモリを消費するので、先に上限に達してしまうからだろう。mergeソートは必ず全ての要素を最小単位まで分割するため、quickソートよりもメモリの消費量は多い。(実装でうまく回避できるのかもしれないが)

ちなみに、メモリ量を最も抑えられるのはBubbleソートですね。必要なメモリは、ソート対象の配列と一つの変数だけ。スピードは非常に遅い(平均の計算量がO(n2))けれど使用可能なメモリ量に厳しい制限があり、かつ、ソート対象の配列のサイズが小さいことが分かっていれば、Bubbleソートが解になりそうです。

  • Bubbleソート
import sys, random

i = int(sys.argv[1])
arr = list(range(i*2))
random.sample(arr,i)
rand_arr = list(arr)
random.shuffle(rand_arr)

def bs(arr):
    i=len(arr)
    while i>0:
        for j in range(0,i-1):
            if arr[j] > arr[j+1]:
                arr[j],arr[j+1] = arr[j+1],arr[j]
        else:
            i -= 1
    return arr

sorted_arr = bs(rand_arr)
assert sorted_arr == arr

if not sorted_arr == arr:
    print(rand_arr)
    print(sorted_arr)
  • Quickソート
import sys,random

#arr = list(map(lambda x: int(x), sys.argv[1:]))

i = int(sys.argv[1])
arr = list(range(i*2))
random.sample(arr,i)
rand_arr = list(arr)
random.shuffle(rand_arr)

def qs(arr):
    #print(arr)

    if len(arr) == 1:
        return arr

    med = arr[0]
    #med = arr[int(len(arr)/2)]
    sg = list(filter(lambda x: x<med, arr))
    lg = list(filter(lambda x: x>med, arr)) 

    if len(sg) == 0:
        return [med]+qs(lg) 
    if len(lg) == 0:
        return qs(sg)+[med]

    return qs(sg)+[med]+qs(lg) 

sorted_arr = qs(rand_arr)
assert sorted_arr==arr

if not sorted_arr == arr:
    print(rand_arr)
    print(sorted_arr)

  • Mergeソート
import sys, random

i = int(sys.argv[1])
arr = list(range(i*2))
random.sample(arr,i)
random_arr = list(arr)
random.shuffle(random_arr)

#print(arr)
#print(random_arr)

def bs(arr):
    size = len(arr)
    if size == 1:
        return arr
    else:
        half = int(size/2)
        left_arr = bs(arr[:half])
        right_arr = bs(arr[half:])

        merged_arr = []
        left_idx = right_idx = 0
        while left_idx<half or right_idx<size-half:
            if left_idx == half:
                merged_arr.append(right_arr[right_idx])
                right_idx += 1
            elif right_idx == size-half:
                merged_arr.append(left_arr[left_idx])
                left_idx += 1
            elif left_arr[left_idx] < right_arr[right_idx]:
                merged_arr.append(left_arr[left_idx])
                left_idx += 1
            elif left_arr[left_idx] >= right_arr[right_idx]:
                merged_arr.append(right_arr[right_idx])
                right_idx += 1

        return merged_arr

sorted_arr = bs(random_arr)
#print(sorted_arr)

assert sorted_arr == arr

if not sorted_arr == arr:
    print(arr)
    print(sorted_arr)
$ time python bubblesort.py 5000

real    0m10.906s
user    0m10.831s
sys 0m0.053s

$ for i in $(seq 1 3); do time python mergesort.py 1000000; done

real    0m26.053s
user    0m24.332s
sys 0m0.363s

real    0m26.276s
user    0m25.174s
sys 0m0.375s

real    0m23.590s
user    0m23.303s
sys 0m0.220s

$ for i in $(seq 1 3); do time python quicksort.py 1000000; done

real    0m28.814s
user    0m28.425s
sys 0m0.341s

real    0m26.910s
user    0m26.567s
sys 0m0.311s

real    0m28.079s
user    0m27.733s
sys 0m0.325s
  • C実装

Run sort algorithms with randomized single data · GitHub

  • C実装のMacでの実行結果
$ ./sort 10000000
Quick sort : 13.00 sec.
Sorted check OK.
Merge sort : 9.92 sec.
Sorted check OK.

$ ./sort 20000000
Quick sort : 21.84 sec.
Sorted check OK.
Merge sort : 16.55 sec.
Sorted check OK.

$ ./sort 50000000
Quick sort : 78.04 sec.
Sorted check OK.
Merge sort : 201.94 sec.
Sorted check OK.

Rustの文字列とコピーのlifetimeについて

Rustで簡単なマルチスレッドプログラムを書いているとき、ふとコンパイルが通らなくなって困ったのでいろいろと調べていて、gitterできいて解決したことを書き留めておく。

標準入力から入力された文字列を取得して関数に渡し、その中でチャネルを通してスレッドに送り、スレッド側で標準出力にその文字列を出力する。 指定された回数分だけそのスレッドが文字列を受け取ったら、メインスレッド側にまた別のチャネルを使って終わったことを知らせる。 sync_channelはブロックするので、このように別のチャネルをスレッドジョインの役目として使うこともできる。 以下が動くバージョンのプログラム。

use std::thread;
use std::sync::mpsc;
use std::io;

fn call<'a>(txt:&'a String, tx1: &mpsc::SyncSender<String>){
    tx1.send(txt.clone()).unwrap();
}

fn main() {
    let mut txt = String::new();
    let _ = io::stdin().read_line(&mut txt);

    let (tx1,rx1) = mpsc::sync_channel(1);
    let (tx2,rx2) = mpsc::sync_channel(1);

    thread::spawn(move||{
        for _ in 0..2 {
            let rtext = rx1.recv().unwrap();
            println!("{}",&rtext);
        }   
        tx2.send(()).unwrap();
    }); 

    call(&txt,&tx1);

    txt = "xyz".to_string();

    call(&txt,&tx1);

    rx2.recv().unwrap();
}

この場合、txtはmainで複数回使われているので、callに対して所有権を渡すことはできず、callがそれを借用することになる。ここで、callの引数となるtxtを&Stringにすべきか&strにすべきかが良く分からなかった。文字列リテラル&strで、to_string()メソッドでStringに変換できるし、&をつけることで&strにもできる。ただしこのプログラムでよく分かる明らかな違いは、strCloneを実装していないということで、callの引数となるtxtはおいそれと&strにはできないはず、ということ。チャネルの先はメインスレッドよりも長生きするかもしれないスレッドのため、txtのポインタを送ることはできないので、ここでCloneは必須。

例えば以下のようにすると、

fn call<'a>(txt:&'a str, tx1: &mpsc::SyncSender<&str>){
    tx1.send(txt.clone()).unwrap();                                                                                                             
}  

以下のようなコンパイルエラーになる。

src/main.rs:6:18: 6:25 error: cannot infer an appropriate lifetime for lifetime parameter `'a` due to conflicting requirements
src/main.rs:6     tx1.send(txt.clone()).unwrap();
                               ^~~~~~~

これは、callの第2引数の型に'aがついていないので第1引数のtxtとはlifetimeが異なるはずだが、callが借用したtxtの参照先をチャネルで渡そうとしているので要求が矛盾している、と言われていることになる。txtに対してclone()を呼んでいるので、意図としては借用したtxtの値をコピーしたものを渡しているつもりだが、これはtxtが&strであることにより、値ではなくポインタをコピーしていることになってしまう。strCloneを持っていないが、ポインタはそうではないということらしい。

なお、ここのコンパイルエラーでは以下のようにも言われてしまうが、

consider using an explicit lifetime parameter as shown: fn call<'a>(txt: &'a str, tx1: &mpsc::SyncSender<&'a str>)

これに従ったからといって解決しない。結局は、

`txt` does not live long enough

となり、上記で説明した通りスレッドの方がメインスレッドよりも長生きするかもしれないため、コンパイルは通らない。

ブログがFlickrの写真貼り付けに対応した

staff.hatenablog.com

ということでFlickr Proで管理していた時代からだいぶ時間が経ってしまっていた写真を載せてみた。ユーザ選択も写真マルチ選択も簡単で、楽しい。

Touring on the back

photo by senbonzakura08

A young man walking into the classic studio

photo by senbonzakura08

Narrow street between shops made by blocks

photo by senbonzakura08

A french restaurant for adults

photo by senbonzakura08

An unnoticeable sign for the popular avenue

photo by senbonzakura08

One noon in the court of great palace

photo by senbonzakura08

A classic station without lights at dusk

photo by senbonzakura08

Overlooking similar houses in a town

photo by senbonzakura08

Square shape houses and a deep pink car

photo by senbonzakura08

A big church across the river over a cup of cappuccino

photo by senbonzakura08

Great shrine gate covered by lots of green

photo by senbonzakura08

A bronze statue of the famous samurai

photo by senbonzakura08

A tower and the city in black and white

photo by senbonzakura08

A big apple hanged in the air

photo by senbonzakura08

Bronze lanterns hanged along the edge of a roof

photo by senbonzakura08

The large stage area in a festival under the bad weather

photo by senbonzakura08

Go for a ride

photo by senbonzakura08

A high building behind the dead gingko tree

photo by senbonzakura08

The park with circles on the ground

photo by senbonzakura08

A tower in the distance

photo by senbonzakura08

Google Code Jam 2016 Qualification Round レビュー in Haskell

今年もスケジュールが合わず参戦できなかったので後日レビュー実施。Pythonで解いた後にHaskellでやってみた。ただしHaskellで解いてみるだけでは今までと変わらない気がするので、do構文とArrayは使用しない、という制約を付けた。ただ、HaskellらしくList(無限リスト)を多用するように心がけてはいるものの、値を持ち回さないで状態系モナドを活用するという姿勢で解けるまでには至っていない。

  • Problem A. Counting Sheep

与えられた数字をNとすると、[N, N*2, N*3, ...] の無限リストを作成し、そのリストの要素を一つずつ集めたリストを準備する。つまり、[ [N], [N, N*2], [N, N*2, N*3], ...] というリストを作って、Unique . Sort で結果が"0123456789"になれば、そのリストの最後の要素を表示して終了。結果がそうならない場合に備えて実はその無限リストを1000に絞っている。

gist.github.com

  • Problem B. Revenge of the Pancakes

先頭のパンケーキと別の面のPancakeが現れたところで、同じ面のPancakeをひっくり返す。takeWhileとdropWhileを組み合わせて、Pancakeをひっくり返すリストと、そのままのリストを作り出し、それを全てのPancakeが同じ面になるまで繰り返す。全てが+ならそれで終了、-なら最後に1回加えて終了。

gist.github.com

  • Problem C. Coin Jam (Only for small)

1で始まり1で終わるnbitのcoinの無限リストを作って、Jamcoinの条件でfilterし、先頭からj個取り出す。出力に必要なNontrivial Divisorはfilterの条件式中に現れるが、出力として取り出せないので、Jamcoinを取り出した後、それを元に改めて作り出している。サンクのキャッシュが効けばそれほど非効率でも無いかなという淡い期待はあるが自信はない。これは、Python版でもLargeが解けないでいるが、Haskell版ではtakeの引数の型がIntのため、桁数制限無しのIntegerを一括して諦めているので、その時点でおそらくLargeは無理だろう。

gist.github.com

IPフラグメントされたTCPの接続テストを行う環境をNetwork Namespaceで構築する

TCP接続の際にはIPフラグメントが発生しないように、通常はPath MTU DiscoveryによってEnd-To-Endのパス中で最小のMTUを認識して、そのサイズに収まるようにパケットを送出する。これはTCPのデータ転送を効率良く行うための最適化の結果であり、インターネットのトラフィックにおいてTCPのIPフラグメントが発生しない、ということを意味しているわけではない。TCPのオフロード機能を持つNICをテストする場合など、テストが必要になるケースもあるだろう。

手順としては、パケット送信元のホストでPath MTU Discoveryを無効にして、送信先までのパスでインターフェースのMTUを小さく設定しておきルーティングを行う、という流れになる。端末を複数台用いることができれば上記手順通りに環境構築を行うことは簡単だが、ここは仮想環境とNetwork Namespaceを用いて最小の端末数で環境構築を行ってみる。ネットワークの概要は以下のようになる。パケット送出元となる仮想環境はVirtual Box上のLinuxを使う。

+-----------------------------------------+
|host1                                    |
|+---------------------------------------+|
|| guest1(fedora on virtual box)         ||
|| +---------------+                     ||
|| |ns1            |       192.168.100.99||
|| |      veth-ns12|===veth-ns21 <=> p1p2|===+ <---(Bridged)
|| |   192.168.1.10|   192.168.1.20      ||  |
|| +---------------+                     ||  |
|+---------------------------------------+|  |
+-----------------------------------------+  |
                         +---------------+   |
                         |host2          |   |
                         |192.168.100.101|===+
                         +---------------+

===は直結、<=>はルーティングを示し、Virtual Boxのネットワーク設定はブリッジとする。NATの場合は、ns1でPath MTU Discoveryを無効化してTCPのフラグメント(IPのフラグメントビット)をクリアしていても、NAT変換される際に該当のビットはセットされてしまうため、今回の目的としては問題がある。*1

Linux上でNetwork Namespace(ns1)を以下のように設定する。ここではLinuxとしてFedora 20(Linux Kernel 3.19/iproute 3.14.0-2)を使っている。

sudo ip netns add ns1
sudo ip link add veth-ns12 type veth peer name veth-ns21
sudo ip link set veth-ns12 netns ns1
sudo ip netns exec ns1 ifconfig veth-ns12 192.168.1.10/24
sudo ip netns exec ns1 route add default gw 192.168.1.20
sudo ip netns exec ns1 sysctl -w net.ipv4.ip_no_pmtu_disc=1

sudo sysctl -w net.ipv4.ip_forward=1
sudo ifconfig veth-ns21 192.168.1.20/24
sudo ifconfig p2p1 192.168.100.99/24
sudo ifconfig p2p1 mtu 1000

sudo iptables -F

ここで、host2からns1に接続するためには、host2にns1のネットワーク(192.168.1.10/24)宛のルーティングを設定するか、guest1上でProxy Arpを有効にする。それによって、guest1はhost2からのARP RequestをProxyしてns1に渡す役目をしてくれる。

sudo sysctl -w net.ipv4.conf.p2p1.proxy_arp=1

ns1からhost2にpingが通ることを確認したら、iPerfでTCPパケットを送り、パケットをキャプチャしてみると、Don't fragmentビットが落とされ、TCPのパケットがIPフラグメントされていることが分かる。

f:id:nishidy:20160427004724p:plain

あとはLinux上でtc/cbqなどを使って好きに帯域制御を行うことで、ある程度実ネットワーク模擬をすることもできる。 なお、Ubuntu 14.04 LTS(Linux Kernel 3.13/iproute2 3.12.0-2)では、作成したNamespace内に仮想ファイル/proc/net/ipv4/ip_no_pmtu_discが無かった。

*1:もしかするとhost1側のPath MTU Discoveryの設定を無効化すればセットされないのかもしれない