Дневник развития смарт-контрактов на Rust (7) Числовая арифметика
1. Проблема точности вычислений с плавающей запятой
Язык Rust изначально поддерживает операции с плавающей запятой, но операции с плавающей запятой имеют неотъемлемую проблему точности вычислений. При написании смарт-контрактов не рекомендуется использовать операции с плавающей запятой, особенно при обработке коэффициентов или процентных ставок, касающихся важных экономических/финансовых решений.
В языке Rust числа с плавающей запятой соответствуют стандарту IEEE 754. Например, для типа с двойной точностью f64 его внутреннее двоичное представление выглядит следующим образом:
!
Числа с плавающей запятой выражаются в научной нотации с основанием 2. Например, 0.8125 может быть представлено конечным двоичным числом 0.1101:
проведение 1 теста
Значение суммы: 0.69999999999999995559
поток 'tests::precision_test_float' паниковал с сообщением 'проверка не удалась: (left == right)
Слева: 0.0699999999999999, справа: 0.07: ', src/lib.rs:185:9
Можно увидеть, что значение amount не точно 0.7, а является приближенным значением 0.69999999999999995559. Результат дальнейшего деления также становится неточным 0.06999999999999999.
Чтобы решить эту проблему, можно рассмотреть использование фиксированной запятой. В протоколе NEAR обычно используется представление 10^24 йоктоNEAR, эквивалентного 1 токену NEAR.
проводится 1 тест
тест тесты::точность_тест_целого ... ок
результат теста: ок. 1 пройден; 0 провален; 0 проигнорировано; 0 измерено; 8 отфильтровано; завершено за 0.00s
2. Проблема точности вычислений целых чисел в Rust
2.1 порядок операций
Изменение порядка операций между умножением и делением с одинаковым приоритетом может непосредственно повлиять на результат вычисления, что приводит к проблемам с точностью целочисленных вычислений.
Пусть result_0 = a
.checked_mul(c)
.expect("ERR_MUL")
.checked_div(b)
.expect("ERR_DIV");
Пусть result_1 = a
.checked_div(b)
.expect("ERR_DIV")
.checked_mul(c)
.expect("ERR_MUL");
assert_eq!(result_0,result_1,"");
}
Результат выполнения:
проведение 1 теста
поток 'tests::precision_test_0' завершился с ошибкой: 'не удалось выполнить проверку: (left == right)
Слева: 2, справа: 0: ', src/lib.rs:175:9
Можно заметить, что result_0 = a * c / b и result_1 = (a / b) * c, несмотря на то, что формулы одинаковы, результаты различаются. Причина в том, что целочисленное деление отбрасывает точность, меньшую чем делитель. При вычислении result_1 сначала вычисляется (a / b), что приводит к потере точности и результату 0; в то время как при вычислении result_0 сначала вычисляется a * c = 20_0000, что больше делителя b, что предотвращает потерю точности.
проводится 1 тест
12:13
поток 'tests::precision_test_decimals' вызвал панику при 'ошибка утверждения: (left == right)
Слева: 12, справа: 13: ', src/lib.rs:214:9
Видно, что результаты result_0 и result_1, эквивалентные по вычислительному процессу, различаются, и result_1 = 13 ближе к фактическому ожидаемому значению 13.3333....
3. Как написать смарт-контракты на Rust для числовых расчетов
3.1 Изменение порядка операций
Приоритизировать умножение целых чисел над делением целых чисел.
3.2 Увеличение порядка величины целых чисел
Целые числа используют более крупные порядки, создавая более крупные числители.
3.3 Потеря точности накопления вычислений
Для неизбежных проблем с точностью вычислений целых чисел можно рассмотреть возможность записи накопленных потерь точности вычислений.
тесты тестов::record_offset_test ... ок
результат теста: ок. 1 пройден; 0 неудач; 0 проигнорировано; 0 измерено; 9 отфильтровано; завершено за 0.00с
3.4 Использование библиотеки Rust Crate rust-decimal
Библиотека подходит для финансовых вычислений с десятичными числами, которые требуют точных вычислений и не имеют ошибок округления.
3.5 Учитывая механизм округления
При проектировании смарт-контрактов проблема округления обычно решается по принципу "Я хочу получить выгоду, другие не должны зарабатывать на мне". Согласно этому принципу, если округление вниз выгодно для меня, то округляем вниз; если округление вверх выгодно для меня, то округляем вверх; округление до ближайшего целого не может быть определено, кому это выгодно, поэтому используется очень редко.
На этой странице может содержаться сторонний контент, который предоставляется исключительно в информационных целях (не в качестве заявлений/гарантий) и не должен рассматриваться как поддержка взглядов компании Gate или как финансовый или профессиональный совет. Подробности смотрите в разделе «Отказ от ответственности» .
17 Лайков
Награда
17
5
Поделиться
комментарий
0/400
LiquidationKing
· 07-16 06:01
То, что ты говоришь 0.7, не имеет большого значения. Настоящий интерес будет, когда произойдет большой крах.
Посмотреть ОригиналОтветить0
BankruptcyArtist
· 07-16 05:57
Эх, написание смарт-контрактов действительно было ужасно из-за плавающей точки.
Посмотреть ОригиналОтветить0
GmGmNoGn
· 07-16 05:55
Этот баг может испортить мою точность.
Посмотреть ОригиналОтветить0
NotSatoshi
· 07-16 05:55
В смарт-контрактах играть с плавающей точкой, мозг сейчас взорвется.
Rust смарт-контракты численные расчеты: ловушки с плавающей запятой и оптимизация точности целых чисел
Дневник развития смарт-контрактов на Rust (7) Числовая арифметика
1. Проблема точности вычислений с плавающей запятой
Язык Rust изначально поддерживает операции с плавающей запятой, но операции с плавающей запятой имеют неотъемлемую проблему точности вычислений. При написании смарт-контрактов не рекомендуется использовать операции с плавающей запятой, особенно при обработке коэффициентов или процентных ставок, касающихся важных экономических/финансовых решений.
В языке Rust числа с плавающей запятой соответствуют стандарту IEEE 754. Например, для типа с двойной точностью f64 его внутреннее двоичное представление выглядит следующим образом:
!
Числа с плавающей запятой выражаются в научной нотации с основанием 2. Например, 0.8125 может быть представлено конечным двоичным числом 0.1101:
0.8125 = 0.5 * 1 + 0.25 * 1 + 0.125 * 0 + 0.0625 * 1
Однако для таких дробей, как 0.7, может возникнуть ситуация бесконечного повторения:
0.7 = 0.1011001100110011...
Это приводит к тому, что невозможно точно представить с помощью конечной длины числа с плавающей запятой, существует "округление".
В качестве примера распределения 0.7 токена NEAR среди десяти пользователей на блокчейне NEAR:
ржавчина #[test] fn precision_test_float() { Пусть сумма: f64 = 0.7;
пусть делитель: f64 = 10.0;
пусть result_0 = сумма / делитель;
println!("Значение суммы: {:.20}", amount); assert_eq!(result_0, 0.07, ""); }
Результат выполнения:
проведение 1 теста Значение суммы: 0.69999999999999995559 поток 'tests::precision_test_float' паниковал с сообщением 'проверка не удалась: (left == right) Слева: 0.0699999999999999, справа: 0.07: ', src/lib.rs:185:9
Можно увидеть, что значение amount не точно 0.7, а является приближенным значением 0.69999999999999995559. Результат дальнейшего деления также становится неточным 0.06999999999999999.
Чтобы решить эту проблему, можно рассмотреть использование фиксированной запятой. В протоколе NEAR обычно используется представление 10^24 йоктоNEAR, эквивалентного 1 токену NEAR.
Измененный тестовый код:
ржавчина #[test] fn precision_test_integer() { пусть N: u128 = 1_000_000_000_000_000_000_000_000;
let amount: u128 = 700_000_000_000_000_000_000_000; пусть делитель: u128 = 10;
пусть result_0 = сумма / делитель; assert_eq!(result_0, 70_000_000_000_000_000_000_000_000, ""); }
Результат выполнения:
проводится 1 тест тест тесты::точность_тест_целого ... ок результат теста: ок. 1 пройден; 0 провален; 0 проигнорировано; 0 измерено; 8 отфильтровано; завершено за 0.00s
2. Проблема точности вычислений целых чисел в Rust
2.1 порядок операций
Изменение порядка операций между умножением и делением с одинаковым приоритетом может непосредственно повлиять на результат вычисления, что приводит к проблемам с точностью целочисленных вычислений.
ржавчина #[test] fn precision_test_div_before_mul() { пусть a: u128 = 1_0000; пусть b: u128 = 10_0000; пусть c: u128 = 20;
}
Результат выполнения:
проведение 1 теста поток 'tests::precision_test_0' завершился с ошибкой: 'не удалось выполнить проверку: (left == right) Слева: 2, справа: 0: ', src/lib.rs:175:9
Можно заметить, что result_0 = a * c / b и result_1 = (a / b) * c, несмотря на то, что формулы одинаковы, результаты различаются. Причина в том, что целочисленное деление отбрасывает точность, меньшую чем делитель. При вычислении result_1 сначала вычисляется (a / b), что приводит к потере точности и результату 0; в то время как при вычислении result_0 сначала вычисляется a * c = 20_0000, что больше делителя b, что предотвращает потерю точности.
2.2 слишком маленький порядок
ржавчина #[test] fn precision_test_decimals() { пусть a: u128 = 10; пусть b: u128 = 3; пусть c: u128 = 4; пусть десятичная: u128 = 100_0000;
}
Результат выполнения:
проводится 1 тест 12:13 поток 'tests::precision_test_decimals' вызвал панику при 'ошибка утверждения: (left == right) Слева: 12, справа: 13: ', src/lib.rs:214:9
Видно, что результаты result_0 и result_1, эквивалентные по вычислительному процессу, различаются, и result_1 = 13 ближе к фактическому ожидаемому значению 13.3333....
3. Как написать смарт-контракты на Rust для числовых расчетов
3.1 Изменение порядка операций
3.2 Увеличение порядка величины целых чисел
3.3 Потеря точности накопления вычислений
Для неизбежных проблем с точностью вычислений целых чисел можно рассмотреть возможность записи накопленных потерь точности вычислений.
ржавчина константа USER_NUM: u128 = 3;
FN distribute(amount: u128, смещение: u128) -> u128 { пусть token_to_distribute = смещение + сумма; пусть per_user_share = token_to_distribute / USER_NUM; println!("per_user_share {}", per_user_share); пусть recorded_offset = token_to_distribute - per_user_share * USER_NUM; recorded_offset }
#[test] FN record_offset_test() { let mut offset: u128 = 0; для i в 1..7 { println!("Round {}", i); смещение = distribute(10_000_000_000_000_000_000_000_000, offset); println!("Смещение {}\n", offset); } }
Результат выполнения:
проведение 1 теста Раунд 1 per_user_share 3333333333333333333333333 Смещение 1
Раунд 2 per_user_share 3333333333333333333333333 Смещение 2
Раунд 3 per_user_share 400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 Смещение 0
Раунд 4 per_user_share 3333333333333333333333333 Смещение 1
Раунд 5 per_user_share 3333333333333333333333333 Смещение 2
Раунд 6 per_user_share 400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 Смещение 0
тесты тестов::record_offset_test ... ок результат теста: ок. 1 пройден; 0 неудач; 0 проигнорировано; 0 измерено; 9 отфильтровано; завершено за 0.00с
3.4 Использование библиотеки Rust Crate rust-decimal
Библиотека подходит для финансовых вычислений с десятичными числами, которые требуют точных вычислений и не имеют ошибок округления.
3.5 Учитывая механизм округления
При проектировании смарт-контрактов проблема округления обычно решается по принципу "Я хочу получить выгоду, другие не должны зарабатывать на мне". Согласно этому принципу, если округление вниз выгодно для меня, то округляем вниз; если округление вверх выгодно для меня, то округляем вверх; округление до ближайшего целого не может быть определено, кому это выгодно, поэтому используется очень редко.
!
!