速い Python コードを書くためのマイクロベンチマーク

Python コードを書くにあたって特性というかクセのようなものは知っておいた方がいいだろうと、Pythonコードの書き方の違いによる時間計測してみた。

計測環境は 2020/5/9 時点の Google の Colaboratory で以下のスペック。

  • Python 3.6.9
  • OS Ubuntu 18.04.3 LTS
  • CPU Xeon 2.30GHz 2core Model 63 Stepping 0
  • メモリ 12GB

まとめ

  • malloc(N) 的なサイズ指定で list を作成する場合、 * 演算子が速い。
  • list に入れる要素数が既知の場合、事前にサイズ指定の list を作ると速い。
  • ループ中で変化しない値で比較する場合、一時変数に代入しておく方が速い。
  • Google Colaboratory 便利かも

指定サイズでの list の確保

malloc(N) 的なサイズ指定で list を作成する場合、 * 演算子を使うほうが10倍速い。
* 演算子 < リスト内包表現 < たぶん for ループ

import timeit

t1 = timeit.timeit('[0] * 1000', number=10000)
t2 = timeit.timeit('[0 for _ in range(1000)]', number=10000)

print(f'[0] * 1000               => {t1:.3f} [sec]')
print(f'[0 for _ in range(1000)] => {t2:.3f} [sec]')
print(f'{t2/t1:.3f} times')
[0] * 1000               => 0.026 [sec]
[0 for _ in range(1000)] => 0.333 [sec]
12.774 times

入れる要素数が既知な場合の list 作成

list に入れる要素数が既知の場合、事前にサイズ指定の list を作っておいた方が1.4倍ほど速い。
append ではバッファの拡張があるので、リストに入れる要素数がより大きくなると影響も大きくなる予感。それは計ってないけれども。

import timeit

def pre_alloc():
  buf = [None] * 1000
  for i in range(1000):
    buf[i] = 'a'

def simple_append():
  buf = []
  for i in range(1000):
    buf.append('a')

t1 = timeit.timeit('pre_alloc()', number=10000, globals=globals())
t2 = timeit.timeit('simple_append()', number=10000, globals=globals())
print(f'pre_alloc     => {t1:.3f} [sec]')
print(f'simple_append => {t2:.3f} [sec]')
print(f'{t2/t1:.3f} times faster')
pre_alloc     => 0.454 [sec]
simple_append => 0.667 [sec]
1.468 times faster

ループ内不変値の比較

例えばNested loop joinのような処理で、外ループで決まって内ループでは変化しない値と比較する場合は、一時変数に代入しておく方が速い。(完全に固定値の場合は、即値で書く方が速い)
リストやタプル、dict、その他オブジェクトを介したアクセスはその分重くなる模様。
動的型付けなので比較そのもの以外の処理の影響も大きい&ループ内不変値の最適化はしてないっぽい。

import timeit

def cmp_by_immidiate():  # 完全固定値の場合
  for i in range(1000):
    if i == 999:
      break

def cmp_by_scala():
  buf = [None] * 1000
  buf[999] = 999
  local_static = buf[999]
  for i in range(1000):
    if i == local_static:
      break

def cmp_by_list():
  buf = [None] * 1000
  buf[999] = 999
  for i in range(1000):
    if i == buf[999]:
      break

t1 = timeit.timeit('cmp_by_immidiate()', number=10000, globals=globals())
t2 = timeit.timeit('cmp_by_scala()', number=10000, globals=globals())
t3 = timeit.timeit('cmp_by_list()', number=10000, globals=globals())
print(f'eval_by_immidiate => {t1:.3f} [sec]')
print(f'eval_by_scala     => {t2:.3f} [sec]')
print(f'eval_by_list      => {t3:.3f} [sec]')
print(f'eval_by_immidiate {t2/t1:.3f} times faster than cmp_by_scala')
print(f'eval_by_scala     {t3/t2:.3f} times faster than cmp_by_list')
eval_by_immidiate => 0.389 [sec]
eval_by_scala     => 0.415 [sec]
eval_by_list      => 0.530 [sec]
eval_by_immidiate 1.068 times faster than cmp_by_scala
eval_by_scala     1.276 times faster than cmp_by_list

計測環境

計測は 2020/5/9 時点の Google Colaboratory の python 環境を利用。

import sys
from pathlib import Path

osinfo = Path('/etc/issue')
cpuinfo = Path('/proc/cpuinfo')
meminfo = Path('/proc/meminfo')

for path in [osinfo, cpuinfo, meminfo]:
  with path.open(mode='r') as fp:
   for line in fp:
      print(line.strip())
print(sys.version_info)
Ubuntu 18.04.3 LTS \n \l

processor	: 0
vendor_id	: GenuineIntel
cpu family	: 6
model		: 63
model name	: Intel(R) Xeon(R) CPU @ 2.30GHz
stepping	: 0
microcode	: 0x1
cpu MHz		: 2300.000
cache size	: 46080 KB
physical id	: 0
siblings	: 2
core id		: 0
cpu cores	: 1
apicid		: 0
initial apicid	: 0
fpu		: yes
fpu_exception	: yes
cpuid level	: 13
wp		: yes
flags		: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm invpcid_single ssbd ibrs ibpb stibp fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt arat md_clear arch_capabilities
bugs		: cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs itlb_multihit
bogomips	: 4600.00
clflush size	: 64
cache_alignment	: 64
address sizes	: 46 bits physical, 48 bits virtual
power management:

processor	: 1
vendor_id	: GenuineIntel
cpu family	: 6
model		: 63
model name	: Intel(R) Xeon(R) CPU @ 2.30GHz
stepping	: 0
microcode	: 0x1
cpu MHz		: 2300.000
cache size	: 46080 KB
physical id	: 0
siblings	: 2
core id		: 0
cpu cores	: 1
apicid		: 1
initial apicid	: 1
fpu		: yes
fpu_exception	: yes
cpuid level	: 13
wp		: yes
flags		: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm invpcid_single ssbd ibrs ibpb stibp fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt arat md_clear arch_capabilities
bugs		: cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs itlb_multihit
bogomips	: 4600.00
clflush size	: 64
cache_alignment	: 64
address sizes	: 46 bits physical, 48 bits virtual
power management:

MemTotal:       13333540 kB
MemFree:        10687540 kB
MemAvailable:   12492736 kB
Buffers:           74588 kB
Cached:          1878092 kB
SwapCached:            0 kB
Active:           711776 kB
Inactive:        1681196 kB
Active(anon):     409164 kB
Inactive(anon):      320 kB
Active(file):     302612 kB
Inactive(file):  1680876 kB
Unevictable:           0 kB
Mlocked:               0 kB
SwapTotal:             0 kB
SwapFree:              0 kB
Dirty:               480 kB
Writeback:             0 kB
AnonPages:        440280 kB
Mapped:           223268 kB
Shmem:               904 kB
Slab:             162112 kB
SReclaimable:     125380 kB
SUnreclaim:        36732 kB
KernelStack:        3408 kB
PageTables:         5316 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:     6666768 kB
Committed_AS:    2539472 kB
VmallocTotal:   34359738367 kB
VmallocUsed:           0 kB
VmallocChunk:          0 kB
Percpu:              920 kB
AnonHugePages:         0 kB
ShmemHugePages:        0 kB
ShmemPmdMapped:        0 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
Hugetlb:               0 kB
DirectMap4k:       79036 kB
DirectMap2M:     6211584 kB
DirectMap1G:     9437184 kB
sys.version_info(major=3, minor=6, micro=9, releaselevel='final', serial=0)