Почему log2 и log1p работают намного быстрее, чем log и log10?

Во время игры этот вопрос я заметил, что я не мог объяснить относительную производительность np.log2, np.log и np.log10:

In [1]: %%timeit x = np.random.rand(100000)
   ....: np.log2(x)
   ....: 
1000 loops, best of 3: 1.31 ms per loop

In [2]: %%timeit x = np.random.rand(100000)
np.log(x)
   ....: 
100 loops, best of 3: 3.64 ms per loop

In [3]: %%timeit x = np.random.rand(100000)
np.log10(x)
   ....: 
100 loops, best of 3: 3.93 ms per loop

np.log2 примерно в 3 раза быстрее, чем np.log и np.log10. Возможно, еще более интуитивно, np.log1p(x), который вычисляет ln (x + 1), находится на одном уровне с np.log2:

In [4]: %%timeit x = np.random.rand(100000)
np.log1p(x)
   ....: 
1000 loops, best of 3: 1.46 ms per loop

Я получил почти одинаковые тайминги в numpy v1.10.1 и v1.8.2.

Есть ли интуитивное объяснение этих несоответствий в производительности во время выполнения?

Ответы

Ответ 1

Это всего лишь примечание, но больше, чем комментарий. По-видимому, это связано с вашей конкретной установкой:

import numpy as np
import numexpr as ne
x = np.random.rand(100000)

Я получаю одинаковые тайминги с numpy 1.10 из conda и версию, скомпилированную с icc:

%timeit np.log2(x)
1000 loops, best of 3: 1.24 ms per loop

%timeit np.log(x)
1000 loops, best of 3: 1.28 ms per loop

Я подумал, что у него может быть что-то с захватом пакета MML VML, но выглядит так: нет:

%timeit ne.evaluate('log(x)')
1000 loops, best of 3: 218 µs per loop

Похоже, ваша установка numpy захватывает реализацию log/log2 из двух разных мест, которые являются нечетными.

Ответ 2

Я нашел ваш вопрос чрезвычайно интересным, поэтому я потратил несколько часов на то, чтобы сделать немного ресерах; Я думаю, что нашел объяснение разницы в производительности , поскольку это применимо к целям (спасибо Matteo Italia за вашу заметку) - Непонятно как это рассуждение может быть расширено до плавания:

Компьютеры используют базу 2. Согласно статьям, связанным в ссылке, вычисление log2 представляет собой процесс с четырьмя процессорными циклами - для log10 требуется умножить log2 (val) на 1/log2 (10), который добавляет еще 5 циклов.

Поиск log2 - это вопрос поиск индекса наименее значимого бита значения. (видео примерно на 23-й минуте).

бит-хаки: Найти целочисленную базу данных 10 целых чисел

бит-хаки: Найти базу данных 2 из N-разрядного целого числа в O (lg (N))

Целочисленная базовая база 10 вычисляется сначала с использованием одной из методы, описанные выше для нахождения базы логов 2. В силу соотношения log10 (v) = log2 (v)/log2 (10), нам нужно умножить его на 1/log2 (10), который составляет приблизительно 1233/4096, или 1233, за которым следует правая смена 12. Добавление одного из них необходимо, потому что IntegerLogBase2 округляется. Наконец, поскольку значение t является только приближением, которое может быть отключено на один, точное значение получается путем вычитания результата v < PowersOf10 [т].

Этот метод выполняет еще 6 операций, чем IntegerLogBase2. Это может быть ускоряется (на машинах с быстрым доступом к памяти), изменяя журнал base 2 table-lookup выше, чтобы записи содержали то, что вычисленный для t (т.е. pre-add, -mulitply и -shift). Делать это потребуется всего 9 операций для поиска базы 10 базы данных, предполагая, что использовались 4 таблицы (по одному для каждого байта v).

Примечание: использование методов поиска последовательностей DeBruijn и методов смещения бит для вычисления log2 в этом MIT video: Lec 2 | MIT 6.172 Performance Engineering of Software Systems, осень 2010 г. (видео на отметке 36 минут).

Обратите внимание, что это сообщение StackOverflow, которое демонстрирует метод, позволяющий эффективно выполнять вычисления log2 по-настоящему перекрестно с платформой С++

Предостережение: я не проверял исходный код numpy, чтобы убедиться, что он действительно реализует подобные методы, но было бы удивительно, что это не так. Фактически, из комментариев в сообщении OP, Fermion Portal, были отмечены:

На самом деле numpy использует math.h от glibc, вы увидите ту же разницу в C/С++, если вы используете math.h/cmath.h. Вы можете найти красиво прокомментированный исходный код для двух функций, например. ic.unicamp.br/~islene/2s2008-mo806/libc/sysdeps/ieee754/dbl-64/... и ic.unicamp.br/~islene/2s2008-mo806/libc/sysdeps/ieee754/dbl-64/... - Fermion Portal [9]

Ответ 3

Отказ от ответственности: я не являюсь ни авторитетным, ни официальным источником.

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

В большинстве случаев numpy использует math.h от glibc, вы увидите ту же самую разницу в C/С++, если используете math.h/cmath.h. В комментариях некоторые люди наблюдают одинаковые скорости для np.log и np.log2; Я подозреваю, что это может происходить из разных сборок/платформ.

Вы можете найти хорошо прокомментированный исходный код для двух функций в файлах e_log.c, e_log2.c, e_logf.c, e_log2f.c в подкаталогах dbl-64/ и flt-32/ это репозиторий GitHub.

Для двойной точности в glibc функция log реализует совершенно другой алгоритм (по сравнению с log2) от IBM с ~ 2001 года, который был включен в их библиотеку libultim. В то время как log2 принадлежит Sun Microsystems с 1993 года. Просто глядя на код, можно увидеть два разных приближения. Напротив, для единственной точности обе функции log и log2 одинаковы, кроме деления на ln2 в случае log2, следовательно, с той же скоростью.

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