Стоимость сравнения строк против int и bool - PullRequest
0 голосов
/ 16 апреля 2020

При определении функций с параметрами нам иногда приходится искать компромисс между удобочитаемостью и скоростью. Вот три примера сравнения str с int и bool:

def f1(mode, x, y):          # the most "explicit" solution, best for readability, ... but uses a str comparison
    if mode == 'mode1_method_foo':
        return 0             # IRL, many lines here
    elif mode == 'mode2_method_bar':
        return x ** 12       # IRL, many lines here too

def f2(mode, x, y):          # with int comparison
    if mode == 1:  
        return 0
    elif mode == 2:
        return x ** 12

def f3(mode, x, y):          # with bool
    if mode:
        return 0
    else:
        return x ** 12

Удивительно, но стоимость этого сравнения кажется незначительной:

import time, random
start = time.time()
for i in range(1000*1000):
    x = random.random()
    y = random.random()
    #f1('mode2_method_bar', x, y)  # 0.760 sec
    #f2(2, x, y)                   # 0.700 sec
    f3(False, x, y)                # 0.644 sec
print(time.time() - start)

Это не очень чисто способ измерения стоимости производительности.

Как лучше измерить стоимость использования строкового параметра по сравнению с int или bool?

Пример: если мой программа выполняет 10 000 таких сравнений в секунду, сколько времени я теряю, используя f1 вместо f3?

Контекст: использование строк для именования методов часто используется в различных API / библиотеках, например : https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html: method='Nelder-Mead' и др. c. В случае этой функции Сципи нет проблем с производительностью, потому что стоимость сравнения строк на много порядков меньше , чем стоимость того, что на самом деле делает функция; Тем не менее, эта проблема может быть интересна для других функций.

1 Ответ

0 голосов
/ 16 апреля 2020

Как правило, при измерении (и сравнении) производительности очень важно точно измерить интересующую вас часть.

Давайте предположим, что мы хотим измерить и сравнить производительность по 3 вещам ( a , b и c). Теперь давайте присвоим каждому (теоретический) показатель производительности:

  • a : 1
  • b : 4
  • c : 8

Как и в тесте производительности , мы начнем с предпосылки, что чем ниже балл, тем лучше (производительность).

В этом случае вышеупомянутые баллы ( 1, 4 , 8 - также называемые абсолютные баллы ) довольно актуальны и значимы для всех.

Но давайте также посчитаем относительные баллы : один из способов - выбрать вариант, который работает хуже всего. (c) и express в процентах относительно него:

  • a : 1 / 8 = 0,125
  • b : 4 / 8 = 0,500
  • c: 8 / 8 = 1.000

Пока все хорошо. Абсолютный и относительный баллы являются явно пропорциональными.

Но в некоторых случаях в реальном мире точно измерить какой-то конкретный элемент c сложно (практически невозможно, если принять во внимание факторы извне программы). В этих случаях элемент + некоторые накладные расходы измеряются вместе, и окончательная оценка составляет сумма (не обязательно арифметическое сложение) баллов по элементу и накладным расходам. Цель здесь состоит в том, чтобы накладные расходы были как можно меньшими, чтобы уменьшить их влияние (вес) на общий балл.
Например, давайте возьмем огромные накладные расходы (назовите это d ) со счетом:

  • d : 2

Вышеуказанные измерения (включая накладные расходы):

  • a ( a + d ): 3
  • a ( b + d ): 6
  • c (c + d ): 10

Все выглядит немного иначе. Теперь относительные оценки:

  • a : (1 + 2) / (8 + 2) = 0,333 ...
  • b : (4 + 2) / (8 + 2) = 0,600
  • c: (8 + 2) / (8 + 2) = 1.000

Как видно, a Относительная оценка почти в три раза (на самом деле это 2. (6) раз выше) предыдущего значения (без накладных расходов).

То же самое происходит в вашем случае. Расчет 2 ** 12 является накладным. Я не говорю, что измерения неверны, но они также не точны. Что точно, так это то, что если один элемент работает лучше, чем другой без дополнительных затрат, он также будет работать лучше с дополнительными издержками, но, как я уже сказал, сравнение между ними даст неверные результаты.

Я изменил ваш код и добавил Еще 3 функции, где я просто избавился от возведения в степень.

code00.py :

#!/usr/bin/env python

import sys
import timeit
import random


def f0(mode, x, y):          # the most "explicit" solution, best for readability, ... but uses a str comparison
    if mode == 'mode1_method_foo':
        return 0             # IRL, many lines here
    elif mode == 'mode2_method_bar':
        return x ** 12       # IRL, many lines here too


def f1(mode, x, y):          # with int comparison
    if mode == 1:  
        return 0
    elif mode == 2:
        return x ** 12


def f2(mode, x, y):          # with bool
    if mode:
        return 0
    else:
        return x ** 12


# The modified versions
def f3(mode, x, y):          # the most "explicit" solution, best for readability, ... but uses a str comparison
    if mode == 'mode1_method_foo':
        return 0             # IRL, many lines here
    elif mode == 'mode2_method_bar':
        return 1       # IRL, many lines here too


def f4(mode, x, y):          # with int comparison
    if mode == 1:  
        return 0
    elif mode == 2:
        return 1


def f5(mode, x, y):          # with bool
    if mode:
        return 0
    else:
        return 1


x = random.random()
y = random.random()
args = ["mode2_method_bar", 2, False]


def main(*argv):
    results = {}
    funcs = [
        f0,
        f1,
        f2,
        f3,
        f4,
        f5,
    ]

    print("Testing functions")
    for i, func in enumerate(funcs):
        t = timeit.timeit(stmt="func(arg, x, y)", setup="from __main__ import {0:s} as func, x, y, args;arg=args[int(func.__name__[-1]) % 3]".format(func.__name__), number=10000000)
        results.setdefault(i // 3, []).append((func.__name__, t))
        print("  Done with {0:s}".format(func.__name__))
    print("\n  Functions absolute scores (seconds)")
    for k in results:
        for result in results[k]:
            print("    {0:s}: {1:.6f}".format(*result))
    print("\n  Functions relative scores (percents - compared to the variant that took longest)")
    for k, v in results.items():
        print("    Batch {0:d}".format(k))
        longest = max(v, key=lambda x: x[-1])[-1]
        for result in v:
            print("    {0:s}: {1:.6f}".format(result[0], result[1] / longest))


if __name__ == "__main__":
    print("Python {0:s} {1:d}bit on {2:s}\n".format(" ".join(item.strip() for item in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
    main(*sys.argv[1:])
    print("\nDone.")

Вывод :

[cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q061250859]> "e:\Work\Dev\VEnvs\py_pc064_03.07.06_test0\Scripts\python.exe" code00.py
Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] 64bit on win32

Testing functions
  Done with f0
  Done with f1
  Done with f2
  Done with f3
  Done with f4
  Done with f5

  Functions absolute scores (seconds)
    f0: 3.833778
    f1: 3.591715
    f2: 3.083926
    f3: 1.671274
    f4: 1.467826
    f5: 1.118479

  Functions relative scores (percents - compared to the variant that took longest)
    Batch 0
    f0: 1.000000
    f1: 0.936861
    f2: 0.804409
    Batch 1
    f3: 1.000000
    f4: 0.878268
    f5: 0.669238

Done.

[cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q061250859]> "e:\Work\Dev\VEnvs\py_pc032_02.07.17_test0\Scripts\python.exe" code00.py
Python 2.7.17 (v2.7.17:c2f86d86e6, Oct 19 2019, 20:49:36) [MSC v.1500 32 bit (Intel)] 32bit on win32

Testing functions
  Done with f0
  Done with f1
  Done with f2
  Done with f3
  Done with f4
  Done with f5

  Functions absolute scores (seconds)
    f0: 3.947840
    f1: 3.613213
    f2: 3.384385
    f3: 1.898074
    f4: 1.604591
    f5: 1.315465

  Functions relative scores (percents - compared to the variant that took longest)
    Batch 0
    f0: 1.000000
    f1: 0.915238
    f2: 0.857275
    Batch 1
    f3: 1.000000
    f4: 0.845379
    f5: 0.693053

Done.

Примечания :

  • Я запустил программу с обоими Python 2 и Python 3 (и, как видно, последний имеет некоторые улучшения скорости)
  • Ключевым моментом здесь является снижение относительных баллов с Пакет 0 (исходные функции) до Пакет 1 (исходные функции без 2 ** 12 - означает меньшую нагрузку)
  • Результаты могут отличаться между запусками (из-за внешних факторов: например, OS планирование процесса на CPU ), так что есть идея это максимально приближено к реальности. Необходимо выполнить несколько тестов

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

...