LINUX.ORG.RU

Числа с плавающей запятой. Откуда взялся минус и как его убрать?

 , , ,


0

3

Тестовый пример, показывающий суть явления:

#include <stdio.h>
int main()
{
    float right=-2.0f, left=2.0f; 
    float res = (right+left)/(right-left);
    printf("%5.12e\n", res);

    float test = -0.00000000000000424f;
    printf("%3.3f\n", test);
}

Результат:

-0.000000000000e+00
-0.000

Понятно, что после right-left знаковый бит становится 1. Но выглядит (когда выводишь на экран с %3.3f) так как будто есть где-то далеко после нулей числа, например -0.00000000000000424. А на самом деле их нет. Это ноль, только со знаком (IEEE754 это допускает). Но с математической точки зрения и при выводе матрицы нам хотелось бы видеть настоящий ноль (в математике он один) без знака. Кто-нибудь сталкивался с такой «проблемой»? Как красиво сделать в программе, чтобы ноль был единственным, без знака. Есть ли изящные решения? Просто не хочется чтобы в матрице было -0.000, это может сбить с толка того, кто будет смотреть вывод на экран или распечатку, по уже выше описанной причине.



Последнее исправление: Gyros (всего исправлений: 1)
Ответ на: комментарий от anonymous

Я имею в виду модели округления описанные в самом стандарте.

В https://gcc.gnu.org/wiki/FloatingPointMath есть интересные комментарии в секции Transformations:

0.0 - 0.0 = +0.0, not -0.0, unless rounding to -Inf
0.0 - 0.0 = -0.0, when rounding to -Inf

Т.е. если округление в сторону -Inf, то будет отрицательный ноль.

«Фикс» анона с хаком установки модели округления:

#include <stdio.h>
#include <xmmintrin.h>

void set_round_down() {
        const unsigned int down = _MM_MASK_MASK | _MM_ROUND_DOWN;
        asm (
        "ldmxcsr %0" : : "m" (down)
        );
}

int main() {
        volatile float right = -2.0, left = 2.0, zero = 0;
        float res = (right + left) / (right - left);
        printf("%5.12e\n", res);
        set_round_down();
        res = res + zero;
        printf("%5.12e\n", res);
        return 0;
}
gcc -O1 -ffast-math main.cpp
./a.out
-0.000000000000e+00
-0.000000000000e+00

Не работает LMAO)

PRN
()
Последнее исправление: PRN (всего исправлений: 1)
Ответ на: комментарий от PRN

да, я так и понял. а в gcc и стандарте си же есть средства, чтобы менять rounding mode. man fesetround.

но я пока что-то не очень разобрался. понял только что по-дефолту там FE_TONEAREST, то есть тот, который нам подходит. хотя в самом стандарте я что-то не смог найти, где бы говорилось про дефолт, только в контексте float decimal (FE_DEC_TONEAREST), а это, насколько понимаю, не совсем то. смотрел драфт си-23 и стандарт си-11.

если менять через fesetround(), то какая-то реакция есть, но что-то там тонкости какие-то с Compliance из первой таблички по ссылке как у тебя. если правильно понимаю, то надо либо прописывать прагму #pragma STDC FENV_ACCESS ON, либо указывать флаг -frounding-math, чтобы менять rounding mode.

anonymous
()
Ответ на: комментарий от anonymous

ну что-то такое получилось:

#include <stdio.h>
#include <fenv.h>

int main() {
    volatile float zero=0.0f, res=-0.0f;
    fesetround(FE_TONEAREST);
    printf("FE_TONEAREST:  %.1f+0.0 = %+.1f\n", res, res+zero);
    fesetround(FE_TOWARDZERO);
    printf("FE_TOWARDZERO: %.1f+0.0 = %+.1f\n", res, res+zero);
    fesetround(FE_UPWARD);
    printf("FE_UPWARD:     %.1f+0.0 = %+.1f\n", res, res+zero);
    fesetround(FE_DOWNWARD);
    printf("FE_DOWNWARD:   %.1f+0.0 = %+.1f\n", res, res+zero);
}
$ gcc -O1 -ffast-math -lm test.c -o test && ./test
FE_TONEAREST:  -0.0+0.0 = +0.0
FE_TOWARDZERO: -0.0+0.0 = +0.0
FE_UPWARD:     -0.0+0.0 = +0.0
FE_DOWNWARD:   -0.0+0.0 = -0.0

если писать #pragma STDC FENV_ROUND ... или #pragma STDC FENV_ACCESS ..., то gcc с -Wall ругается

warning: ignoring ‘#pragma STDC FENV_ROUND’ [-Wunknown-pragmas]

-frounding-math тоже не даёт ощутимого эффекта.

gcc version 14.1.1 20240507
anonymous
()
Ответ на: комментарий от Gyros

это чтобы компилятор не вздумал оптимизировать значение. квалификатор volatile подразумевает, что значение переменной может измениться не из кода этой программы, а, например, аппаратно или из другой программы, поэтому компилятор «не имеет права» оптимизировать любые операции с такой переменной и обязан перед операцией каждый раз прочитывать её значение. в некоторых критичных к производительности случаях это может добавлять приличных тормозов. ну в случаях чтения, например, аппаратных регистров такой спецификатор просто необходим.

anonymous
()
Ответ на: комментарий от anonymous

Спасибо anonymous за подробный ответ!

Всегда имел туманное представление о действии этого квалификатора, потому что в книгах по Си ему уделяется мало внимания и почти не приводится примеров на его использование.

Gyros
() автор топика