はじめに
前回の記事で「なんとなくnp.float32が速い気がする」とか書いたので、実際に測ってみる。
予め断っておくと、計算速度なんて環境によって違うし、どの型が速いかもCPUのアーキテクチャに依存する。numpyはバリバリにSIMD命令を使って最適化する(と、思う)ので、演算の種類とかによっても優劣は変わる。あくまでも私の環境での試験である。
つかったCPU
- CPU名:Core i7-3540M
4年くらい前のモバイル版i7。モバイル版なのが泣ける。物理コアが2コアしかない時点でお察しである。とはいえ、個々のコアの性能は腐ってもi7
- クロック周波数:3GHz(たーぼ・ぶーすとなる技術によって負荷をかけると3.5GHzくらいまで引っ張ってくれる)
- コア数:物理コア*2(HTがあるので、論理コア*4)
- L1キャッシュ:128KB
- L2キャッシュ:512KB
- L3キャッシュ:4.0MB
ソースコード
# coding: UTF-8 import warnings;warnings.filterwarnings('ignore') import time from random import randint import numpy as np def make_random(l, n): return [randint(1,n) for _ in range(l)] def f(a,b,c): return a * b - c def g(a,b,c): return a/b/c def main(): for vec_size in [100, 1000, 10000, 100000, 1000000]: af64 = np.array(make_random(vec_size, 10000), dtype=np.float64) bf64 = np.array(make_random(vec_size, 10000), dtype=np.float64) cf64 = np.array(make_random(vec_size, 10000), dtype=np.float64) ai64 = np.array(make_random(vec_size, 10000), dtype=np.int64) bi64 = np.array(make_random(vec_size, 10000), dtype=np.int64) ci64 = np.array(make_random(vec_size, 10000), dtype=np.int64) af32 = np.array(af64, dtype=np.float32) bf32 = np.array(bf64, dtype=np.float32) cf32 = np.array(cf64, dtype=np.float32) ai32 = np.array(ai64, dtype=np.int32) bi32 = np.array(bi64, dtype=np.int32) ci32 = np.array(ci64, dtype=np.int32) af16 = np.array(af64, dtype=np.float16) bf16 = np.array(bf64, dtype=np.float16) cf16 = np.array(cf64, dtype=np.float16) ai16 = np.array(ai64, dtype=np.int16) bi16 = np.array(bi64, dtype=np.int16) ci16 = np.array(ci64, dtype=np.int16) print("vec size:",vec_size) start1 = time.time() f(af64,bf64,cf64) end1 = time.time() start2 = time.time() g(af64,bf64,cf64) end2 = time.time() print("float64: {0:.6f} {1:.6f}".format(end1-start1, end2-start2)) start1 = time.time() f(ai64,bi64,ci64) end1 = time.time() start2 = time.time() g(ai64,bi64,ci64) end2 = time.time() print("int64: {0:.6f} {1:.6f}".format(end1-start1,end2-start2)) start1 = time.time() f(af32,bf32,cf32) end1 = time.time() start2 = time.time() g(af32,bf32,cf32) end2 = time.time() print("float32: {0:.6f} {1:.6f}".format(end1-start1, end2-start2)) start1 = time.time() f(ai32,bi32,ci32) end1 = time.time() start2 = time.time() g(ai32,bi32,ci32) end2 = time.time() print("int32: {0:.6f} {1:.6f}".format(end1-start1,end2-start2)) start1 = time.time() f(af16,bf16,cf16) end1 = time.time() start2 = time.time() g(af16,bf16,cf16) end2 = time.time() print("float16: {0:.6f} {1:.6f}".format(end1-start1, end2-start2)) start1 = time.time() f(ai16,bi16,ci16) end1 = time.time() start2 = time.time() g(ai16,bi16,ci16) end2 = time.time() print("int16: {0:.6f} {1:.6f}".format(end1-start1,end2-start2)) if __name__ == '__main__': main()
そのまま回すとオーバーフローしまくるので警告は消している。関数fとgは適当に考えた計算処理。本来はもっと色々なものを試すべきだが、面倒くさいのでこれだけ。
実行すると乱数生成に時間がかかるので十数秒待たされる。ベクトルの計算自体は一瞬で終わる。
実行結果
vec size: 100 float64: 0.000051 0.000012 int64: 0.001442 0.000989 float32: 0.000040 0.000009 int32: 0.001109 0.000763 float16: 0.000125 0.000019 int16: 0.001328 0.001525 vec size: 1000 float64: 0.000123 0.000015 int64: 0.000014 0.000091 float32: 0.000009 0.000007 int32: 0.000008 0.000017 float16: 0.000129 0.000047 int16: 0.000007 0.000016 vec size: 10000 float64: 0.000682 0.000051 int64: 0.000033 0.000406 float32: 0.000017 0.000015 int32: 0.000015 0.000101 float16: 0.000933 0.000438 int16: 0.000015 0.000103 vec size: 100000 float64: 0.000842 0.000844 int64: 0.000672 0.003524 float32: 0.001567 0.000242 int32: 0.000189 0.003431 float16: 0.009531 0.004141 int16: 0.000073 0.002247 vec size: 1000000 float64: 0.034912 0.004574 int64: 0.004239 0.010060 float32: 0.002116 0.002203 int32: 0.002295 0.010271 float16: 0.089270 0.038791 int16: 0.001104 0.010050
まず全体を見て気づくのは、ほとんどのケースでfloat64に比べてfloat32が速いこと。ちゃんと調べてないが、恐らくCPUアーキテクチャの問題なのだろう。また、float16にメリットはまったくない(半精度の浮動小数点演算を高速に実行したいというニーズはあまりないのだろう)。int16は掛け算と引き算しかない計算では無双しているが、割り算になると極端に遅いことがわかる。というか、intの割り算は全般に遅い。
結論としては、やはり32bit浮動小数点数が速い。あくまでも私の環境ではと最初に断ったが、実際にはx86系のアーキテクチャなら似たような結果になる可能性が高い訳で、速度にシビアな(かつ精度を要求されない)状況ではこれを使っておけばよさそう。