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

Разве divmod() быстрее, чем с помощью операторов% и//?

Я помню из сборки, что целые команды деления дают как фактор, так и остаток. Таким образом, в python встроенная функция divmod() будет лучше по производительности, чем использование операторов% и//(предположим, конечно, что нужно как частное, так и остальное)?

q, r = divmod(n, d)
q, r = (n // d, n % d)
4b9b3361

Ответ 1

Чтобы измерить, нужно знать (все тайминги на Macbook Pro 2.8Ghz i7):

>>> import sys, timeit
>>> sys.version_info
sys.version_info(major=2, minor=7, micro=12, releaselevel='final', serial=0)
>>> timeit.timeit('divmod(n, d)', 'n, d = 42, 7')
0.1473848819732666
>>> timeit.timeit('n // d, n % d', 'n, d = 42, 7')
0.10324406623840332

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

>>> timeit.timeit('dm(n, d)', 'n, d = 42, 7; dm = divmod')
0.13460898399353027

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

>>> import dis
>>> dis.dis(compile('divmod(n, d)', '', 'exec'))
  1           0 LOAD_NAME                0 (divmod)
              3 LOAD_NAME                1 (n)
              6 LOAD_NAME                2 (d)
              9 CALL_FUNCTION            2
             12 POP_TOP             
             13 LOAD_CONST               0 (None)
             16 RETURN_VALUE        
>>> dis.dis(compile('(n // d, n % d)', '', 'exec'))
  1           0 LOAD_NAME                0 (n)
              3 LOAD_NAME                1 (d)
              6 BINARY_FLOOR_DIVIDE 
              7 LOAD_NAME                0 (n)
             10 LOAD_NAME                1 (d)
             13 BINARY_MODULO       
             14 BUILD_TUPLE              2
             17 POP_TOP             
             18 LOAD_CONST               0 (None)
             21 RETURN_VALUE        

Варианты // и % используют больше кодов операций, но байт-код CALL_FUNCTION является медвежьим, производительность мудрая.


В PyPy для малых целых чисел на самом деле нет большой разницы; небольшое преимущество в скорости, которое коды кодов ускоряют при чистой скорости целочисленной арифметики C:

>>>> import platform, sys, timeit
>>>> platform.python_implementation(), sys.version_info
('PyPy', (major=2, minor=7, micro=10, releaselevel='final', serial=42))
>>>> timeit.timeit('divmod(n, d)', 'n, d = 42, 7', number=10**9)
0.5659301280975342
>>>> timeit.timeit('n // d, n % d', 'n, d = 42, 7', number=10**9)
0.5471200942993164

(Мне пришлось перекрутить количество повторений до 1 миллиарда, чтобы показать, насколько мала разница, PyPy здесь невероятно быстро).

Однако, когда числа становятся большими, divmod() выигрывает по сельской миле:

>>>> timeit.timeit('divmod(n, d)', 'n, d = 2**74207281 - 1, 26', number=100)
17.620037078857422
>>>> timeit.timeit('n // d, n % d', 'n, d = 2**74207281 - 1, 26', number=100)
34.44323515892029

(Теперь мне пришлось настроить количество повторений в 10 раз по сравнению с числами hobbs, чтобы получить результат за разумное количество времени).

Это связано с тем, что PyPy больше не может распаковать эти целые числа в виде целых чисел C; вы можете увидеть поразительную разницу в таймингах между использованием sys.maxint и sys.maxint + 1:

>>>> timeit.timeit('divmod(n, d)', 'import sys; n, d = sys.maxint, 26', number=10**7)
0.008622884750366211
>>>> timeit.timeit('n // d, n % d', 'import sys; n, d = sys.maxint, 26', number=10**7)
0.007693052291870117
>>>> timeit.timeit('divmod(n, d)', 'import sys; n, d = sys.maxint + 1, 26', number=10**7)
0.8396248817443848
>>>> timeit.timeit('n // d, n % d', 'import sys; n, d = sys.maxint + 1, 26', number=10**7)
1.0117690563201904

Ответ 2

Ответ Martijn правильный, если вы используете "маленькие" нативные целые числа, где арифметические операции очень быстрые по сравнению с вызовами функций. Однако, с bigints, это совершенно другая история:

>>> import timeit
>>> timeit.timeit('divmod(n, d)', 'n, d = 2**74207281 - 1, 26', number=1000)
24.22666597366333
>>> timeit.timeit('n // d, n % d', 'n, d = 2**74207281 - 1, 26', number=1000)
49.517399072647095

при делении 22-миллионного числа divmod почти в два раза быстрее, чем разделение и модуль отдельно, как и следовало ожидать.

На моей машине кроссовер происходит где-то около 2 ^ 63, но не смирись с этим. Как говорит Мартин, измерьте! Когда производительность действительно имеет значение, не предполагайте, что то, что было верно в одном месте, по-прежнему будет истинным в другом.