LINUX.ORG.RU

Почему C++ не может без потери данных сдвинуть запятую во float типе данных?

 ,


2

2

Привет всем!

Столкнулся с проблемой, простейшее умножение числа 0.56 на 10.0 не даёт точного результата. C++ просто не в состоянии перенести знак справа налево когда я хочу перенести разряд. Но при этом, 0.56 * 100.0 даёт точный ответ, точное число 56.0! Lol! ))))

Многие ответят скорее всего, что - «округли, да и всё!». Нет, округление не подходит, так как задача выполняется не в традиционных языках программирования, а в нодовой системе шейдеров Blender где ноды математики полагаются полностью на логику C++ в отношении математики и я не могу ничего с этим поделать кроме того, что доступно из математики в Блендер.

Да, в нодах есть операции округления, но мне это не подходит, потому что мне нужно получать из большого целого числа отдельные части разрядов, т.е. из числа 12345 получать отдельно число 1,2,3,4 и 5. При этом у меня нет никаких переменных, циклов и т.д.как в традиционных языках программирования. Есть только нодовый поток. Я научился это делать умножением и делением, получать отдельные разряды в нодовом потоке, но столкнулся со странной математикой в C++ на которые эти ноды опираются (полагаются).

Почему C++ не может просто сдвинуть запятую справа налево при умножении на 10, а при умножении на 100 может? Это баг какой-то или фича?

В других языках, которых я немного знаю, Java и Python (да, я понимаю, что это интерпретируемые языки) такого нет, результат всегда ожидаемый: 0.56 * 10.0 = 5.6 - P.S. Как оказалось - нет, см. комметарии.

https://godbolt.org/z/ErnbfePhf



Последнее исправление: dva20 (всего исправлений: 8)

посмотри, как хранится плавающее число в машинном формате. там нет никаких десяток. там мантисса и экспонента-степень двойки. не может она «сдвигать десятичную запятую», поскольку там двоичное, а не десятичное представление.

сишечке тоже пофиг. он просто заряжяет плавающие операции на процессор, подсовывая плавающие числа в машинном формате.

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

Очень крутой материал, очевидно что там кроется мой ответ, но я с этим если и разберусь, то вероятно через несколько десятков лет ))) Боюсь, что у меня не хватит ни сил, ни жизни. Есть ли ответ попроще для чайников так сказать? ))

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

По заголовку понял, что речь идет о floats. Скорее всего, кто-то уже реализовал типы, которые ведут себя так, как тебе нужно.

Bagrov ★★★★★
()
Ответ на: комментарий от alysnix

Ок, но тогда почему Java, Python и скорее всего многие другие языки это могут делать точно? Почему они научились это делать, а C++ не может?

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

В С++ и в Си арифметика плавающих чисел работает далеко не так, как это обычно представляется. Ссылка выше ведёт собственно на стандарт, который реализован в этих языках. Объяснения более простым языком можно найти в гугле по запросу «What Every Computer Scientist Should Know About Floating-Point Arithmetic».

mxfm ★★
()
Ответ на: комментарий от alysnix

сишечке тоже пофиг. он просто заряжяет плавающие операции на процессор, подсовывая плавающие числа в машинном формате.

Ммм… Интересно…

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

Ок, но тогда почему Java, Python

не проверял, что они там делают, пытон вообще может делать через эмуляцию плавающей арифметики с бесконечной точностью(я не пытонист). си++ тоже может делать через эмуляцию, если ему ключик правильный подложить.

в чем вопрос-то? си++ должен нормально «умножать» такую ерунду, поскольку это делает проц, а не си

alysnix ★★★
()
Ответ на: комментарий от dva20

Ок, но тогда почему Java, Python и скорее всего многие другие языки это могут делать точно? Почему они научились это делать, а C++ не может?

Потому что они тоже не могут:

$ python3
Python 3.9.10 (main, Jan 16 2022, 02:41:55) 
[GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
@>>> 0.56 * 100.0
56.00000000000001
@>>> 0.56 * 10.0
5.6000000000000005

Ты скорее всего просто не выводил никогда полученные значения с достаточной точностью.

xaizek ★★★★★
()
Ответ на: комментарий от dva20

В этих языках есть обёртка над float, которая более удобна на практике. Но Python, например, не полностью избавлен от этих недостатков (см например 0.3*3).

В С++ можно использовать какие-то библиотеки, если это очень нужно.

mxfm ★★
()

А ты можешь рассказать про логику твоего алгоритма более полно. Может там можно сделать как-то иначе? Зачем тебе нужно ТОЧНО 5.6? Чтобы в последствии сравнить это число с некоторой константой? В таком случае нужно реализовывать сравнение ДО определенной точности.

rumgot ★★★★★
()
Ответ на: комментарий от dva20

Есть ли ответ попроще для чайников так сказать?

Если сказать просто, то не каждое число в десятичной системе имеет точное представление в двоичной, лишь приближенно в виде бесконечной дроби. Можешь расчитать вес разрядов после запятой в двиочной системе, всё станет очевидно (0.5; 0.25; 0.125; …).

kvpfs ★★
()
Последнее исправление: kvpfs (всего исправлений: 1)

такого нет, результат всегда ожидаемый

Нет, неправильно

peregrine ★★★★★
()
Ответ на: комментарий от dva20

Выдаёт ровные 5.6

вообще-то это были константные вычисления скорее всего. это компилятор посчитал константу с бесконечной точностью(ну или сильно повышенной) и выдал результат.

что такого не было, надо или ручками вводить число в ком строке или через параметр передавать в функцию.

alysnix ★★★
()
Ответ на: комментарий от Keltir

В java флоатинг-поит арифметика такая же всратая как и везде, но там есть BigDecimal, который мастюз, если работаешь с нецелыми числами. Т.е. видишь в java коде float/double - писатель дятел.

А автор просто слишком джун, чтобы работать с тем, о чем пишет.

untitl3d
()

Короче говоря, на правах 4.2:

Процессор может без искажений оперировать только теми числами с плавающей точкой, которые можно представить в виде X/(2^Y). 1/2, 1/4, 372/512 - ок, 1/3, 1/10, 56/10 - не ок. Это фундаментальное ограничение.

Длинная математика и методы противодействия накоплению ошибки - вещь сильно отдельная и блендера вряд ли касается.

izzholtik ★★★
()
Ответ на: комментарий от xaizek

Ты скорее всего просто не выводил никогда полученные значения с достаточной точностью.

Ммм.. Понял в чём «проблема». Просто интерпретируемые языки округляют результат принудительно в отличии от C++. Спасибо.

Здесь я попытался вывести 20 знаков после запятой и не получил точного значения:

class Main {
    public static void main(String[] args) {
        float value = 0.56f;
        value = value * 10.0f;

        System.out.println(String.format("%1$,.20f", value));
        // System.out.println(value);
    }
}

Результат: 5.59999990463256800000

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

открою тебе секрет, точные 5.6 (и 0.56 тоже) не то что вычислить тут не получится, их даже просто записать в переменную нельзя

записать в переменную можно только те числа, которые можно представить как простую дробь со степенью двойки в знаменателе, например 0.25 = 1/4 или 0.625 = 5/8

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

Есть ли ответ попроще для чайников так сказать?

Можно.

ни float ни double не в состоянии покрыть все возможные числа с плавающей точкой. Это из за двоичного форамата храниния их в памяти (ссыль но описание ты не осиливаещь). Поэтому образуются дырки в числовом ряде. Дыры равны значению епсилон, которое в плусах можно получить так: std::numeric_limits::epsilon().

Как с этим жить - решать тебе в зависимости от твоей задачи.

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

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

Keltir
()
Ответ на: комментарий от peregrine
  float value = 0.56f;
  value = value * 10.0f;

я говорю о том, что это константное вычисление и уважающий себя компилятор вычислит это на этапе компиляции, а не в рантайме.

а разговор идет о рантайме, то есть пример надо доработать

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

А разве в java бинари не -O0 всегда? Помню реверсил какую-то приложуху от андроида и там были мертвые ветви и константы, которые инициализируются при загрузке, и больше нигде не используются

SR_team ★★★★★
()
Ответ на: комментарий от PhysShell

Да, но языки тоже следуют этому стандарту. В стандартах Си и С++ в разделе normative references IEEE-754 включен в перечень.

mxfm ★★
()
Ответ на: комментарий от rumgot

Мне нужно выводить числа на поверхности объектов, и нужно как-то перебрать порядки в исходном числе.

Например, на входе число 12345. Мне нужно получить отдельно 1,2,3,4,5 с ведущим нулём. Т.е. нужна ещё ёмкость. Делаю это просто:

  1. 7 это ёмкость или 7 циклов для понимания в ЯП
  2. 12345 / power(10,7) = 0.0012345
  3. 0.0012345 * 10.0 = 0.012345 (в системе Blender могут быть только float значения)
  4. trunc(0.012345) = 0 (это значение я вывожу)
  5. Далее всё повторяется - 0.012345 * 10.0 = 0.12345
  6. trunc(0.12345) = 0 (это значение я вывожу)
  7. 0.12345 * 10.0 = 1.2345
  8. trunc(1.2345) = 1 (это значение я вывожу)

и т.д. до последнего числа. Но проблема в том, что когда я разделил число 56 на 1000000, то я получил значение 0,00056 и обратно получить число 56 уножением уже не могу, число совсем другое, как если бы вы порезали апельсины, а когда вместе собрали части, то вышло яблоко )))))

dva20
() автор топика
Ответ на: комментарий от SR_team

А разве в java бинари не -O0 всегда?

я вот откуда знаю. я не явщик. но скорее всего компилятор ея увидит констатность и вычислит сразу. это конечно зависит от его устройства.

alysnix ★★★
()

Есть ли вообще в C++ возможность получить математикой 0.56 * 10.0 = 5.6 без округления?

Только если ты используешь арифметику с фиксированной точкой. Запиши в десятичной записи 1/7. Ты получишь периодическую дробь 0.14285714285714285714285714285714…., Так и для компьютера 1/10 - это периодическая дробь в двоичном представлении. И он вынужден обрезать её, получаю почти тоже самое число, но с погрешностью.

Погрешность эта, как правило, ничтожна мала. Но её надо учитывать при сравнении действительных чисел. Поэтому правильно сравнивать float-ы через abs(a - b) < epsilon.

AlexVR ★★★★★
()
Ответ на: комментарий от alysnix

Нифига оно не сделает, у вас же есть «дизассемблированный» байткод чуть выше.

izzholtik ★★★
()
Ответ на: комментарий от dva20

Но проблема в том, что когда я разделил число 56 на 1000000,

при каждой лишней операции точность теряется. не дели все время на 10, дели исходное на 10, 100, 1000… или я чета не понял.

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

alysnix ★★★
()
Ответ на: комментарий от peregrine

Да, теперь стало понятна «проблема» лучше. Спасибо. Я когда-то давно изучал и писал, но всё забыл.

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

Да это понятно, просто удивляет вот что:

#include <cmath>
#include <cstdio>

int main(){
    float value = 0.56f;
    value = value * 100.0;

    printf("%.50f\n", value);
    return 0;
}

Выводит 56.00000000000000000000000000000000000000000000000000

Т.е. умножение на 100 отлично работает, а на 10 «не может»

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

Но проблема в том, что когда я разделил число 56 на 1000000, то я получил значение 0,00056 и обратно получить число 56 уножением уже не могу, число совсем другое,

если ты хочешь на компьютере получить числа от 0 до 56 с шагом 56/1000000 то надо использовать (i*56.0)/1000000 для целых i от 0 до 1000000. Увы, такова арифметика с погрешностью.

как если бы вы порезали апельсины, а когда вместе собрали части,

Правильная ассоциация в данном случае. Если порезать апельсин грязным ножом, и после каждого реза ты не знаешь, сколько налипнет к ножу от кусочка, а сколько налипнет с ножа на кусочек. И в сумме ты получишь почти тот же апельсин, с учетом погрешности от грязного разрезания.

AlexVR ★★★★★
()
Ответ на: комментарий от alysnix

дели исходное на 10, 100, 1000… или я чета не понял.

Из числа 12345 мне нужно получить в цикле все числа разрядов - 1,2,3,4,5, но могу я их получить исключительно только математикой, никаких переменных, других типов у меня нет.

Можно делить на 10 исходное число, потом откидывать транком целую часть и опять же, умножать на 10, чтобы вернуть целое которое я выведу на экран. На экран невозможно вывести float значения, только одно целое за одну итерацию. В итоге на экран нужно вывести 12345 или даже 0012345.

Вот ссылка на багтрекере, там в видео более понятно

https://developer.blender.org/T95164

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

Т.е. умножение на 100 отлично работает, а на 10 «не может»

А теперь посмотри на представление 10.0 и 100.0 в бинарном виде.

https://www.h-schmidt.net/FloatConverter/IEEE754.html

там разная дробь используется. Отсюда и разница вычисления.

AlexVR ★★★★★
()
Ответ на: комментарий от izzholtik

просто используй math -> round?

Не могу. Тогда я потеряю число, которое нужно вывести на экран. Но у меня есть ещё одна идея, но я её ещё не проверил.

dva20
() автор топика
Ответ на: комментарий от xaizek
$ python3 -q
>>> 0.56 * 10
5.6000000000000005
>>> from decimal import Decimal as D
>>> D('0.56') * 10
Decimal('5.60')
>>> float(_)
5.6

В Блендере, кста, есть Пифон.

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

Спасибо! Да, это очень интересно:

Binary Representation:

10  = 01000001001000000000000000000000
100 = 01000010110010000000000000000000
dva20
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.