Как заставить pow (float, int) возвращать float
Перегруженная функция float pow(float base, int iexp )
была удалена в С++ 11, и теперь pow
возвращает a double
. В моей программе я вычисляю много таких (в одной точности), и меня интересует наиболее эффективный способ его выполнения.
Есть ли какая-либо специальная функция (в стандартных библиотеках или любой другой) с указанной сигнатурой?
Если нет, лучше ли (с точки зрения производительности в одинарной точности) явно приводить результат pow
в float
перед любыми другими операциями (которые будут отдавать все остальное в double
) или лить iexp
в float
и использовать перегруженную функцию float pow(float base, float exp)
?
EDIT: зачем мне нужно float
и не использовать double
?
В первую очередь это ОЗУ - мне нужны десятки или сотни ГБ, поэтому это сокращение является огромным преимуществом. Поэтому мне нужно от float
получить float
. И теперь мне нужен самый эффективный способ добиться этого (меньше прикладов, используйте уже оптимизирующие алгоритмы и т.д.).
Ответы
Ответ 1
Вы можете легко написать свой собственный fpow
с помощью возведения в степень возведения в квадрат.
float my_fpow(float base, unsigned exp)
{
float result = 1.f;
while (exp)
{
if (exp & 1)
result *= base;
exp >>= 1;
base *= base;
}
return result;
}
Расточная часть:
Этот алгоритм дает лучшую точность, которая может быть заархивирована с типом float
, когда | base | > 1
Доказательство:
Предположим, что мы хотим вычислить pow(a, n)
, где a
является базовым, а n
- показателем.
Пусть определите b 1= a 1 b 2= a 2 b 3= a 4 b 4= a 8 и т.д.
Тогда a n является произведением над всем таким b i, где бит я th устанавливается в n.
Итак, мы упорядочили множество B = {b k1, b k1,..., b kn} и для любого j бит k j устанавливается в n.
Для минимизации минимизации округления можно использовать следующий очевидный алгоритм A:
- Если B содержит один элемент, то это результат
- Выберем два элемента p и q из B с минимальным модулем
- Удалите их из B
- Рассчитать произведение s = p * q и поместить его в B
- Перейдите на первый шаг
Теперь давайте докажем, что элементы в B можно просто умножать слева направо без потери точности. Это связано с тем, что:
b j > b 1 * b 2 *... * b j-1
потому что b j= b j-1 * b j-1= b j-1 * б <югу > J-2суб > * б <югу > J-2суб > =... = Ь <югу > J-1суб > * б <югу > J-2суб > *... * b 1 * b 1
Так как b 1= a 1= a и по модулю более одного:
b j > b 1 * b 2 *... * b j-1
Следовательно, мы можем заключить, что при умножении слева направо переменная аккумулятора меньше любого элемента из B.
Тогда выражение result *= base;
(за исключением самой первой итерации, конечно) делает умножение двух минимальных чисел из B, поэтому ошибка округления минимальна. Итак, в коде используется алгоритм A.
Ответ 2
Если вы настроите GCC, вы можете попробовать
float __builtin_powif(float, int)
Я не знаю, насколько это сложно.
Ответ 3
Другой вопрос, на который можно честно ответить "неправильным вопросом". Или, по крайней мере: "Ты действительно хочешь пойти туда?". float
теоретически необходимо ca. На 80% меньше места для хранения (для такого же количества циклов), и поэтому для массовой обработки может быть намного дешевле. Графические процессоры любят float
по этой причине.
Однако, посмотрите на x86 (по общему признанию, вы не сказали, в какой архитектуре вы находитесь, поэтому я выбрал наиболее распространенные). Цена в купе уже оплачена. Вы буквально ничего не получаете, используя float
для вычислений. Фактически, вы можете даже потерять пропускную способность, потому что требуются дополнительные расширения от float
до double
и дополнительное округление до промежуточной float
точности. Другими словами, вы платите дополнительно, чтобы иметь менее точный результат. Это, как правило, можно избежать, за исключением, может быть, когда вам нужна максимальная совместимость с какой-либо другой программой.
См. также комментарий Йенса. Эти параметры дают компилятору разрешение игнорировать некоторые языковые правила для достижения более высокой производительности. Излишне говорить, что это может иногда иметь неприятные последствия.
Существует два сценария, в которых float
может быть более эффективным, на x86:
- GPU (в том числе GPGPU), на самом деле многие графические процессоры даже не поддерживают
double
, и если они это делают, это обычно намного медленнее. Тем не менее, вы заметите, когда делаете очень много расчетов такого рода.
- CPU SIMD aka vectorization
Вы знаете, что вы сделали GPGPU. Явная векторизация с использованием встроенных инсайдеров компилятора - это также выбор, который вы можете сделать наверняка, но для этого требуется довольно анализ затрат и результатов. Возможно, ваш компилятор способен авто-векторизовать некоторые циклы, но обычно это ограничивается "очевидными" приложениями, например, когда вы умножаете каждое число в vector<float>
на другое float
, и этот случай не столь очевидный IMO. Даже если вы pow
каждое число в таком векторе одним и тем же int
, компилятор может быть недостаточно умным, чтобы эффективно векторизовать его, особенно если pow
находится в другой единицы перевода и без эффективной генерации кода времени ссылки.
Если вы не готовы рассмотреть возможность изменения всей структуры вашей программы, чтобы обеспечить эффективное использование SIMD (включая GPGPU), и вы не находитесь в архитектуре, где float
действительно намного дешевле по умолчанию, я предлагаю вам придерживайтесь double
всеми средствами и считайте float
в лучшем случае форматом хранения, который может быть полезен для сохранения ОЗУ, или для улучшения местоположения кеша (когда их у вас их много). Даже тогда измерение - отличная идея.
Тем не менее, вы можете попробовать алгоритм ivaigult (только с double
для промежуточного и для результата), который связан с классическим алгоритмом, называемым Египетское умножение (и множество других имен), только то, что операнды умножаются и не добавляются. Я не знаю, как работает pow(double, double)
, но вполне возможно, что в некоторых случаях этот алгоритм может быть быстрее. Опять же, вы должны быть OCD о бенчмаркинге.
Ответ 4
Есть ли какая-либо специальная функция (в стандартных библиотеках или любой другой) с указанной сигнатурой?
К сожалению, не то, что я знаю.
Но, как многие уже упоминали, необходим бенчмаркинг, чтобы понять, есть ли вообще проблема.
Я собрал быстрый тест онлайн. Код проверки:
#include <iostream>
#include <boost/timer/timer.hpp>
#include <boost/random/mersenne_twister.hpp>
#include <boost/random/uniform_real_distribution.hpp>
#include <cmath>
int main ()
{
boost::random::mt19937 gen;
boost::random::uniform_real_distribution<> dist(0, 10000000);
const size_t size = 10000000;
std::vector<float> bases(size);
std::vector<float> fexp(size);
std::vector<int> iexp(size);
std::vector<float> res(size);
for(size_t i=0; i<size; i++)
{
bases[i] = dist(gen);
iexp[i] = std::floor(dist(gen));
fexp[i] = iexp[i];
}
std::cout << "float pow(float, int):" << std::endl;
{
boost::timer::auto_cpu_timer timer;
for(size_t i=0; i<size; i++)
res[i] = std::pow(bases[i], iexp[i]);
}
std::cout << "float pow(float, float):" << std::endl;
{
boost::timer::auto_cpu_timer timer;
for(size_t i=0; i<size; i++)
res[i] = std::pow(bases[i], fexp[i]);
}
return 0;
}
Результаты тестов (быстрые выводы):
- gcc: С++ 11 последовательно быстрее, чем С++ 03.
- clang: действительно
int
-версия С++ 03 выглядит немного быстрее. Я не уверен, что он находится в пределах допустимой погрешности, так как я запускаю только эталонный тест.
- Оба: даже с С++ 11, вызывающий
pow
с int
, кажется, немного более результативным.
Было бы здорово, если бы другие могли проверить, подходит ли это для их конфигураций.
Ответ 5
Попробуйте вместо этого использовать powf(). Это функция C99, которая также должна быть доступна на С++ 11.