development

문자열의 시작이 in보다 느린 이유는 무엇입니까?

big-blog 2021. 1. 8. 22:47
반응형

문자열의 시작이 in보다 느린 이유는 무엇입니까?


놀랍게도 다음 startswith보다 느립니다 in.

In [10]: s="ABCD"*10

In [11]: %timeit s.startswith("XYZ")
1000000 loops, best of 3: 307 ns per loop

In [12]: %timeit "XYZ" in s
10000000 loops, best of 3: 81.7 ns per loop

우리 모두 알다시피 in작업은 전체 문자열을 검색 startswith하고 처음 몇 글자 만 확인하면되므로 startswith더 효율적이어야합니다.

s충분히 클 startswith더 빠릅니다.

In [13]: s="ABCD"*200

In [14]: %timeit s.startswith("XYZ")
1000000 loops, best of 3: 306 ns per loop

In [15]: %timeit "XYZ" in s
1000000 loops, best of 3: 666 ns per loop

따라서 호출 startswith에는 약간의 오버 헤드가있어 문자열이 작을 때 속도가 느려지는 것 같습니다 .

그리고 startswith전화 의 오버 헤드가 무엇인지 알아 내려고 노력한 것보다 .

첫째, f답변 에서 언급했듯이 점 연산의 비용을 줄이기 위해 변수를 사용했습니다. 여기서 우리 startswith는 여전히 더 느린 것을 알 수 있습니다 .

In [16]: f=s.startswith

In [17]: %timeit f("XYZ")
1000000 loops, best of 3: 270 ns per loop

또한 빈 함수 호출 비용을 테스트했습니다.

In [18]: def func(a): pass

In [19]: %timeit func("XYZ")
10000000 loops, best of 3: 106 ns per loop

도트 연산 및 함수 호출 비용에 관계없이 시간 startswith은 약 (270-106) = 164ns이지만 in작업에는 81.7ns 만 걸립니다. 에 대한 오버 헤드가 여전히있는 것 같습니다. 그게 뭔데요 startswith?

poke 및 lvc에서 제안한대로 startswith사이에 테스트 결과를 추가합니다 __contains__.

In [28]: %timeit s.startswith("XYZ")
1000000 loops, best of 3: 314 ns per loop

In [29]: %timeit s.__contains__("XYZ")
1000000 loops, best of 3: 192 ns per loop

주석에서 이미 언급했듯이 사용 s.__contains__("XYZ")하면 s.startswith("XYZ")동일한 경로를 취해야 하기 때문에 보다 유사한 결과를 얻을 수 있습니다 . 문자열 객체에 대한 멤버 조회, 함수 호출이 이어집니다. 이것은 일반적으로 다소 비쌉니다 (물론 걱정할만큼 충분하지 않습니다). 반면에를 수행 "XYZ" in s하면 파서가 연산자를 해석하고에 대한 멤버 액세스를 단축 할 수 있습니다 __contains__(또는 __contains__그 자체가 구현에 액세스하는 한 가지 방법 이기 때문에 뒤에 있는 구현).

바이트 코드를 살펴보면 이에 대한 아이디어를 얻을 수 있습니다.

>>> dis.dis('"XYZ" in s')
  1           0 LOAD_CONST               0 ('XYZ')
              3 LOAD_NAME                0 (s)
              6 COMPARE_OP               6 (in)
              9 RETURN_VALUE
>>> dis.dis('s.__contains__("XYZ")')
  1           0 LOAD_NAME                0 (s)
              3 LOAD_ATTR                1 (__contains__)
              6 LOAD_CONST               0 ('XYZ')
              9 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             12 RETURN_VALUE

따라서 s.__contains__("XYZ")비교 s.startswith("XYZ")하면 더 유사한 결과가 생성되지만 예제 string sstartswith경우 여전히 느려집니다.

To get to that, you could check the implementation of both. Interesting to see for the contains implementation is that it is statically typed, and just assumes that the argument is a unicode object itself. So this is quite efficient.

The startswith implementation however is a “dynamic” Python method which requires the implementation to actually parse the arguments. startswith also supports a tuple as an argument, which makes the whole start-up of the method a bit slower: (shortened by me, with my comments):

static PyObject * unicode_startswith(PyObject *self, PyObject *args)
{
    // argument parsing
    PyObject *subobj;
    PyObject *substring;
    Py_ssize_t start = 0;
    Py_ssize_t end = PY_SSIZE_T_MAX;
    int result;
    if (!stringlib_parse_args_finds("startswith", args, &subobj, &start, &end))
        return NULL;

    // tuple handling
    if (PyTuple_Check(subobj)) {}

    // unicode conversion
    substring = PyUnicode_FromObject(subobj);
    if (substring == NULL) {}

    // actual implementation
    result = tailmatch(self, substring, start, end, -1);
    Py_DECREF(substring);
    if (result == -1)
        return NULL;
    return PyBool_FromLong(result);
}

This is likely a big reason why startswith is slower for strings for which a contains is fast because of its simplicity.


This is likely because str.startswith() does more than str.__contains__(), and also because I believe str.__contains__ operates fully in C, whereas str.startswith() has to interact with Python types. Its signature is str.startswith(prefix[, start[, end]]), where prefix can be a tuple of strings to try.

ReferenceURL : https://stackoverflow.com/questions/31917372/why-is-strings-startswith-slower-than-in

반응형