a.insert(0,0)がa [0:0] = [0]よりもはるかに遅いのはなぜですか?


61

リストのinsert関数を使用すると、スライス割り当てを使用して同じ効果を得るよりもはるかに遅くなります。

> python -m timeit -n 100000 -s "a=[]" "a.insert(0,0)"
100000 loops, best of 5: 19.2 usec per loop

> python -m timeit -n 100000 -s "a=[]" "a[0:0]=[0]"
100000 loops, best of 5: 6.78 usec per loop

a=[]これは設定にすぎないため、a空から開始されますが、100,000要素まで増加します。)

最初は多分それは属性ルックアップか関数呼び出しのオーバーヘッドかと思ったが、最後の方に挿入するとそれは無視できることを示している:

> python -m timeit -n 100000 -s "a=[]" "a.insert(-1,0)"
100000 loops, best of 5: 79.1 nsec per loop

おそらくより単純な専用の「単一要素の挿入」機能が非常に遅いのはなぜですか?

repl.itでも再現できます

from timeit import repeat

for _ in range(3):
  for stmt in 'a.insert(0,0)', 'a[0:0]=[0]', 'a.insert(-1,0)':
    t = min(repeat(stmt, 'a=[]', number=10**5))
    print('%.6f' % t, stmt)
  print()

# Example output:
#
# 4.803514 a.insert(0,0)
# 1.807832 a[0:0]=[0]
# 0.012533 a.insert(-1,0)
#
# 4.967313 a.insert(0,0)
# 1.821665 a[0:0]=[0]
# 0.012738 a.insert(-1,0)
#
# 5.694100 a.insert(0,0)
# 1.899940 a[0:0]=[0]
# 0.012664 a.insert(-1,0)

Windows 10 64ビットでPython 3.8.1 32ビットを使用しています。
repl.itは、Linux 64ビット上でPython 3.8.1 64ビットを使用します。


それa=[]; a[0:0]=[0]が同じことをすることに注目するのは興味深いですa=[]; a[100:200]=[0]
smac89

空のリストだけでこれをテストする理由はありますか?
MisterMiyagi

@MisterMiyagiさて、私は何かから始めなければなりません。最初の挿入の前にのみ空であり、ベンチマーク中に100,000要素まで増加することに注意してください。
ヒープオーバーフロー

@ smac89 a=[1,2,3];a[100:200]=[4]は興味深い4リストの最後に追加していaます。
Ch3steR

1
@ smac89それは本当ですが、それは本当に質問とは関係がなく、誰かが私がベンチマークa=[]; a[0:0]=[0]a[0:0]=[0]している、またはそれと同じことをすることを誤解させるかもしれないと恐れていa[100:200]=[0]ます...
ヒープオーバーフロー

回答:


57

私はそれは、彼らが使用することを忘れてしまっただけのことだろうと思うmemmoveの中でlist.insert。要素をシフトするために使用するコード list.insertを見ると、それは単なる手動ループであることがわかります。

for (i = n; --i >= where; )
    items[i+1] = items[i];

しばらくlist.__setitem__スライス割り当てパス上の用途memmove

memmove(&item[ihigh+d], &item[ihigh],
    (k - ihigh)*sizeof(PyObject *));

memmove 通常、SSE / AVX命令を利用するなど、多くの最適化が行われています。


5
ありがとう。これを参照して問題を作成しました。
ヒープオーバーフロー

7
インタープリターが-O3自動ベクトル化を有効にして作成された場合、その手動ループは効率的にコンパイルされる可能性があります。ただし、コンパイラがループをmemmoveとして認識し、それをへの実際の呼び出しにコンパイルしない限り、memmoveコンパイル時に有効になっている命令セット拡張を利用することしかできません。(-march=nativeベースラインでビルドされたディストリビューションバイナリではそれほどではなく、を使用して独自にビルドしている場合は問題ありません)。また、PGO(-fprofile-generate/ run / ...-use)を使用しない限り、GCCはデフォルトでループを展開しません
Peter Cordes

@PeterCordesコンパイラが実際のmemmove呼び出しにコンパイルすると、実行時に存在するすべての拡張機能を利用できることを正しく理解していますか?
ヒープオーバーフロー

1
@HeapOverflow:はい。たとえば、GNU / Linuxでは、glibcは、保存されたCPU検出結果に基づいて、このマシンに最適な手書きのasmバージョンのmemmoveを選択する関数で動的リンカーシンボル解決をオーバーロードします。(たとえば、x86では、glibcのinit関数はを使用しますcpuid)。他のいくつかのmem / str関数についても同じです。したがって、ディストリビューションは-O2run-anywhereバイナリを作成するためだけでコンパイルできますが、少なくともmemcpy / memmoveで、展開されていないAVXループを使用して、命令ごとに32バイトをロード/格納します。(または、それが良いアイデアであるいくつかのCPUではAVX512です。XeonPhiだと思います。)
Peter Cordes

1
@HeapOverflow:いいえ、いくつかのmemmoveバージョンが共有ライブラリlibc.soにあります。関数ごとに、ディスパッチはシンボル解決中に1回発生します(早期バインディングまたは従来の遅延バインディングによる最初の呼び出し時)。先ほど言ったように、関数自体をラップするのではなく、動的リンクが発生する方法を単にオーバーロード/フックします。(特にGCCのifuncメカニズムを介して:code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/…)。関連:memsetの最新のCPUでの通常の選択は__memset_avx2_unaligned_erms 、このQ&Aを参照してください
Peter Cordes
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.