静かなる名辞

pythonとプログラミングのこと


【python】numpyの型の違いによる計算速度差を見てみる

はじめに

 前回の記事で「なんとなく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系のアーキテクチャなら似たような結果になる可能性が高い訳で、速度にシビアな(かつ精度を要求されない)状況ではこれを使っておけばよさそう。