Как заставить 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.