Dell XPS 13 (9370) に Ubuntu 16.04 をインストールした

Dell XPS 13 (9370) の全部入り( i7-8550U・4Kタッチパネル・1TB SSD ・16 GBメモリ) を購入した。

これまでのところ、思ったより使い勝手がよい。キーボードはいい感じだし、パームレストのカーボンファイバも触り心地が良い。

液晶は非光沢がなくて光沢のみということでターミナルの背景におじさんが出現する不具合が怖かったが、あまり気にならない。手元のMacBook Airと比べても遜色ない気がする。

Linux環境でのタッチパッドも、設定でかなりまともに使えるようになった。 ← 後述のとおり、16.04ではダメだったので17.10に切り替えた

以下作業メモ

事前準備

  • どっちにしろWindows 10 Proをインストールするので、プリインストールのWindows 10 Homeは潔くパーティションごと削除。

  • UbuntuWindowsのインストール用USBを作成する。 XPS 13 (9370) では、Type-AのUSBメモリがいくつあっても無意味。Type-C - Type-A の変換アダプタを用意する。

  • Ubuntu 16.04もWindows 10も、インストールメディアで立ち上げた状態ではWiFiBluetoothが使えないので、USB Type-C で使えるテザリングかLanアダプタを用意する。

Ubuntuのインストール

XPS 13を再起動し、Dellのロゴが表示される前にF2キーを連打し、BIOS設定画面を表示する。

BIOS画面で、

  1. SATA-controllerをRaid -> AHCI とする。
  2. SecureBoot をオフにする

UbuntuUSBメモリを挿し、再起動する。Dellのロゴが表示される前にF12キーを押し、ブートデバイスにUSBを選択する。

Ubuntu 16.04インストールメディアに含まれているドライバでは、XPS 13のBluethoothやWiFi、画面のスケール設定などが動作しない。

とりあえず 16.04 をインストールし、LanアダプタやスマホのUSBテザリングなどを使って sudo apt update; sudo apt upgrade して再起動すれば正常に動作する。

libinput

[追記] 以下の手順で16.04にlibinputをインストールすると、ログインーログアウトを繰り返すとハングする模様。。。

デフォルトの状態ではタッチパッドpalm detectionまじ使えないので、次のページを参考に libinput をインストールする。

askubuntu.com

$ sudo apt install xserver-xorg-input-libinput

/usr/share/X11/xorg.conf.d/40-libinput.conf

# Match on all types of devices but tablet devices and joysticks
Section "InputClass"
    Identifier "libinput pointer catchall"
    MatchIsPointer "on"
    MatchDevicePath "/dev/input/event*"
    Driver "libinput"
EndSection


Section "InputClass"
    Identifier "libinput keyboard catchall"
    MatchIsKeyboard "on"
    MatchDevicePath "/dev/input/event*"
    Driver "libinput"
EndSection

Section "InputClass"
    Identifier "libinput touchpad catchall"
    MatchIsTouchpad "on"
    MatchDevicePath "/dev/input/event*"
    Driver "libinput"
    Option "Tapping" "True"
    Option "NaturalScrolling" "True"
EndSection

Section "InputClass"
    Identifier "libinput touchscreen catchall"
    MatchIsTouchscreen "on"
    MatchDevicePath "/dev/input/event*"
    Driver "libinput"
EndSection

Section "InputClass"
    Identifier "libinput tablet catchall"
    MatchIsTablet "on"
    MatchDevicePath "/dev/input/event*"
    Driver "libinput"
EndSection

この状態だとタッチパッドの設定をGUIで行えないので、次のページを参考に設定を行う。

github.com

.profile

touchpad_id=$(xinput --list | grep -i "Synaptics Touchpad" | xargs -n 1 | grep "id=" | sed 's/id=//g')

# touchpad_id='13'

natural_scrolling_code=$(xinput --list-props "$touchpad_id" | grep "Natural Scrolling" | awk '{print $5}' |  grep -o '[0-9]\+')
xinput --set-prop "$touchpad_id" "$natural_scrolling_code" 0

tap_to_click_code=$(xinput --list-props "$touchpad_id" | awk '/Tapping Enabled \(/ {print $4}' | grep -o '[0-9]\+')
xinput --set-prop "$touchpad_id" "$tap_to_click_code" 1

accel_speed_code=$(xinput --list-props "$touchpad_id" | awk '/Accel Speed \(/ {print $4}' | grep -o '[0-9]\+')
xinput --set-prop "$touchpad_id" "$accel_speed_code" "1.0"   # -1.0 ~ 1.0

disable_while_typing_code=$(xinput --list-props "$touchpad_id" | grep "Disable While Typing Enabled (" | awk '{print $6}' |  grep -o '[0-9]\+')
xinput --set-prop "$touchpad_id" "$disable_while_typing_code" 1

Windowsのインストール

Windows 10 Fall Creators Updateのインストールメディアでは、WiFi/Bluetooth が認識できない。

http://www.dell.com/support/home/jp/ja/jpbsd1/products/laptop/xps_laptop?app=drivers を参照し、Dell Mobile Connect Driver をインストールするとWifiに接続できる。

リブートすると、デフォルトでWindowsがブートしてしまうので、Ubuntugrubでブートする場合は、UbuntuBoot-repair をインストールして設定を変更する。

https://askubuntu.com/questions/666631/how-can-i-dual-boot-windows-10-and-ubuntu-on-a-uefi-hp-notebook

hashlibのハッシュアルゴリズム、計算速度ってどのぐらい差があるの選手権

雑に図ってみた (Python 3.6.4)

対象ファイル: 画像ファイル約20000件(2GB弱)

アルゴリズム 処理時間(秒) ファイル読み込み時間は含まず
md5 2.536
sha1 1.798
sha256 3.873
sha384 2.591
sha512 2.626

まとめ

sha1はええ。md5そんな速くねえ。sha512とそんな変わんねえ。

コード

import hashlib, sys, pathlib, time

sizes = []
times = []

HASH = gatattr(hashlib, sys.argv[1])

def h(b):
    f = time.time()
    h = HASH(b).digest()
    times.append(time.time()-f)
    sizes.append(len(b))

imgs = pathlib.Path(sys.argv[2]).glob("*.jpg")
for i in imgs:
    h(i.read_bytes())

print(len(sizes), sum(sizes), sum(times), sum(times)/len(times))

Python/CFFIでCUDAしてみる

ふと思い立って、CUDAでCPUからGPUにデータを転送する時の速度を測ってみた。

普通にCUDA SDKのサンプルで測定しても良いが、PythonCFFI で実行してみよう。

コードはこんな感じで書ける。

import time
import sys
from cffi import FFI
ffi=FFI()

ffi.cdef(r"""
typedef unsigned int cudaError_t;
cudaError_t cudaGetLastError();
char* cudaGetErrorString(cudaError_t error);

cudaError_t cudaDeviceSynchronize();
cudaError_t cudaMalloc(void **p, size_t s);
cudaError_t cudaFree(void *p);
cudaError_t cudaMallocHost(void **p, size_t s);
cudaError_t cudaFreeHost(void *p);
cudaError_t cudaMemcpy(void *dst, const void *src, size_t count, unsigned int kind);

""")

cudaMemcpyHostToHost          =   0
cudaMemcpyHostToDevice        =   1
cudaMemcpyDeviceToHost        =   2
cudaMemcpyDeviceToDevice      =   3
cudaMemcpyDefault             =   4

cuda = ffi.dlopen("/usr/local/cuda/lib64/libcudart.so")

def check():
    err = cuda.cudaGetLastError()
    if err:
        print(ffi.string(cuda.cudaGetErrorString(ret)))
    assert err == 0

def run(host, dev, n, size):
    for i in range(n):
        ret = cuda.cudaMemcpy(dev[0], host[0], size, cudaMemcpyHostToDevice)
        check()

size = int(sys.argv[1])

pinned = ffi.new("void **")
cuda.cudaMallocHost(pinned, size)
check()

ffi.memmove(pinned[0], b'a'*size, size)

dev = ffi.new("void **")
cuda.cudaMalloc(dev, size)
check()

run(pinned, dev, 1, 1) # warm up

buf = ffi.new("char []", b'a'*size)
host = ffi.new("char **", buf)

N = 1000
for s in range(1, size, size//20):
    f = time.time()
    run(pinned, dev, N, s)
    p = time.time()-f

    f = time.time()
    run(host, dev, N, s)
    n= time.time()-f

    print(f'{s}, {p:0.5f}, {n:0.5f}')

AWSのp2.xlargeとp3.2xlargeで実行してみた。

p2.xlarge
Tesla K80
E5-2686 v4 @ 2.30GHz
p3.2xlarge   
Tesla V100-SXM2-16GB
E5-2686 v4 @ 2.30GHz

f:id:atsuoishimoto:20180130100853p:plain

ざっくり、Pinnedメモリのほうが15%くらい速い。また、転送速度に関してはp2.xlargeでもp3.2xlargeでも大差はないようだ。

ちなみに、手元にあったi7-6700 CPU @ 3.40GHz/GeForce GTX 1070 と E5-1650 v4 @ 3.60GHz/GeForce GTX 1080 Ti のデスクトップは、EC2のインスタンスより2割程度高速だった。

また、~100KBぐらいまでの転送量を見てみると、50KBぐらいまではPinnedメモリより普通のメモリのほうが速いようだ。

f:id:atsuoishimoto:20180130100921p:plain

これは、データの転送よりも、転送の前処理・後処理に時間がかかってしまっているのかもしれない。ストリームを使って並列化するなどすればまた違ってくるかもしれないので、あとで実験してみたい。

クラスブロックのひみつ

さて、 リスト内包のひみつ - atsuoishimoto's diary で、Python3では、リスト内包式は関数呼び出しとなることを説明した。

>>> a = [i*2 for i in range(3)]

というスクリプトは、次のように展開される。

>>> def _listcomp(_it):
...     ret = []
...     for i in it:
...         ret.append(i*2)
...     return ret
...
>>> _it = range(3)
>>> a = _listcomp(it)

通常、この点はあまり気にする必要はないが、問題となるケースもなくはない。

クラスブロックのリスト内包

クラスブロックで次の処理を実行してみよう。

class Foo:
    NUMS = [i*2 for i in range(3)]

まあ、これは当然動作する。Foo.NUMS の値は、[0,2,4] となる。

では、これをちょっと直してみよう。

class Foo:
    N = 3
    NUMS = [i*2 for i in range(N)]

これも問題ない。Foo.NUMS の値は、同じく [0,2,4] となる。

もうちょっと変えてみよう。

class Foo:
    N = 3
    F = 2
    NUMS = [i*F for i in range(N)]

一見、これも問題なさそうだが、実行するとエラーとなってしまう。

>>> class Foo:
...     N = 3
...     F = 2
...     NUMS = [i*F for i in range(N)]
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in Foo
  File "<stdin>", line 4, in <listcomp>
NameError: name 'F' is not defined

なんで N は良くて、 F はダメなのだろう。

Pythonの名前解決ルール

Pythonspam*2 のような式の値を計算する時、Pythonは変数 spam の値を、次の順序で検索する。

  1. 式のスコープ。関数内で定義された入れ子の関数で実行していれば、親関数のスコープも検索する。

  2. 式のモジュールのグローバル変数

  3. 組み込み関数のモジュール __builtins__

例えば、

def foo():
    return 2 * spam

という関数を実行すると、Pythonは変数 spam

  1. 式を実行している foo() のローカル変数。
  2. foo() が所属するモジュールのグローバル変数
  3. __builtins__

の順番で検索し、見つからなければ NameError 例外が発生する。

この名前の解決ルールは、Pythonプログラミングにおいて非常に重要なルールだ。それほど難しいルールではないので、確実に頭に叩き込んでおこう。

Pythonのプログラムを読んでいて、どこで定義されているかわからない変数や関数などがあっても、「ローカル変数かグローバル変数__builtins__ のいずれかに必ず存在する」 ということも覚えておくと良いだろう。

クラスブロック

Pythonの名前解決ルールを頭に叩き込んだら、次のコードを見てみよう。

class Foo:
    N = 3
    M = 2*N

当然ながら、Foo.M の値は 2*3=6 となる。2*N という式は N という変数を参照しているが、Pythonは同じブロックで定義されている変数 N を見つけ、値を取得する。

では、このコードをちょっと修正しよう。

class Foo:
    N = 3
    def f():
        return 2*N
    M = f()

このコードを実行すると、次のようなエラーが発生する。

>>> class Foo:
...     N = 3
...     def f():
...         return 2*N
...     M = f()
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in Foo
  File "<stdin>", line 4, in f
NameError: name 'N' is not defined

関数 f() 内で、変数 N が見つからないと言っている。

しかし、

N = 3
def f():
    return 2*N
M = f()

この処理は、どう見ても問題のないコードだ。実際、クラス定義以外で実行すると、正常に動作する。

次の例では、関数 f() の式 2*N は、グローバル変数 N を参照して値を評価する。

>>> N = 3
>>> def f():
...     return 2*N
...
>>> M = f()
>>> M
6

また、次の例では、入れ子の関数 f() は、親関数 bar() のローカル変数 N を参照する。

>>> def bar():
...     N = 3
...     def f():
...         return N*2
...     print(f())
...
>>> bar()
6

では、さきほどの例をもう一度見てみよう。

>>> class Foo:
...     N = 3
...     def f():
...         return 2*N
...     M = f()
...

Foo.f() は変数 N を参照しているが、この N はどこで見つかるだろうか?

関数 Foo.f() のローカル変数ではない。また、Foo.f()入れ子の関数ではないので、親となる関数も存在しない。

Foo.f() のモジュールにも、N は存在しない。

__builtins__ モジュールにも、当然 N は存在しない。

「でも、Foo.N があるじゃないか!」と言うかもしれない。しかし、それが何だというのか。

  • Foo.N は、Foo.f() のローカル変数ではなく
  • Foo.N は、Foo.f() のモジュールのグローバル変数ではなく
  • Foo.N は、__builtins__ モジュールの値でもない

つまり、Pythonの名前解決ルールのどの項目でも、Foo.f() では、 N という名前を解決することはできないのである。

リスト内包

さて、冒頭のコードをもう一度見てみよう。

class Foo:
    N = 3
    F = 2
    NUMS = [i*F for i in range(N)]

このコードは、次のように実行される。

class Foo:
    N = 3
    F = 2

    def _f(_iter):
        ret = []
        for i in _iter:
            ret.append(i*F)

    _iter = range(N)
    NUMS = _f(_iter)

リスト内包式が関数に変換されるのは、リスト内包式の [] 内のすべてではなく、in 以下のイテレータを指定する式は関数内では実行されない。

したがって、

class Foo:
    N = 3
    F = 2
    NUMS = [i*F for i in range(N)]

の、 range(N) の部分は、そのままクラスブロックで実行されるため NameError とはならない。

しかし、i*F の部分は関数内で実行されるため、クラス変数 F を参照できず、NameError とはなってしまうのである。

これはリスト内包だけではなく、ジェネレータ式や辞書内包、集合内包でも同様にエラーとなる。

対処方法

実のところ、クラスブロックに直接リスト内包を記述する場合は、うまい回避方法は思いつかない。

障害報告も出ているが、どうも治らなそうだ

リスト内包を避けて通常のループを使用するか、または

class Foo:
    N = 3
    F = 2
    def _init(N, F):
        return [i*F for i in range(N)]
    NUMS = _init(N, F)

のように、関数でラップするのが無難だろう。

まとめ

  • クラスブロックの中に定義した関数は、クラスブロックの変数を参照できない。
  • クラスブロック内のリスト内包などでクラス変数を参照すると、エラーとなる場合がある。ならない場合もある。
  • 対処方法: うーん。。。

リスト内包のひみつ

こちらのTweetが Python.jp slack でちょっと話題になっていた。

次のようなコードだ

>>> a = [lambda: print(i) for i in range(3)]
>>> for i in a: i()
2
2
2

結論としては cocoatomo さんの書かれているように、変数の評価タイミングの問題で、

対処としては、次のように、出力する値を、関数の実行時ではなく、関数の作成時に決定する必要がある。

>>> a = [lambda x=i: print(x) for i in range(3)]
>>> for i in a: i()
0
1
2

もともとの問題はこれで解決するのだが、このコード、単純だがPython2とPython3では動作が異なっている。

Pythonの動作を理解するのに良い教材だと思うので、ちょっと解説してみよう。

Python2の場合

Python2では、先程のコードは、次のようなループとして実行される。

>>> from __future__ import print_function
>>> a=[]
>>> for i in range(3):
...     f = lambda : print(i)
...     a.append(f)
...
>>> for i in a: i()
2
2
2

リスト内包式で使われているループ変数 i は、グローバル変数 i として、リスト内包式を終了した後も参照できる。

i の値は、必ず 2 となる。

>>> a = [lambda: print(i) for i in range(3)]
>>> i
2

この場合、lambda 関数の print(i) という式は、グローバル変数i を参照して、出力している。

したがって、 lambda 関数を実行する前に i の値を変更すると、出力も変化する。

>>> a = [lambda: print(i) for i in range(3)]
>>> i = 10000000
>>> for u in a:u()
...
10000000
10000000
10000000

Python3の場合

Python3では、リスト内包は次のように展開され、関数呼び出しとして実行される。

>>> _it = range(3)
>>> def _listcomp(_it):
...     ret = []
...     for i in it:
...         ret.append(lambda : print(i))
...     return ret
...
>>> a = _listcomp(_it)
>>> for i in a: i()
2
2
2

Python2と違って、Python3ではループ変数 i は外部からは参照できない。リスト内包は関数内で実行され、 iグローバル変数ではなくローカル変数となるためだ。

>>> a = [lambda: print(i) for i in range(3)]
>>> i
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'i' is not defined

Python2 では lambda 関数の print(i) は、グローバル変数 i を参照するが、Python3 では、lambda 式はローカル変数 i を参照するクロージャとなる。

クロージャとは、Pythonの関数のなかに別の関数を作成した時、親となる関数のローカル変数を子となる関数が参照する仕組みのことだ。

クロージャの仕組みは、以前

atsuoishimoto.hatenablog.com

に書いた。

したがって、Python2のように、後から i の値を変更することはできない。

>>> a = [lambda: print(i) for i in range(3)]
>>> i = 10000000
>>> for u in a:u()
...
2
2
2

Python2/3の違い

Python2 と Python3 では、リスト内包の実現方法に上記のような違いがある。これは、Python2 では

>>> i = 10000
>>> a = [lambda: print(i) for i in range(3)]
>>> print(i)
3

のように、リスト内包式があると他のローカル変数を上書きしてしまい、わかりにくいという問題を解決するためだ。

この経緯は

python-history-jp.blogspot.jp

でも解説されているので、参照されたい

Python文法詳解(二刷) 正誤表

以下の正誤表は、二刷のものです。一刷の正誤表は Python文法詳解(一刷) 正誤表 - atsuoishimoto's diary を参照してください。

第1章

P.1

>>> 1 + 1j  # 整数 + 複素数
(1+2j) # 結果は複素数となる
>>> 1 + 1j  # 整数 + 複素数
(1+1j) # 結果は複素数となる

第4章

P.118

>>> ' スパムハム '.find(' ハム ', 4)
-1
>>> ' スパムハム '.rfind(' ハム ', 4)
-1

検索失敗がエラーとなるよ うなケースでは index() を使い

検索失敗がエラーとなるよ うなケースでは rindex() を使い

第6章

P.201

スクリプトファイルやディレクトリを格納した zip ファイル

スクリプトファイルやディレクトリを格納した Zip ファイル

P.206

$ zip -r spam.zip __main__.py
$ zip  spam.zip __main__.py

P.210

図中

Func_spam関数

func_spam関数

P.240

第7章

x | ファイルを新しくファイルを作成し......

x+ | ファイルを新しくファイルを作成し......

x | 新しくファイルを作成し......

x+ | 新しくファイルを作成し......

Pythonのfor文は遅い?

bicycle1885.hatenablog.com

こちらの記事を拝見していて、ちょっと気になったので注釈。

PythonやRを使っている人で、ある程度重い計算をする人達には半ば常識になっていることとして、いわゆる「for文を使ってはいけない。ベクトル化*1しろ。」という助言があります。 これは、PythonやRのようなインタープリター方式の処理系をもつ言語では、極めてfor文が遅いため、C言語Fortranで実装されたベクトル化計算を使うほうが速いという意味です。

昔からよくこういう言い方がよくされるが、本当にPythonのfor文は遅いのだろうか。

聞くところによるとRのfor文はガチで遅いそうだが、Pythonの計算が遅いのはインタープリタ方式だからでも、for文が遅いからでもない。もちろん、Pythonインタープリタなので遅いし、for文だって極めて遅い。しかし、これはPythonの計算が遅い要因の一部でしかない。

まずは手元の環境(Macbook Air 2015, Python 3.6)で速度を測ってみよう。以下のコードはすべて Jupyter Notebookで実行している。

import numpy as np
a = np.ones(100000)
b=np.ones(100000 )

def dot(a, b):
    s = 0
    for i in range(len(a)):
        s += a[i] * b[i]
    return s

timeit dot(a, b)
41.6 ms ± 1.51 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

timeit np.dot(a, b)
62 µs ± 5.6 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Pythonのループを使った演算と、numpyを使った演算ではパフォーマンスに大きな差がある。これは for文が遅いから なのだろうか?

試しに、演算をせずにforループだけを実行してみよう。

def loop(a, b):
    s = 0
    for i in range(len(a)):
        pass
    return s

timeit loop(a, b)
3.44 ms ± 129 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

演算を行わず、forループを実行するだけなら、全体の10%以下しかかかっていない。まあ、forループ遅いが、全体の遅さの主犯ではないようだ。

では、Pythonの遅さの残り9割はどこからくるのだろう?

ここからは、Cythonを使って原因を探っていこう。

まず、Cythonで dot()C言語に変換する。

%%cython
def dot_cython(a,b):
    s = 0
    for i in range(len(a)):
        s += a[i] * b[i]
    return s

timeit dot_cython(a,b)
21.9 ms ± 717 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

単純にC言語に変換しただけでは、それほど変化はない。

ここでは、dot_cython()for ループは、CythonによってC言語のループに展開されており、Pythonのようなループによるオーバーヘッドはなくなっている。

また、Pythonバイトコードを経由せずに実行しているため、Pythonインタープリタのオーバヘッドはなくなっている。処理時間が 41.6 ms -> 21.9 ms と約半分になっているが、これはほぼインタープリタのオーバヘッドが解消したためだ。

ここでわかるのは、単純にPythonと同じ処理をC言語で書き直すだけでは、numpyの62µsという圧倒的な速度には遠くおよばない、ということだ。 Pythonインタープリタである、というのも、Pythonの遅さの原因の一部でしかないのである。

インタープリタ言語というと、何もかもがコンパイル型言語より数百倍遅くなるようなイメージがあるかもしれないが、それほど極端な差はつかないものだ。

Pythonの遅さの別の原因として、Pythonが静的な型定義を持たない、という点がある。

例えば、dot() では s += a[i] * b[i]という式を実行しているが、この中の X*Y のような乗算処理では、次のような処理が行われる。

  1. X が乗算をサポートしているかチェックする
  2. X の乗算関数を取得する
  3. Y が被乗数として適切なデータかチェックする
  4. X の値と Y の値を取り出し、乗算する
  5. 乗算の結果から新しい浮動小数点数オブジェクトを作成する

しかし、C や Javaのような、静的な型定義をもつプログラミング言語では、そもそも乗算を行えないような処理はコンパイルエラーとなるため、 上記の1. 〜 3. の処理の必要がなく、さまざまな最適化を行って処理を高速化できる。

Cythonでは、明示的にC言語のデータ型を指定して値を変換できる。まず、数値演算処理の部分にデータ型を宣言し、高速化してみよう。

%%cython
def dot_typed(a,b):
    cdef double s = 0.0
    for i from 0 <= i < len(a):
        s += <double>(a[i]) * <double>(b[i])
    return s

timeit dot_typed(a,b)
11.8 ms ± 189 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

この宣言により、次のような高速化が行われる。

  1. 上記の1. 〜 3. の処理は必要ない。
  2. 上記の 4. の演算処理は、Pythonの加算処理と乗算処理ではなく、CPUの機能で高速に演算されるようになる。
  3. 上記の 5. の浮動小数点数オブジェクトを生成せず、ハードウェアがサポートしている浮動小数点数が作成される。

だいぶ速くなったが、やはりまだ Numpy には及ばない。

dot_typed() では、演算中に <double>(a[i]) のようにして、Numpyの配列から要素を取得して、C言語double 型に変換している。実は、これもかなり複雑な処理なのだ。

  1. a が添字によるインデックスをサポートしているかチェックする
  2. a のインデックス関数を取得する
  3. 添字 i が添字として適切かチェックする
  4. i の整数値を取得する
  5. a から i 番目の値を取得する
  6. 取得した値で、numpy.float64 オブジェクトを作成する
  7. numpy.float64 オブジェクトを double 型に変換可能かチェックする
  8. numpy.float64 オブジェクトの変換関数を取得する
  9. double 型に変換する

この処理をスキップして、Numpy配列からデータを直接取得してみよう。

実は、Numpy配列には double 型のデータが格納されており、適切なデータ型を指定して直接参照してしまえば、変換は一切必要なくなってしまう。Numpy内部では、このような形式で要素を参照して効率的に処理を行えるようになっている。

Cythonには、Numpyなどのバッファを直接参照する、Typed Memoryview 型が備わっている。この機能で、単なる double 型データの入った配列としてアクセスできるようにしてみよう。

%%cython
def dot_view(double[:] a, double[:] b):
    cdef double s = 0.0
    for i from 0 <= i < len(a):
        s += a[i] * b[i]
    return s

timeit dot_view(a,b)
152 µs ± 6.32 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

おお。速くなった。データをPythonプロトコルを使って取り出す部分が最後のボトルネックだったわけで、この部分を最適化することで、Numpyの4割ぐらいのパフォーマンスまで迫ることができた。

のこりの速度差は、おそらくNumpy内部の内積処理の、高度な最適化によるものだと思う。もう一段階せまってみようかと思ったが、面倒なのでやめた。

つまり

  • Pythonを使った処理は遅くなるが、インタープリタだから、というのは実はそれほど大きな要因ではない。

  • Pythonの演算が遅い最大の要因は、Pythonが静的な型宣言を行わない言語で、型推論JITもなく、常に動的にオブジェクトの演算を行う、という点にある場合がほとんどだ。

  • Numpyでは、配列をすべて同じデータ型しか格納できない、 Homogeneous なコンテナとすることで、効率的に計算を行えるようにしている。

  • Numpy 「は?型推論?ぜんぶfloatにすればよくね?」

ついでに

Pythonが遅い原因として、GIL(Global Interpreter Lock) によってマルチコアをうまく使えないから、と言われることもある。

これもまあ、なくはない。

しかし、仮にGILがなくとも、Pythonの演算はせいぜいCPU数分しか速くならない。CPUが16個あってもたかだか16倍になるにすぎない。これではとてもNumpyには対抗できないのである。

おまけに

この根本的な原因は、今のJulia(v0.3.7)には破壊的な演算子が無いため、いつでも新しい配列を確保してしまう点にあります。

Pythonには、破壊的な代入演算子( += など) がある。

石本敦夫氏に聞く、Pythonの歴史とこれから〜Pythonエンジニア列伝 Vol.3 - PyQオフィシャルブログ でも話したが、Pythonにこの種の代入演算子が導入されたのは、実はNumpyで使用するためだった。

Python1.5までは、+= の導入には否定的な意見が多かった。これは、

X = [1,2,3]
X += [4,5]

のように、リストなどの更新可能なオブジェクトなら、リストオブジェクト X に新しく要素を追加すればよい。

しかし、同じようなスクリプトでも、

X = (1,2,3)
X += (4,5)

では、Xは更新不可能なタプルオブジェクトなので、要素を追加できない。この場合は、X に要素が追加されるのではなく、新しく (1,2,3,4,5) というタプルオブジェクトが、X に代入されることになる

このような判りにくさから、+= は導入されないという判断がくだされていた。

しかし、Numpyで大きな配列を効率的に演算するため、ということで必要性を認められ、導入されたのである。