Как вы находите ближайшую не равную ценность поплавка?
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;
}