Сколько циклов процессора занимает одно добавление?

Я хочу измерить количество тактов, необходимых для выполнения операции сложения в Python 3.

Я написал программу для вычисления среднего значения операции сложения:

from timeit import timeit

def test(n):
    for i in range(n):
      1 + 1

if __name__ == '__main__':

    times = {}
    for i in [2 ** n for n in range(10)]:
      t = timeit.timeit("test(%d)" % i, setup="from __main__ import test", number=100000)
      times[i] = t
      print("%d additions takes %f" % (i, t))

    keys = sorted(list(times.keys()))

    for i in range(len(keys) - 2):
      print("1 addition takes %f" % ((times[keys[i+1]] - times[keys[i]]) / (keys[i+1] - keys[i])))

Вывод:

16 additions takes 0.288647
32 additions takes 0.422229
64 additions takes 0.712617
128 additions takes 1.275438
256 additions takes 2.415222
512 additions takes 5.050155
1024 additions takes 10.381530
2048 additions takes 21.185604
4096 additions takes 43.122559
8192 additions takes 88.323853
16384 additions takes 194.353927
1  addition takes 0.008292
1 addition takes 0.010068
1 addition takes 0.008654
1 addition takes 0.010318
1 addition takes 0.008349
1 addition takes 0.009075
1 addition takes 0.008794
1 addition takes 0.008905
1 addition takes 0.010293
1 addition takes 0.010413
1 addition takes 0.010551
1 addition takes 0.010711
1 addition takes 0.011035

Таким образом, в соответствии с этим выводом одно добавление занимает примерно 0,0095 мкс. Следуя инструкциям этой страницы, я подсчитал, что одно добавление занимает 25 циклов ЦП. . Является ли это нормальным значением и почему? Потому что ассемблерная инструкция ADD занимает всего 1-2 такта процессора.


person Arsen    schedule 31.03.2016    source источник
comment
Кроме того, добавление даже не происходит, потому что оно оптимизируется.   -  person user2357112 supports Monica    schedule 31.03.2016
comment
в порядке. но если я запускаю более 1000 дополнений, я думаю, это не повлияет   -  person Arsen    schedule 31.03.2016
comment
Цикл for внутри test определенно влияет на время независимо от количества итераций.   -  person Michael    schedule 31.03.2016
comment
Ваш код для расчета времени для одной итерации цикла (1 добавление) неверен. Должно быть times[keys[i]] / keys[i]. Ваш расчет того, сколько циклов соответствует 0,0095 секунды, также неверен. ЦП, работающий на частоте 2 ГГц, выполняет 19 000 000 циклов за 0,0095 секунды. Python медленный, очень медленный. Его операции занимают на несколько порядков больше времени, чем эквивалентные инструкции по сборке.   -  person Ross Ridge    schedule 31.03.2016
comment
@ross, о, это просто опечатка. Я имею в виду usec вместо sec.((times[keys[i+1]] - times[keys[i]]) / (keys[i+1] - keys[i])) этот фрагмент кода не является неправильным. Разделив дельты, я могу настроить точность вычислений.   -  person Arsen    schedule 31.03.2016


Ответы (2)


Вы синхронизируете вызов функции (test()), цикл for и вызов range(). Добавление не рассчитано по времени.

def test(n):
    for i in range(n):
        1 + 1

import dis
dis.dis(test)

Вот байтовый код для вашей тестовой функции (не включает вызов test()):

  2           0 SETUP_LOOP              24 (to 27)
              3 LOAD_GLOBAL              0 (range)
              6 LOAD_FAST                0 (n)
              9 CALL_FUNCTION            1
             12 GET_ITER            
        >>   13 FOR_ITER                10 (to 26)
             16 STORE_FAST               1 (i)

  3          19 LOAD_CONST               2 (2)   **** 
             22 POP_TOP             
             23 JUMP_ABSOLUTE           13
        >>   26 POP_BLOCK           
        >>   27 LOAD_CONST               0 (None)
             30 RETURN_VALUE        

**** Обратите внимание, добавление выполняется во время компиляции. Довольно много других языков и их компиляторов будут делать это, включая C. Однако стандарты редко определяют, когда на самом деле выполняется 1 + 1, поэтому это часто зависит от реализации.

ИЗМЕНИТЬ:

Ваш вызов функции timeit может быть таким:

    t = timeit("x += 1", setup="x = 1", number=100000)

Мы можем создать фиктивную функцию для проверки операции:

def myfunc(x):
    x += 1

import dis
dis.dis(myfunc)

Внесение этого изменения дает:

1 additions takes 0.008976
2 additions takes 0.007419
4 additions takes 0.007282
8 additions takes 0.007693
16 additions takes 0.007026
32 additions takes 0.007793
64 additions takes 0.010168
128 additions takes 0.008124
256 additions takes 0.009064
512 additions takes 0.007256
1 addition takes -0.001557
1 addition takes -0.000068
1 addition takes 0.000103
1 addition takes -0.000083
1 addition takes 0.000048
1 addition takes 0.000074
1 addition takes -0.000032
1 addition takes 0.000007

 26           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (1)
              6 INPLACE_ADD
              7 STORE_FAST               0 (x)
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE

Обратите внимание, что x += 1 — это INPLACE_ADD, в отличие от x = x + 1, который является BINARY_ADD. Поэтому вам нужно решить, какой OPCode вы хотите измерить.

person cdarke    schedule 31.03.2016
comment
Спасибо, теперь мне стало ясно. Но есть ли способы более корректно рассчитать время сложения? - person Arsen; 31.03.2016
comment
Чего вы пытаетесь достичь? Когда вы вызываете + на объектно-ориентированном языке высокого уровня, вы добавляете два объекта вместе, а + включает вызов функции. Некоторые языки, такие как Java и C++, используют так называемые примитивы для целых чисел — они вообще не являются объектами. В python вы получаете значительную гибкость. Модули Python с высокой плотностью обработки чисел, такие как numpy, выполняют свои вычисления в расширениях C, а не в собственном Python, в основном из соображений производительности. Какова цель ваших измерений? - person cdarke; 31.03.2016
comment
Вы также должны понимать, что Python выполняет и другие оптимизации. Если вы пишете такой низкоуровневый тест, вы должны убедиться, что байт-код действительно делает то, что вы ожидаете. - person cdarke; 31.03.2016
comment
См. мой РЕДАКТИРОВАТЬ для предложения, со всеми предостережениями, приведенными в моих комментариях. - person cdarke; 31.03.2016

Вы можете получить немного больше информации о том, что происходит за кулисами здесь, используя модуль dis.

В частности, функция dis.dis берет фрагмент скомпилированного кода Python и возвращает байтовый код, как этот фрагмент интерпретируется. В случае 1 + 1:

In [1]: import dis

In [2]: def add1and1():
    return 1 + 1

In [3]: dis.dis(add1and1)
  2           0 LOAD_CONST               2 (2)
              3 RETURN_VALUE 

Таким образом, в этом случае, когда исходный код компилируется в байтовый код, операция 1 + 1 выполняется только один раз, а затем результат сохраняется как константа. Мы можем обойти это, возвращая сумму параметров, которые передаются в функцию:

In [1]: import dis

In [2]: def add(x, y):
    return x + y

In [3]: dis.dis(add)
      2           0 LOAD_FAST                0 (x)
              3 LOAD_FAST                1 (y)
              6 BINARY_ADD          
              7 RETURN_VALUE          

Таким образом, инструкция байт-кода, которая вас действительно интересует, это BINARY_ADD. Если вы хотите узнать об этом больше, вы можете найти соответствующий раздел в файле ceval.c интерпретатора CPython (здесь):

TARGET(BINARY_ADD) {
    PyObject *right = POP();
    PyObject *left = TOP();
    PyObject *sum;
    if (PyUnicode_CheckExact(left) &&
             PyUnicode_CheckExact(right)) {
        sum = unicode_concatenate(left, right, f, next_instr);
        /* unicode_concatenate consumed the ref to v */
    }
    else {
        sum = PyNumber_Add(left, right);
        Py_DECREF(left);
    }
    Py_DECREF(right);
    SET_TOP(sum);
    if (sum == NULL)
        goto error;
    DISPATCH();
}

Так что здесь происходит больше, чем вы могли изначально ожидать. У нас есть:

  1. условие, чтобы определить, используем ли мы BINARY_ADD для конкатенации строк или для добавления числовых типов

  2. фактический вызов PyNumber_Add там, где можно было ожидать чего-то большего, вроде left + right

Оба эти момента объясняются динамической природой Python; поскольку Python не знает тип x или y до тех пор, пока вы не вызовете add, проверка типа выполняется во время выполнения, а не во время компиляции. Чтобы обойти это, в динамических языках можно провести разумную оптимизацию (см. V8 для JavaScript или PyPy для Python), но, вообще говоря, это цена, которую вы платите за гибкость интерпретируемого языка с динамической типизацией.

person chucksmash    schedule 31.03.2016