Подтвердить что ты не робот

Почему строка запускается медленнее, чем в?

Удивительно, но 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) = 164 нс, но операция in занимает всего 81,7 нс. Кажется, есть еще некоторые накладные расходы для startswith, что это?

Добавьте результат теста между startswith и __contains__, как предложено poke и lvc:

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
4b9b3361

Ответ 1

Как уже упоминалось в комментариях, если вы используете 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") приведет к более сходному результату, однако для строки вашего примера s startswith будет все еще медленнее.

Чтобы дойти до этого, вы можете проверить реализацию обоих. Интересно видеть, что содержит реализацию состоит в том, что он статически типизирован и просто предполагает, что аргумент является объектом unicode. Так что это довольно эффективно.

startswith реализация, однако, является "динамическим" методом Python, который требует, чтобы реализация фактически анализировала аргументы. startswith также поддерживает кортеж в качестве аргумента, что делает весь запуск метода более медленным: (сокращенный мной, с моими комментариями):

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);
}

Вероятно, это большая причина, по которой startswith работает медленнее для строк, для которых a contains является быстрым из-за его простоты.

Ответ 2

Вероятно, потому что str.startswith() делает больше, чем str.__contains__(), а также потому, что я полагаю, что str.__contains__ полностью работает в C, тогда как str.startswith() должен взаимодействовать с типами Python. Его подпись str.startswith(prefix[, start[, end]]), где префикс может быть кортежем строк, чтобы попробовать.