概要
「トップレベルのループと関数内のループの違い」で見ていた
loop_toplevel.py
for i in range(10**7):
    pass
をサンプルとして、Python VMの処理をプロファイルして、STORE_NAME の処理が実行時間に効いてるのを見てみましょう。
Ubuntu 18.04環境で確認します。
Pythonのソースコード取得
https://github.com/python/cpython から取得
~$ git clone https://github.com/python/cpython.git
Pythonのビルド
gcc をインストール
~$ sudo apt install -y gcc
プロファイラを有効に、最適化を無効にしてビルドする
~$ cd cpython
~/cpython$ ./configure --enable-profiling --with-pydebug
~/cpython$ make
プロファイル
対象のスクリプトを用意
~/cpython$ vi loop_toplevel.py
~/cpython$ cat loop_toplevel.py
for i in range(10**7):
    pass
ビルドしたpythonを単純に実行すればプロファイルしてくれる。
~/cpython$ ./python loop_toplevel.py
gmon.outにプロファイル結果が保存されている。サマリを見るには gprof ./python すればよい。
~/cpython$ ls -l gmon.out
-rw-rw-r-- 1 vagrant vagrant 1284656 Nov  4 03:16 gmon.out
~/cpython$ gprof ./python|head -n 20
Flat profile:
Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total
 time   seconds   seconds    calls   s/call   s/call  name
 17.46      0.22     0.22 20043118     0.00     0.00  read_size_t
  9.52      0.34     0.12     2229     0.00     0.00  _PyEval_EvalFrameDefault
  7.94      0.44     0.10 10007889     0.00     0.00  insertdict
  6.35      0.52     0.08 10026827     0.00     0.00  _PyMem_DebugRawAlloc
  6.35      0.60     0.08 10021559     0.00     0.00  _PyMem_DebugCheckAddress
  4.76      0.66     0.06 20045319     0.00     0.00  PyGILState_Check
  3.97      0.71     0.05 10020541     0.00     0.00  address_in_range
  3.97      0.76     0.05 10001148     0.00     0.00  PyLong_FromLong
  3.97      0.81     0.05 10046396     0.00     0.00  lookdict_unicode_nodummy
  3.97      0.86     0.05 10021024     0.00     0.00  _PyMem_DebugRawFree
  3.57      0.91     0.05 10019912     0.00     0.00  _PyObject_Free
  3.17      0.95     0.04 10027556     0.00     0.00  write_size_t
  3.17      0.99     0.04 10019415     0.00     0.00  _PyMem_DebugFree
  2.38      1.02     0.03 10017088     0.00     0.00  _PyDict_CheckConsistency
  2.38      1.05     0.03 10008245     0.00     0.00  PyDict_SetItem
gprof2dotで可視化
gprof2dotを使ってコールグラフを可視化してみる。
~/cpython$ sudo apt install -y graphviz
~/cpython$ sudo apt install -y python3-pip
~/cpython$ pip3 install --user gprof2dot
~/cpython$ gprof ./python | ~/.local/bin/gprof2dot | dot -Tpng -o output.png
PyDict_SetItemがループ回数のオーダーで呼ばれていて、それ以下が時間を食っているのが見れた(total time 51.14%, self time 2.31%, 10008245 calls)。
ソースで確認
Python VMのSTORE_NAMEの実装を見ると、たしかにPyDict_SetItemを呼んでる。
cpython/Python/ceval.c
case TARGET(STORE_NAME): {
    PyObject *name = GETITEM(names, oparg);
    PyObject *v = POP();
    PyObject *ns = f->f_locals;
    int err;
    if (ns == NULL) {
        _PyErr_Format(tstate, PyExc_SystemError,
                      "no locals found when storing %R", name);
        Py_DECREF(v);
        goto error;
    }
    if (PyDict_CheckExact(ns))
        err = PyDict_SetItem(ns, name, v);
    else
        err = PyObject_SetItem(ns, name, v);
    Py_DECREF(v);
    if (err != 0)
        goto error;
    DISPATCH();
}
