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

Почему реализация startwith медленнее, чем нарезка?

In [1]: x = 'foobar'

In [2]: y = 'foo'

In [3]: %timeit x.startswith(y)
1000000 loops, best of 3: 321 ns per loop

In [4]: %timeit x[:3] == y
10000000 loops, best of 3: 164 ns per loop

Удивительно, но даже с учетом расчета длины нарезка все равно выполняется значительно быстрее:

In [5]: %timeit x[:len(y)] == y
1000000 loops, best of 3: 251 ns per loop

Примечание: первая часть этого поведения отмечена в Python для анализа данных (глава 3), но никаких объяснений этому не дается.

.

Если полезно: вот код C для startswith; и вот результат dis.dis:

In [6]: import dis

In [7]: dis_it = lambda x: dis.dis(compile(x, '<none>', 'eval'))

In [8]: dis_it('x[:3]==y')
  1           0 LOAD_NAME                0 (x)
              3 LOAD_CONST               0 (3)
              6 SLICE+2             
              7 LOAD_NAME                1 (y)
             10 COMPARE_OP               2 (==)
             13 RETURN_VALUE        

In [9]: dis_it('x.startswith(y)')
  1           0 LOAD_NAME                0 (x)
              3 LOAD_ATTR                1 (startswith)
              6 LOAD_NAME                2 (y)
              9 CALL_FUNCTION            1
             12 RETURN_VALUE 

person Andy Hayden    schedule 07.11.2012    source источник
comment
Нарезка, вероятно, более сильно оптимизирована на уровне C   -  person Jakob Bowyer    schedule 07.11.2012
comment
это не эквивалентный код. вы можете передать кортеж в startswith, вы можете передать параметры start и end, это требует времени.   -  person SilentGhost    schedule 07.11.2012
comment
Когда вы говорите об оптимизации, очень важны базовая ОС и архитектура процессора. Это x86 (Intel или AMD) или x86-64bit? или ядро ​​ARM, на linux или windows ...?   -  person Louis Ricci    schedule 07.11.2012
comment
@LastCoder Это Intel x86-64 под управлением osx. Я думал, что (в широком смысле) реализации C (и, следовательно, CPython) на разных платформах будут сопоставимы (т.е. если одна архитектура запускает код медленнее, то другие тоже) ...?   -  person Andy Hayden    schedule 07.11.2012
comment
@hayden - различия в версиях стандартных библиотек ОС, версия используемого компилятора, объем доступной памяти при запуске теста, фоновые задачи / запущенные процессы, процессор Intel или AMD и т. д. ВСЕ могут вызывать различия в результаты тестов. Особенно, когда вы имеете дело с повторным тестом (где значения совпадают и сравниваются наносекундные измерения. Вы можете столкнуться с оптимизацией кэширования и прогнозирования ветвлений на уровне архитектуры, что может исказить ваши результаты.   -  person Louis Ricci    schedule 07.11.2012
comment
@LastCoder Я согласен, что сами числа будут разными (если я запущу их дважды, они будут разными!), но я был бы удивлен, если бы значительные различия (например, одно быстрее другого) не отразились на все платформы с учетом зрелости компилятора C. Я не считал кеширование и предсказания ветвлений на уровне архитектуры ... интересными.   -  person Andy Hayden    schedule 07.11.2012


Ответы (5)


Некоторую разницу в производительности можно объяснить, если принять во внимание время, необходимое оператору . для выполнения своей задачи:

>>> x = 'foobar'
>>> y = 'foo'
>>> sw = x.startswith
>>> %timeit x.startswith(y)
1000000 loops, best of 3: 316 ns per loop
>>> %timeit sw(y)
1000000 loops, best of 3: 267 ns per loop
>>> %timeit x[:3] == y
10000000 loops, best of 3: 151 ns per loop

Другая часть различия может быть объяснена тем фактом, что startswith - это функция, и даже вызовы безоперационных функций занимают немного времени:

>>> def f():
...     pass
... 
>>> %timeit f()
10000000 loops, best of 3: 105 ns per loop

Это не полностью объясняет разницу, поскольку версия, использующая нарезку и len, вызывает функцию, но при этом работает быстрее (сравните с sw(y) выше - 267 нс):

>>> %timeit x[:len(y)] == y
1000000 loops, best of 3: 213 ns per loop

Я предполагаю, что, возможно, Python оптимизирует время поиска для встроенных функций или что len вызовы сильно оптимизированы (что, вероятно, верно). Возможно, это удастся проверить с помощью пользовательского len func. Или, возможно, именно здесь проявляются различия, выявленные LastCoder. Также обратите внимание на larsmans", которые показывают, что startswith на самом деле быстрее для более длинных строк. Все приведенные выше рассуждения применимы только к тем случаям, когда накладные расходы, о которых я говорю, действительно имеют значение.

person senderle    schedule 07.11.2012

Сравнение нечестно, поскольку вы измеряете только случай, когда startswith возвращает True.

>>> x = 'foobar'
>>> y = 'fool'
>>> %timeit x.startswith(y)
1000000 loops, best of 3: 221 ns per loop
>>> %timeit x[:3] == y  # note: length mismatch
10000000 loops, best of 3: 122 ns per loop
>>> %timeit x[:4] == y
10000000 loops, best of 3: 158 ns per loop
>>> %timeit x[:len(y)] == y
1000000 loops, best of 3: 210 ns per loop
>>> sw = x.startswith
>>> %timeit sw(y)
10000000 loops, best of 3: 176 ns per loop

Кроме того, для гораздо более длинных строк startswith намного быстрее:

>>> import random
>>> import string
>>> x = '%030x' % random.randrange(256**10000)
>>> len(x)
20000
>>> y = r[:4000]
>>> %timeit x.startswith(y)
1000000 loops, best of 3: 211 ns per loop
>>> %timeit x[:len(y)] == y
1000000 loops, best of 3: 469 ns per loop
>>> sw = x.startswith
>>> %timeit sw(y)
10000000 loops, best of 3: 168 ns per loop

Это все еще верно, когда нет совпадений.

# change last character of y
>>> y = y[:-1] + chr((ord(y[-1]) + 1) % 256)
>>> %timeit x.startswith(y)
1000000 loops, best of 3: 210 ns per loop
>>> %timeit x[:len(y)] == y
1000000 loops, best of 3: 470 ns per loop
>>> %timeit sw(y)
10000000 loops, best of 3: 168 ns per loop
# change first character of y
>>> y = chr((ord(y[0]) + 1) % 256) + y[1:]
>>> %timeit x.startswith(y)
1000000 loops, best of 3: 210 ns per loop
>>> %timeit x[:len(y)] == y
1000000 loops, best of 3: 442 ns per loop
>>> %timeit sw(y)
10000000 loops, best of 3: 168 ns per loop

Итак, startswith, вероятно, медленнее для коротких строк, потому что он оптимизирован для длинных.

(Уловка для получения случайных строк, взятых из этого ответа.)

person Fred Foo    schedule 07.11.2012

startswith сложнее, чем нарезка ...

2924 result = _string_tailmatch(self,
2925 PyTuple_GET_ITEM(subobj, i),
2926 start, end, -1);

Это не простой цикл сравнения символов для иглы в начале стога сена. Мы смотрим на цикл for, который выполняет итерацию по вектору / кортежу (подобъекту j) и вызывает для него другую функцию (_string_tailmatch). Множественные вызовы функций имеют накладные расходы в отношении стека, проверки корректности аргументов и т. Д.

startswith - это библиотечная функция, а нарезка кажется встроенной в язык.

2919 if (!stringlib_parse_args_finds("startswith", args, &subobj, &start, &end))
2920 return NULL;
person Louis Ricci    schedule 07.11.2012
comment
StartsWith может обрабатывать векторы. - понятия не имею, что вы имеете в виду. - person Eric; 07.11.2012
comment
@senderle - нет C # ... lol ... в любом случае, вы технически правильно, поэтому я правильно его разместил. - person Louis Ricci; 07.11.2012
comment
@Eric - посмотрите на источник, он может обрабатывать несколько аргументов, в то время как нарезка является внутренним. - person Louis Ricci; 07.11.2012
comment
Хорошо, +1, теперь это имеет больше смысла. - person senderle; 07.11.2012
comment
@LastCoder: Как выглядит использование нескольких аргументов? - person Eric; 07.11.2012
comment
@Eric - Моя формулировка была неудачной, я имел в виду только дополнительную сложность из-за дополнительной обработки, связанной с необязательными аргументами str.startswith (prefix [, start [, end]]). Исходный код создает впечатление, что startWith может обрабатывать несколько префиксов, но в документации этот случай не описывается. - person Louis Ricci; 07.11.2012

Чтобы процитировать документы, startswith делает больше, чем вы можете подумать:

str.startswith(prefix[, start[, end]])

Вернуть True, если строка начинается с префикса, в противном случае вернуть False. prefix также может быть набором префиксов, которые нужно искать. С необязательным start тестовая строка, начинающаяся с этой позиции. С дополнительным параметром end прекратите сравнение строки в этой позиции.

person Eric    schedule 07.11.2012

Вызвать функцию довольно дорого. Однако я не знаю, относится ли это также к встроенным функциям, написанным на C.

Однако имейте в виду, что нарезка может включать вызов функции в зависимости от используемого объекта.

person glglgl    schedule 07.11.2012