Как вы находите ближайшую не равную ценность поплавка?

A float (a.k.a. single) значение является 4-байтовым значением и должно представлять любое действительное значение. Из-за способа форматирования и конечного количества байтов он отключается, существует минимальное значение и максимальное значение, которое оно может представлять, и оно имеет конечную точность, в зависимости от его собственного значения.

Я хотел бы знать, если есть способ, чтобы получить наиболее близкое возможное значение выше или ниже некоторого эталонного значения, учитывая конечную точность поплавка. С целыми числами это тривиально: один просто добавляет или вычитает 1. Но с float вы не можете просто добавить или вычесть минимальное значение поплавка и ожидать, что он будет отличаться от вашего исходного значения. То есть.

float FindNearestSmaller (const float a)
{
    return a - FLT_MIN; /* This doesn't necessarily work */
}

Фактически, вышеизложенное почти никогда не будет работать. В приведенном выше случае возврат, как правило, будет равен a, так как FLT_MIN намного превосходит точность a. Вы можете легко попробовать это самостоятельно: он работает, например, 0.0f, или для очень маленьких чисел порядка FLT_MIN, но не для чего-либо между 0 и 100.

Итак, как бы вы получили значение, которое является самым близким, но меньшим или большим, чем a, с учетом точности с плавающей запятой?

Примечание: Хотя я в основном интересуюсь ответом на C/С++, я предполагаю, что ответ будет применим для большинства языков программирования.

Ответы

Ответ 1

Стандартным способом поиска соседей с плавающей запятой является функция nextafter для double и nextafterf для float. Второй аргумент дает направление. Помните, что бесконечности являются юридическими значениями в плавающей запятой IEEE 754, поэтому вы можете очень хорошо позвонить nextafter(x, +1.0/0.0), чтобы получить значение сразу над x, и это будет работать даже для DBL_MAX (тогда как если бы вы написали nextafter(x, DBL_MAX), он будет возвращать DBL_MAX при применении для x == DBL_MAX).

Иногда могут использоваться нестандартные способы:

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

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


Для полноты я добавлю, что даже без изменения режима округления с его "до ближайшего" значения по умолчанию, умножение поплавка на (1.0f + FLT_EPSILON) вызывает число, которое является либо ближайшим соседом от нуля, либо соседним после этого. Это, вероятно, самый дешевый, если вы уже знаете знак плавающего, который хотите увеличить/уменьшить, и вы не возражаете, что он иногда не создает ближайшего соседа. Функции nextafter и nextafterf задаются таким образом, что правильная реализация на x86 должна проверяться на наличие ряда специальных значений и FPU и, следовательно, является дорогостоящим для того, что он делает.

Чтобы перейти к нулю, умножьте на 1.0f - FLT_EPSILON.

Это не работает для 0.0f, очевидно, и вообще для меньших денормализованных чисел.

Значения, для которых умножение на 1.0f + FLT_EPSILON продвигается на 2 ULPS, находятся чуть ниже двух, особенно в интервале [0.75 * 2 p... 2 p). Если вы не возражаете делать умножение и добавление, x + (x * (FLT_EPSILON * 0.74)) должен работать для всех нормальных чисел (но все же не для нуля или для всех маленьких денормальных чисел).

Ответ 2

Посмотрите на функцию "nextafter", которая является частью Standard C (и, вероятно, С++, но я не проверял).

Ответ 3

Я попробовал это на своей машине. И все три подхода:
1. добавление с 1 и memcopying
2. добавление FLT_EPSILON
3. умножение на (1.0f + FLT_EPSILON)
похоже, дает тот же ответ.


см. результат здесь
bash -3.2 $cc float_test.c -o float_test;./float_test 1.023456 10
Оригинальный номер: 1.023456
int added = 1.023456 01-eps added = 1.023456 mult by 01 * (eps + 1) = 1.023456
int добавлено = 1.023456 02-eps добавлено = 1.023456 mult на 02 * (eps + 1) = 1.023456
int добавлено = 1.023456 03-eps добавлено = 1.023456 mult на 03 * (eps + 1) = 1.023456
int добавлено = 1.023456 04-eps добавлено = 1.023456 mult на 04 * (eps + 1) = 1.023456
int added = 1.023457 05-eps added = 1.023457 mult by 05 * (eps + 1) = 1.023457
int added = 1.023457 06-eps added = 1.023457 mult by 06 * (eps + 1) = 1.023457
int added = 1.023457 07-eps added = 1.023457 mult by 07 * (eps + 1) = 1.023457
int added = 1.023457 08-eps добавлено = 1.023457 mult by 08 * (eps + 1) = 1.023457
int added = 1.023457 09-eps добавлено = 1.023457 mult на 09 * (eps + 1) = 1.023457
int added = 1.023457 10-eps added = 1.023457 mult by 10 * (eps + 1) = 1.023457

код
#include <float.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>

int main(int argc, char *argv[])
{

    if(argc != 3) {
        printf("Usage: <binary> <floating_pt_num> <num_iter>\n");
        exit(0);
    }

    float f = atof(argv[1]);
    int count = atoi(argv[2]);

    assert(count > 0);

    int i;
    int num;
    float num_float;

    printf("Original num: %f\n", f);
    for(i=1; i<=count; i++) {
        memcpy(&num, &f, 4);
        num += i;
        memcpy(&num_float, &num, 4);
        printf("int added = %f \t%02d-eps added = %f \tmult by %2d*(eps+1) = %f\n", num_float, i, f + i*FLT_EPSILON, i, f*(1.0f + i*FLT_EPSILON));
    }

    return 0;
}