Обрезать число с плавающей точкой до ближайшей степени 2 в C ++ - Производительность - PullRequest
0 голосов
/ 10 февраля 2019

В C ++ (предположим, по крайней мере, C ++ 11), учитывая значение с плавающей запятой a, мне нужно найти значение b с плавающей запятой, удовлетворяющее следующим ограничениям:

  • b должно иметь то же самоезнак как.
  • Величина b должна быть меньше или равна [*] величине a.
  • Величина b должна быть степенью 2.
  • Величина b должна быть как можно больше в этих ограничениях.

Другими словами, мне нужно "урезать" величину a до ближайшей степени 2, оставляя при этомзнак без изменений.

[* В моем случае ограничение «меньше или равно» является слабым, и «меньше» также будет работать.]

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

Более переносимым подходом было бы:

  1. Получить базу-2 логарифма величины, округленной в меньшую сторону, например, logb, ilogb,или даже более переносимый, log2 или frexp.
  2. Повышение 2 до n-й степени с использованием, например, целочисленного сдвига битов (остерегайтесь отрицательных степеней и проблем диапазона значений), pow(2.0,n), exp2(n),или ldexp(1.0,n).
  3. Скопируйте знак с помощью copysign.

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

Ответы [ 2 ]

0 голосов
/ 10 февраля 2019

Исходя из моих собственных тестов, я до сих пор прихожу к следующим выводам (но поскольку у меня нет тестовой лаборатории в моем распоряжении, мои данные наблюдений ограничены, а присяжные все еще отсутствуют):

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

  • Функции одинарной точности без суффикса f (например,ilogb) следует избегать, поскольку они, как правило, работают хуже, чем их f суффиксные аналоги (например, ilogbf).

  • "битовое смещение" не имеет себе равных с точки зрения производительности.Удивительно, но это также работает лучше в 64-битной области (опять же, я тестирую на 64-битной машине).Я вижу менее 1 нс за исполнение.Для сравнения, мой «испытательный стенд» сам весит около 15 нс за итерацию.

Что касается реализации подхода «pow2 (floor (log2))»), вот что яна данный момент:

  • Я не вижу никакой специальной комбинации базовых строительных блоков, которая бы дала прирост производительности от неожиданных синергетических эффектов, поэтому это кажется разумнымрассмотреть типы строительных блоков ("pow2", "floor (log2)" и sign fix) по отдельности.

  • Предполагая, что случай 0,0 не представляет особого интереса, самый быстрый способ обработкизнак должен по существу выполнить операцию «pow2 (floor (log2 (abs))))», а затем зафиксировать знак простым if(a<0) b=-b;, который примерно на 5 нс быстрее copysign.Если строительный блок "pow2" имеет фактор, подобный мантиссе (как ldexp), использование сравнения для выбора положительного или отрицательного фактора также является приемлемым вариантом, поскольку он лишь немного медленнее, чем условное исправление после операции.

  • Безусловно, худший выбор для операции «pow2» (и тот, который программное обеспечение, над которым я работаю, использовался целую вечность в двух реализациях) - наивно использовать pow(2.0,x),В то время как компилятор мог предположительно оптимизировать его во что-то намного более быстрое, мой нет.exp2 примерно на 60 нс быстрее.ldexp еще на 15 нс быстрее, что делает его лучшим выбором, с весом приблизительно 10-10 нс.

  • Существует еще более быстрая опция (также используется в программном обеспеченииЯ работаю над этим), а именно с использованием битовых сдвигов в целочисленной области, но это достигается ценой строгого ограничения диапазона значений, для которых работает функция.Если этот путь будет решен, операция должна быть выполнена в домене long long, поскольку она лишь немного медленнее, чем в домене int.Этот подход может сэкономить еще 4-5 нс.

  • Самый медленный строительный блок "floor (log2)", который я мог найти (кроме (int)(log(x)/log(2)), который я даже не удосужилсятест) был (int)log2(fabs(x)) и их родня.frexp примерно на 30 нс быстрее, взвешиваясь с предположительными 8-10 нс.

  • Если тип с плавающей запятой использует представление base-2, ilogb является жизнеспособнымальтернатива frexp и сохраняет еще 1 нс.logb немного медленнее, чем ilogb (наравне с frexp), что, я думаю, имеет смысл.


В общем, пока что следующие реализации кажутсяСтоит учесть:

double Pow2Trunc(double a)
{
    union { double f; uint64_t i; } hack;
    hack.f = a;
    hack.i &= 0xFFF0000000000000u;
    return hack.f;
}

- самая быстрая реализация (около 1 нс), при условии, что специальные значения не имеют значения, известен двоичный формат с плавающей запятой (в данном случае двоичный код IEEE64) и тип intдоступен тот же размер и порядок байтов;

double Pow2Trunc(double a)
{
    int exp;
    (void)frexp(a,&exp);
    double b = ldexp(0.5, exp);
    if (a < 0) b = -b;
    return b;
}

- самая быстрая полностью переносимая реализация (около 16 нс);и, возможно,

double Pow2Trunc(double a)
{
    double b = ldexp(1.0, ilogb(a));
    if (a < 0) b = -b;
    return b;
}

- немного менее переносимая, но и несколько более быстрая альтернатива (около 15 нс).

(обработка специальных значений может быть улучшена; для моего случая использования, однако онине имеет значения, достаточного для дальнейшего рассмотрения.)

Предоставление альтернатив на основе float, похоже, не стоит усилий;если они предоставлены, важно использовать f -софиксированные варианты функций.


Очевидно, что эти результаты зависят от аппаратной платформы, компилятора и настроек (i7-5820K, Windows 10 Subsystemдля Linux g ++ 5.4.0, -std=gnu++11 -o3 -ffast-math).Пробег других сред может варьироваться, и изучение случаев, когда результаты качественно отличаются, будет для меня наиболее ценным .

0 голосов
/ 10 февраля 2019

Используйте frexp() 1 , ldexp() 2 , чтобы отобразить a и сформировать ответ.

Эти 2 функции - почти все, что есть.Необходимый.

Функции frexp разбивают число с плавающей запятой на нормализованную дробь, а целая степень, равная 2. ... frexp, функции возвращают значение x, такое что x имеет величину в интервале [1/2, 1) или ноль.

Функции ldexp умножают число с плавающей запятой на целую степень, равную 2.

#include <math.h>
#include <stdio.h>

double round_pow2(double a) {
  int exp;
  double frac = frexp(a, &exp);
  if (frac > 0.0) frac = 0.5;
  else if (frac < 0.0) frac = -0.5;
  double b = ldexp(frac, exp);

  printf("% 20g % 25a % 25a", a, a, b);
  printf(" %d", !!signbit(a) == !!signbit(b)); // b must have the same sign as a.
  printf(" %d\n", !(fabs(b) > fabs(a)));       // magnitude `b` must be <= magnitude `a`.

  return b;
}

Тестовый код

void round_pow2_test(double x) {
  round_pow2(nextafter(-x, -INFINITY));
  round_pow2(-x);
  round_pow2(nextafter(-x, INFINITY));
  round_pow2(nextafter(x, -INFINITY));
  round_pow2(x);
  round_pow2(nextafter(x, INFINITY));
}

int main(void) {
  round_pow2_test(0);
  round_pow2_test(DBL_MIN);
  round_pow2_test(1.0);
  round_pow2_test(42.0);
  round_pow2_test(DBL_MAX);
  round_pow2(NAN);
  return 0;
}

Выход

   -4.94066e-324                -0x1p-1074                -0x1p-1074 1 1
              -0                   -0x0p+0                   -0x0p+0 1 1
    4.94066e-324                 0x1p-1074                 0x1p-1074 1 1
   -4.94066e-324                -0x1p-1074                -0x1p-1074 1 1
               0                    0x0p+0                    0x0p+0 1 1
    4.94066e-324                 0x1p-1074                 0x1p-1074 1 1
   -2.22507e-308  -0x1.0000000000001p-1022                -0x1p-1022 1 1
   -2.22507e-308                -0x1p-1022                -0x1p-1022 1 1
   -2.22507e-308  -0x1.ffffffffffffep-1023                -0x1p-1023 1 1
    2.22507e-308   0x1.ffffffffffffep-1023                 0x1p-1023 1 1
    2.22507e-308                 0x1p-1022                 0x1p-1022 1 1
    2.22507e-308   0x1.0000000000001p-1022                 0x1p-1022 1 1
              -1     -0x1.0000000000001p+0                   -0x1p+0 1 1
              -1                   -0x1p+0                   -0x1p+0 1 1
              -1     -0x1.fffffffffffffp-1                   -0x1p-1 1 1
               1      0x1.fffffffffffffp-1                    0x1p-1 1 1
               1                    0x1p+0                    0x1p+0 1 1
               1      0x1.0000000000001p+0                    0x1p+0 1 1
             -42     -0x1.5000000000001p+5                   -0x1p+5 1 1
             -42                 -0x1.5p+5                   -0x1p+5 1 1
             -42     -0x1.4ffffffffffffp+5                   -0x1p+5 1 1
              42      0x1.4ffffffffffffp+5                    0x1p+5 1 1
              42                  0x1.5p+5                    0x1p+5 1 1
              42      0x1.5000000000001p+5                    0x1p+5 1 1
            -inf                      -inf                   -0x1p-1 1 1
   -1.79769e+308  -0x1.fffffffffffffp+1023                -0x1p+1023 1 1
   -1.79769e+308  -0x1.ffffffffffffep+1023                -0x1p+1023 1 1
    1.79769e+308   0x1.ffffffffffffep+1023                 0x1p+1023 1 1
    1.79769e+308   0x1.fffffffffffffp+1023                 0x1p+1023 1 1
             inf                       inf                    0x1p-1 1 1
             nan                       nan                       nan 1 1

1 Из OP "Получите логарифм величины основание-2, округленный в меньшую сторону, используя, например, ... frexp. "

2 Из OP" Поднимите 2 до n-й степени, используя, например, ... ldexp (1.0, n). "

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...