Путаница при выполнении модуля Python timeit

Я пытаюсь использовать модуль timeit в Python (EDIT: мы используем Python 3), чтобы выбирать между несколькими различными потоками кода. В нашем коде у нас есть серия операторов if, которые проверяют наличие кода символа в строке, и если он есть, заменяют его следующим образом:

if "<substring>" in str_var:
    str_var = str_var.replace("<substring>", "<new_substring>")

Мы делаем это несколько раз для разных подстрок. Мы спорим между этим и использованием только такой замены:

str_var = str_var.replace("<substring>", "<new_substring>")

Мы попытались использовать timeit, чтобы определить, какой из них быстрее. Если первый кодовый блок выше — «stmt1», а второй — «stmt2», наша строка установки выглядит так:

str_var = '<string><substring><more_string>',

наши операторы timeit будут выглядеть так:

timeit.timeit(stmt=stmt1, setup=setup)

а также

timeit.timeit(stmt=stmt2, setup=setup)

Теперь, запустив его просто так, на 2 наших ноутбуках (такое же оборудование, аналогичная вычислительная нагрузка) stmt1 (оператор с оператором if) работает быстрее даже после нескольких запусков (3-4 сотых секунды против около четверти). секунды для stmt2).

Однако, если мы определим функции для выполнения обеих задач (включая настройку создания переменной) следующим образом:

def foo():
    str_var = '<string><substring><more_string>'
    if "<substring>" in str_var:
        str_var = str_var.replace("<substring>", "<new_substring>")

а также

def foo2():
    str_var = '<string><substring><more_string>'
    str_var = str_var.replace("<substring>", "<new_substring>")

и запустить timeit как:

timeit.timeit("foo()", setup="from __main__ import foo")
timeit.timeit("foo2()", setup="from __main__ import foo2")

оператор без оператора if (foo2) работает быстрее, что противоречит неработающим результатам.

Мы что-то упускаем из того, как работает Timeit? Или как Python справляется с подобным случаем?

редактируйте здесь наш фактический код:

>>> def foo():
    s = "hi 1 2 3"
    s = s.replace('1','5')

>>> def foo2():
    s = "hi 1 2 3"
    if '1' in s:
        s = s.replace('1','5')


>>> timeit.timeit(foo, "from __main__ import foo")
0.4094226634183542
>>> timeit.timeit(foo2, "from __main__ import foo2")
0.4815539780738618

против этого кода:

>>> timeit.timeit("""s = s.replace("1","5")""", setup="s = 'hi 1 2 3'")
0.18738432400277816
>>> timeit.timeit("""if '1' in s: s = s.replace('1','5')""", setup="s = 'hi 1 2 3'")
0.02985000199987553

person CraigularB    schedule 06.01.2014    source источник
comment
Можете ли вы опубликовать свой точный код тестирования?   -  person Blender    schedule 07.01.2014
comment
@Blender Я добавил наш точный код тестирования в свой пост выше.   -  person CraigularB    schedule 07.01.2014
comment
Это странно. Для меня первый медленнее второго для обоих тестов. Какую версию Python вы используете?   -  person Blender    schedule 07.01.2014
comment
Я немного изменил порядок вещей, чтобы тесты остались прежними. Возможно, я сделал это до того, как вы попытались что-то проверить. Если это не так, то я не уверен, что происходит!   -  person CraigularB    schedule 07.01.2014
comment
Я вижу в основном то же самое, что и ОП. Если не использовать метод функции foo, метод оператора if всегда составляет около 0,06, а метод без if — около 0,3. Когда я использую функции foo, то в этом случае метод оператора if составляет около 0,61, а метод без if — около 0,53. (Это средства использования timeit 10 раз для каждой из четырех возможностей.) Я использую довольно быстрый настольный компьютер, используя IPython с Python 2.7.3.   -  person ely    schedule 07.01.2014
comment
Я должен упомянуть, что это Python 3. Я добавлю это к вопросу, я забыл его включить.   -  person CraigularB    schedule 07.01.2014
comment
Я хотел бы отметить, что время выполнения увеличивается как с версией функции. Есть ли здесь какие-то накладные расходы на синтаксический анализ функций? Поскольку вторая функция длиннее, могут ли это объяснить большие накладные расходы на синтаксический анализ? Из любопытства попробуйте изменить свои функции так, чтобы они принимали строку в качестве параметра, а не устанавливали ее в качестве внутренней переменной, и посмотрите, что произойдет.   -  person jpmc26    schedule 07.01.2014
comment
foo2 по-прежнему занимает больше времени, хотя разница гораздо меньше (0,311 для foo против 0,326 для foo2).   -  person CraigularB    schedule 07.01.2014
comment
Хм. Теперь, когда я думаю об этом, действительно странно то, что if вообще короче. На самом деле он выполняет больше работы, чем replace, поскольку тест if является True (поэтому replace выполняется в обоих случаях). Единственный способ, которым if может сэкономить ваше время, — это если строка не содержит строку для замены, тем самым экономя ваше время на самом вызове replace, и это должно быть правдой только в том случае, если in имеет более быстрый порядок времени, чем replace. Я не знаю, почему это будет иметь большое значение, но вы используете другую строку для замены в не-функции с версией if.   -  person jpmc26    schedule 07.01.2014
comment
@ jpmc26 Я тоже использовал ту же строку. Я должен был скопировать версии, которые используют все те же строки, но даже с теми же строками версии без функций получают те же результаты. Я изменил утверждения, чтобы они использовали одну и ту же строку в вопросе.   -  person CraigularB    schedule 07.01.2014


Ответы (2)


Думаю, я понял.

Посмотрите на этот код:

timeit.timeit("""if '1' in s: s = s.replace('1','5')""", setup="s = 'hi 1 2 3'")

В этом коде setup запускается ровно один раз. Это означает, что s становится "глобальным". В результате оно изменяется на hi 5 2 3 в первой итерации, а in теперь возвращает False для всех последующих итераций.

См. этот код:

timeit.timeit("""if '1' in s: s = s.replace('1','5'); print(s)""", setup="s = 'hi 1 2 3'")

Это выведет hi 5 2 3 один раз, потому что print является частью оператора if. Сравните это, что заполнит ваш экран кучей hi 5 2 3:

timeit.timeit("""s = s.replace("1","5"); print(s)""", setup="s = 'hi 1 2 3'")

Таким образом, проблема здесь в том, что не-функция с if test ошибочна и дает вам ложные тайминги, если только повторные вызовы уже обработанной строки не являются тем, что вы пытались проверить. (Если это то, что вы пытались протестировать, ваши версии функций ошибочны.) Причина, по которой функция с if не работает лучше, заключается в том, что она запускает replace на новой копии строки для каждой итерации.

Следующий тест делает то, что, как я полагаю, вы намеревались, поскольку он не переназначает результат replace обратно на s, оставляя его неизменным для каждой итерации:

>>> timeit.timeit("""if '1' in s: s.replace('1','5')""", setup="s = 'hi 1 2 3'"
0.3221409016812231
>>> timeit.timeit("""s.replace('1','5')""", setup="s = 'hi 1 2 3'")
0.28558505721252914

Это изменение добавляет много времени к тесту if и добавляет немного времени к тесту без if для меня, но я использую Python 2.7. Однако, если результаты Python 3 непротиворечивы, эти результаты предполагают, что in экономит много времени, когда строки редко нуждаются в замене. Если они обычно требуют замены, похоже, что in требует немного времени.

person jpmc26    schedule 07.01.2014
comment
Благодарю вас! Я думаю, что это имеет смысл. На самом деле мы также пытаемся протестировать оператор присваивания (поскольку получение замененного значения и выполнение следующих операций над ним важно для того, как мы обрабатываем данные). Я выполнил следующее: >>> s = """ s = 'hi 1 2 3' if '1' in s: s = s.replace('1','5') """ >>> timeit.timeit(s) 0.26153747701027896 >>> s2 = """ s = 'hi 1 2 3' s = s.replace('1','5') """ >>> timeit.timeit(s2) 0.24757718200271484 и, похоже, это подтверждает ваши выводы и наши предыдущие выводы. Спасибо! - person CraigularB; 07.01.2014

Стало еще страннее, если посмотреть на дизассемблированный код. Второй блок имеет версию if (которая работает быстрее для меня, используя timeit, как в примере OP).

Тем не менее, глядя на коды операций, кажется, что у него есть 7 дополнительных кодов операций, начиная с первого BUILD_MAP, а также включая один дополнительный POP_JUMP_IF_TRUE (предположительно, для самой проверки оператора if). До и после этого все коды одинаковы.

Это предполагает, что построение и выполнение проверки в операторе if каким-то образом сокращает время вычислений для последующей проверки в вызове replace. Как мы можем увидеть конкретную информацию о времени для различных кодов операций?

In [55]: dis.disassemble_string("s='HI 1 2 3'; s = s.replace('1','4')")
          0 POP_JUMP_IF_TRUE 10045
          3 PRINT_NEWLINE
          4 PRINT_ITEM_TO
          5 SLICE+2
          6 <49>
          7 SLICE+2
          8 DELETE_SLICE+0
          9 SLICE+2
         10 DELETE_SLICE+1
         11 <39>
         12 INPLACE_MODULO
         13 SLICE+2
         14 POP_JUMP_IF_TRUE 15648
         17 SLICE+2
         18 POP_JUMP_IF_TRUE 29230
         21 LOAD_NAME       27760 (27760)
         24 STORE_GLOBAL    25955 (25955)
         27 STORE_SLICE+0
         28 <39>
         29 <49>
         30 <39>
         31 <44>
         32 <39>
         33 DELETE_SLICE+2
         34 <39>
         35 STORE_SLICE+1

In [56]: dis.disassemble_string("s='HI 1 2 3'; if '1' in s: s = s.replace('1','4')")
          0 POP_JUMP_IF_TRUE 10045
          3 PRINT_NEWLINE
          4 PRINT_ITEM_TO
          5 SLICE+2
          6 <49>
          7 SLICE+2
          8 DELETE_SLICE+0
          9 SLICE+2
         10 DELETE_SLICE+1
         11 <39>
         12 INPLACE_MODULO
         13 SLICE+2
         14 BUILD_MAP        8294
         17 <39>
         18 <49>
         19 <39>
         20 SLICE+2
         21 BUILD_MAP        8302
         24 POP_JUMP_IF_TRUE  8250
         27 POP_JUMP_IF_TRUE 15648
         30 SLICE+2
         31 POP_JUMP_IF_TRUE 29230
         34 LOAD_NAME       27760 (27760)
         37 STORE_GLOBAL    25955 (25955)
         40 STORE_SLICE+0
         41 <39>
         42 <49>
         43 <39>
         44 <44>
         45 <39>
         46 DELETE_SLICE+2
         47 <39>
         48 STORE_SLICE+1
person ely    schedule 06.01.2014
comment
Если я правильно понял, то единственное отличие — это вставка строк с 14 по 24 во второй набор инструкций. Это правильно? - person jpmc26; 07.01.2014
comment
Да. Но похоже, проблема заключалась в том, как функция установки использовалась с timeit, а не в самом фактическом коде. - person ely; 07.01.2014