Самый быстрый способ округлить случайные числа в python

Я хочу генерировать случайные числа с определенной точностью, по одному (поэтому я не ищу векторизованное решение).

Я нашел метод в этом QnA на stackoverflow, и он дал мне эти тесты, как и было обещано. Метод определенно почти в два раза быстрее. Теперь вот что меня озадачивает.

%timeit int(0.5192853551955484*(10**5)+0.5)/(10.**5)      #> 149 ns ± 5.76 ns per loop 
%timeit round(0.5192853551955484, 5)                      #> 432 ns ± 11.7 ns per loop
## Faster as expected

fl = random.random()
pr = 5
%timeit int(fl*(10**pr)+0.5)/(10.**pr)                    #> 613 ns ± 27.9 ns per loop 
%timeit round(fl, pr)                                     #> 444 ns ± 9.25 ns per loop
## Slower?!

%timeit int(random.random()*(10**5)+0.5)/(10.**5)         #> 280 ns ± 29.3 ns per loop 
%timeit round(random.random(), 5)                         #> 538 ns ± 17.5 ns per loop
## Faster than using a variable even though it has the overhead
## of creating a random number for each call?

Почему описанный выше метод работает медленнее, когда я использую переменные? Он восстанавливает потерянную скорость, когда я напрямую передаю случайно сгенерированное число. Что мне не хватает?

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

def rounder(fl, pr):
    p = float(10**pr)
    return int(fl * p + 0.5)/p

%timeit rounder(random.random(), 5)                       #> 707 ns ± 14.2 ns per loop 
%timeit round(random.random(), 5)                         #> 525 ns ± 22.1 ns per loop

## Having a global variable does make it faster by not having to do the 10**5 everytime
p = float(10**5)
def my_round_5(fl):
    return int(fl* p + 0.5)/p

%timeit my_round_5(random.random())                       #> 369 ns ± 18.9 ns per loop

Я бы предпочел не иметь в своем коде глобальной переменной, но, допустим, я уступаю этому требованию, но выигрыш в производительности меньше, чем при использовании формулы без переменных.

Итак, последний вопрос будет заключаться в том, какой метод будет наиболее полезным для меня? При переключении на функцию, требующую глобальной переменной, выигрыш составляет всего 100-150 нс. Или есть что-то, что может быть быстрее.

Ну, функция rounder, которую вы указали, не совсем такая же, как описанная в ссылке, которую вы предоставили...

Dani Mesejo 15.12.2020 09:22

@DaniMesejo Привет, Дэни, извини, я упустил одну тонкость. Я сделал то же самое сейчас, но проблема все еще сохраняется.

Rithwik 15.12.2020 09:23

можно ли использовать генератор? то есть что-то, что yield является единственным значением каждый раз, это было бы хорошим местом для предварительного расчета p

Sam Mason 15.12.2020 09:42

@SamMason На самом деле это звучит как хороший способ решить проблему. Я попробую. Можете ли вы добавить код для него и сравнить его с вышеуказанным методом и добавить его в свое решение. Тогда ваш ответ будет полным ответом на мой вопрос, так что я смогу отметить его и, надеюсь, двигаться дальше. :D

Rithwik 15.12.2020 09:45
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
4
630
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Байт-компилятор Python «знает», как работают числа, и использует это знание для оптимизации вещей там, где это возможно. Вы можете использовать модуль dis, чтобы увидеть, что происходит.

Например, ваш первый «быстрый» пример:

from dis import dis

def fn():
    return int(0.5192853551955484*(10**5)+0.5)/(10.**5)

dis(fn)

на самом деле делает:

  2           0 LOAD_GLOBAL              0 (int)
              2 LOAD_CONST               1 (51929.03551955484)
              4 CALL_FUNCTION            1
              6 LOAD_CONST               2 (100000.0)
              8 BINARY_TRUE_DIVIDE
             10 RETURN_VALUE

то есть он знает, что оценивает 0.5192853551955484*(10**5)+0.5, и делает это при компиляции байт-кода. Если у вас есть pr в качестве параметра, он не может этого сделать, поэтому при запуске кода приходится выполнять больше работы.

Чтобы ответить на вопрос «что было бы лучше», может быть что-то вроде:

def fn(pr):
     # cache to prevent global lookup
     rng = random.random
     # evaluate precision once
     p = 10. ** pr
     # generate infinite stream of random numbers
     while True:
         yield int(rng() * p + 0.5) / p

можно сравнить с:

x = fn(5)
%timeit next(x)

давая ~ 160 нс за цикл, в то время как:

%timeit int(fl*(10**pr)+0.5)/(10**pr)
%timeit int(fl*(10.**pr)+0.5)/(10.**pr)

работать в ~ 500 и ~ 250 нс. Не понимал, что возведение в степень int и float настолько отличается!

Я понимаю. Так это кеширование значения после первого вызова?

Rithwik 15.12.2020 09:43

до первого звонка! интерпретатор Python оценивает только «байт-код», оптимизация выполняется в процессе преобразования исходного кода в байт-код.

Sam Mason 15.12.2020 09:46

Хорошо. Я не знал ни об этом, ни о модуле dis. Это будет удобно в дальнейшем. Спасибо!

Rithwik 15.12.2020 09:47

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

Rithwik 15.12.2020 10:03

Еще один вопрос по rng = random.random. # cache to prevent global lookup, значит ли это, что этот генератор случайных чисел является локальным и имеет начальное число, отличное от других вызовов random.random(), которые могут быть в моем коде? Если я заменю rng() на random.random(), какие последствия это будет иметь?

Rithwik 15.12.2020 10:06

нет, просто кеширование поиска функции. просто доступ к локальным переменным (немного) быстрее, чем к глобальным переменным. когда вы используете random.random(), вы делаете три вещи: 1. ищете модуль random, 2. ищете функцию random, 3. вызываете функцию (см. вывод dis). мы просто кэшируем первые два шага в (более быструю) локальную переменную. это быстрее, потому что локальные переменные - это просто целочисленный индекс, а не поиск по хэшу, используемый для глобальных переменных

Sam Mason 15.12.2020 10:42

Другие вопросы по теме