web-dev-qa-db-ja.com

str.strip()がstr.strip( '')よりもはるかに高速なのはなぜですか?

空白での分割は、_str.strip_を使用して2つの方法で実行できます。引数なしで呼び出しを発行するか、str.strip()を発行できます。これは、デフォルトで空白区切り文字を使用するか、引数にstr.strip(' ')を明示的に指定します。

しかし、なぜこれらの機能が非常に異なって実行されるのですか?

意図的な量の空白を含むサンプル文字列の使用:

_s = " " * 100 + 'a' + " " * 100
_

s.strip()s.strip(' ')のタイミングはそれぞれ次のとおりです。

_%timeit s.strip()
The slowest run took 32.74 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 396 ns per loop

%timeit s.strip(' ')
100000 loops, best of 3: 4.5 µs per loop
_

stripは_396ns_を取り、strip(' ')は_4.5 μs_を取りますが、同じ条件下でrsplitlsplitを使用した場合も同様のシナリオが存在します。 。また、 _bytes objects_も影響を受けているようです

タイミングは_Python 3.5.2_に対して実行され、_Python 2.7.1_では差はそれほど大きくありません。 _str.split_ のドキュメントは有用なものを何も示していないので、なぜこれが発生するのですか

Tl; dr方式で:

これは、 _unicode_strip_ ;に見られるように、2つの異なるケースに対して2つの関数が存在するためです。 _do_strip_および__PyUnicodeXStrip_最初の実行は2番目の実行よりもはるかに高速です。

関数_do_strip_は、引数が存在しない一般的なケースstr.strip()用です。 _do_argstrip_ (__PyUnicode_XStrip_をラップします)str.strip(arg)が呼び出された場合つまり、引数が提供されます。


_do_argstrip_はセパレーターをチェックするだけで、それが有効でNone(この場合は_do_strip_を呼び出します)と等しくない場合は __PyUnicode_XStrip_ を呼び出します。

_do_strip_と__PyUnicode_XStrip_はどちらも同じロジックに従い、2つのカウンターが使用されます。1つはゼロに等しく、もう1つは文字列の長さに等しくなります。

2つのwhileループを使用して、最初のカウンターはセパレーターと等しくない値に達するまでインクリメントされ、2番目のカウンターは同じ条件が満たされるまでデクリメントされます。

違いは、現在の文字が区切り文字と等しくないかどうかをチェックする方法にあります。

_do_strip_の場合:

分割される文字列の文字をasciiで表すことができる最も一般的なケースでは、追加の小さなパフォーマンスの向上が見られます。

_while (i < len) {
    Py_UCS1 ch = data[i];
    if (!_Py_ascii_whitespace[ch])
        break;
    i++;
}
_
  • データ内の現在の文字へのアクセスは、基になる配列にアクセスすることですばやく実行できます:_Py_UCS1 ch = data[i];_
  • 文字が空白であるかどうかのチェックは、単純な配列インデックスによって __Py_ascii_whitespace[ch]_ という配列に行われます。

つまり、非常に効率的です。

文字がasciiの範囲にない場合、違いはそれほど劇的ではありませんが、全体的な実行が遅くなります。

_while (i < len) {
    Py_UCS4 ch = PyUnicode_READ(kind, data, i);
    if (!Py_UNICODE_ISSPACE(ch))
        break;
    i++;
}
_
  • アクセスはPy_UCS4 ch = PyUnicode_READ(kind, data, i);で行われます
  • 文字が空白であるかどうかのチェックは、 Py_UNICODE_ISSPACE(ch) マクロ(単に別のマクロを呼び出す: _Py_ISSPACE_ )によって行われます。

__PyUnicodeXStrip_の場合:

この場合、基になるデータへのアクセスは、前の場合と同様に、_PyUnicode_Read_で行われます。一方、文字が空白(または実際に提供された文字)であるかどうかを確認するチェックは、かなり複雑です。

_while (i < len) {
     Py_UCS4 ch = PyUnicode_READ(kind, data, i);
     if (!BLOOM(sepmask, ch))
         break;
     if (PyUnicode_FindChar(sepobj, ch, 0, seplen, 1) < 0)
         break;
     i++;
}
_

_PyUnicode_FindChar_ が使用されます。これは効率的ですが、配列アクセスに比べてはるかに複雑で低速です。文字列内の各文字について、その文字が指定された区切り文字に含まれているかどうかを確認するために呼び出されます。文字列の長さが長くなると、この関数を継続的に呼び出すことによって生じるオーバーヘッドも大きくなります。

興味のある人のために、かなりのチェックの後、_PyUnicode_FindChar_は最終的にstringlib内で _find_char_ を呼び出します。セパレータの長さが_< 10_の場合、キャラクターを見つけます。

これとは別に、ここに到達するためにすでに呼び出される必要がある追加の関数を検討してください。


lstriprstripについては、状況は似ています。実行するストライピングのモードが存在するフラグ。つまり、RIGHTSTRIPの場合はrstripLEFTSTRIPの場合はlstripBOTHSTRIPの場合はstrip。 _do_strip_および__PyUnicode_XStrip_内のロジックは、フラグに基づいて条件付きで実行されます。

@Jimsの回答で説明されている理由により、同じ動作がbytesオブジェクトに見られます。

b = bytes(" " * 100 + "a" + " " * 100, encoding='ascii')

b.strip()      # takes 427ns
b.strip(b' ')  # takes 1.2μs

bytearrayオブジェクトの場合、これは発生しません。この場合、splitを実行する関数は、どちらの場合も同様です。

さらに、Python 2私のタイミングによると、同じことが少しは当てはまります。

7
root