Деление MPFR быстрее, чем собственное целочисленное деление?
Я всегда предполагал, что целочисленное деление было быстрее, чем деление с плавающей запятой, но я провел несколько тестов, которые, казалось, доказывали обратное.
import gmpy2, time, math
digits = 100000
scale = 10**digits # Decimal precision
gmpy2.get_context().precision = int(math.log2(10) * digits) # Binary precision
def start_timer():
global start_time
start_time = time.time()
def print_timer():
print("%s s" % (time.time() - start_time))
start_timer()
for i in range(1000):
x = scale // 3
print_timer()
start_timer()
for i in range(1000):
x = gmpy2.mpfr(1) / 3
print_timer()
start_timer()
for i in range(1000):
x = gmpy2.mpfr(1) / gmpy2.mpfr(3)
print_timer()
Целочисленное деление заняло 0,17 секунды, деление mpfr заняло 0,06 секунды, а деление между двумя числами с плавающей запятой - 15,56 секунды.
Мои вопросы:
- Я правильно настраиваю этот тест?
- Действительно ли деление mpfr более оптимизировано, чем нативное?
- Разве деление с использованием числа с плавающей запятой и целого числа намного быстрее, чем деление с использованием двух чисел с плавающей запятой?
3 ответа
Я использую IPython для составления коротких примеров, а затем попытаюсь объяснить результаты.
from gmpy2 import mpfr, get_context
get_context().precision=1000
a=mpfr(1);b=mpfr(3)
%timeit a/b
1000000 loops, best of 3: 669 ns per loop
%timeit a/3
1000000 loops, best of 3: 464 ns per loop
get_context().precision=10000
a=mpfr(1);b=mpfr(3)
%timeit a/b
100000 loops, best of 3: 12.9 µs per loop
%timeit a/3
1000000 loops, best of 3: 1.33 µs per loop
get_context().precision=100000
a=mpfr(1);b=mpfr(3)
%timeit a/b
1000 loops, best of 3: 505 µs per loop
%timeit a/3
100000 loops, best of 3: 8.13 µs per loop
Обратите внимание, что с увеличением точности время работы a/b
увеличивается быстрее, чем a/3
, При расчете a/b
MPFR использует полную точность обоих значений, а время выполнения (приблизительно) O(n * ln(n)). При расчете a/3
MPFR использует короткое, но точное представление 3, а время выполнения (примерно) O(n). Это объясняет почему a/b
медленнее, чем a/3
для высокой точности. (п длина a
в битах.)
Когда Python вычисляет scale//3
, он использует тот факт, что 3 будет вписываться в один digit
и время работы является линейным по длине scale
, Это фактически тот же расчет, что и a/3
но поскольку базовая библиотека GMP работает быстрее, чем Python, a/3
вычисляется быстрее чем scale//3
,
Вот краткий пример различия в производительности между Python и GMP.
from gmpy2 import mpz
scale = 10**100000
%timeit scale//3
10000 loops, best of 3: 162 µs per loop
scale = mpz(scale)
%timeit scale//3
100000 loops, best of 3: 19 µs per loop
Вы измеряли производительность между n
от n
разделение и n
от k
разделение при сравнении a/b
а также a/3
, (n
длина a
в битах, и k
намного, намного меньше, чем n
.) Когда вы сравнили scale//3
и `a/3', вы сравнивали простую, прямую реализацию деления с высокооптимизированной реализацией.
Примечание по реализации: В текущей ветке нестабильной разработки, a/3
звонки mpfr_div_ui
непосредственно. Это исключает создание временного объекта MPFR. Это улучшает производительность, как показано ниже.
from gmpy2 import mpfr, get_context
get_context().precision=1000
a=mpfr(1);b=mpfr(3)
%timeit a/b
1000000 loops, best of 3: 593 ns per loop
%timeit a/3
1000000 loops, best of 3: 231 ns per loop
get_context().precision=10000
a=mpfr(1); b=mpfr(3)
%timeit a/b
100000 loops, best of 3: 12.7 µs per loop
%timeit a/3
1000000 loops, best of 3: 927 ns per loop
get_context().precision=100000
a=mpfr(1);b=mpfr(3)
%timeit a/b
1000 loops, best of 3: 505 µs per loop
%timeit a/3
100000 loops, best of 3: 6.77 µs per loop
Замечание о реализации GNU MPFR (я разработчик MPFR, хотя я не особо работал над делением): выбор лучшего алгоритма для умножения и деления довольно сложен, потому что существуют различные параметры (точность входы и выходы, и могут ли входы быть представлены с меньшей точностью из-за конечных нулей, в частности), и некоторые случаи могут быть более сложными для округления, чем другие. Более того, алгоритмы, таким образом, время может изменяться от одного выпуска к другому, улучшая некоторые случаи, но в то же время делая другие случаи более медленными. Еще недавно (два месяца назад) у нас была дискуссия о том, нужно ли делать специальное распознавание постоянных степеней двойки для целых чисел в mpfr_mul_ui и mpfr_div_ui.
Если вы действительно хотите сравнить целочисленное деление с MPFR FP, вам следует сравнить с целочисленным делением GMP. MPFR основан на подразделении GMP, но не наивно. Лучший способ узнать, что делает MPFR, - это использовать протоколирование MPFR (для этого может потребоваться перестройка с --enable-logging
) с соответствующими переменными среды. Обратите внимание, что когда ведение журнала включено в сборке MPFR, даже если ведение журнала не используется, MPFR может быть немного медленнее.
Деление с плавающей точкой обычно быстрее, чем целочисленное деление на процессоре. Можно предположить, что это связано с тем, что FPU более оптимизирован для этой операции, или что представление с плавающей запятой облегчает деление. Но какими бы ни были причины, это не изменит факт. В конце концов, единственный способ получить конкретные ответы на ваши вторые и третьи вопросы - это проверить их. И да, ваши тесты выглядят хорошо для меня.
Если бы мне пришлось рисковать, я думаю, что случай, когда вы делите число MPFR на целое число, быстрее, потому что GMP может использовать свою ограниченную точность в своих интересах при вычислении деления.