Journal de développement des smart contracts Rust (7) Calculs numériques
1. Problèmes de précision dans les opérations sur les nombres à virgule flottante
Le langage Rust prend en charge nativement les opérations sur les nombres à virgule flottante, mais ces opérations présentent des problèmes de précision de calcul inévitables. Lors de la rédaction de smart contracts, il est déconseillé d'utiliser des opérations sur les nombres à virgule flottante, en particulier lors du traitement de taux ou de taux d'intérêt impliquant des décisions économiques/financières importantes.
Le langage Rust suit la norme IEEE 754 pour les nombres à virgule flottante. Prenons l'exemple du type à virgule flottante double précision f64, sa représentation binaire interne est comme suit :
Les nombres à virgule flottante sont exprimés en notation scientifique à base 2. Par exemple, 0.8125 peut être représenté par le nombre binaire à une longueur finie 0.1101 :
Cependant, pour un nombre décimal aussi petit que 0.7, il peut y avoir des cas de répétition infinie :
0.7 = 0.1011001100110011...
Cela entraîne une incapacité à représenter avec précision les nombres à virgule flottante de longueur limitée, ce qui provoque un phénomène d'"arrondi".
Prenons l'exemple de la distribution de 0,7 NEAR tokens à dix utilisateurs sur la blockchain NEAR :
rouille
#[test]
fn precision_test_float() {
let amount: f64 = 0.7;
let divisor: f64 = 10.0;
let result_0 = amount / divisor;
println!("La valeur du montant : {:.20}", montant);
assert_eq!(result_0, 0.07, "");
}
Résultat d'exécution :
exécuter 1 test
La valeur du montant : 0.69999999999999995559
le fil 'tests::precision_test_float' a paniqué à 'assertion échouée : (left == right)
gauche: 0.06999999999999999, droite: 0.07: ', src/lib.rs:185:9
On peut voir que la valeur de amount n'est pas exactement 0.7, mais plutôt une valeur approximative de 0.69999999999999995559. De plus, le résultat de la division devient également imprécis à 0.06999999999999999.
Pour résoudre ce problème, on peut envisager d'utiliser des nombres à virgule fixe. Dans le protocole NEAR, on utilise généralement une représentation où 10^24 yoctoNEAR équivaut à 1 jeton NEAR.
Code de test modifié :
rouille
#[test]
fn precision_test_integer() {
let N: u128 = 1_000_000_000_000_000_000_000_000;
let amount: u128 = 700_000_000_000_000_000_000_000;
let divisor: u128 = 10;
let result_0 = amount / divisor;
assert_eq!(result_0, 70_000_000_000_000_000_000_000, "");
}
Résultat de l'exécution:
exécution d'un test
test tests::precision_test_integer ... ok
résultat du test : ok. 1 réussi ; 0 échoué ; 0 ignoré ; 0 mesuré ; 8 filtrés ; terminé en 0,00s
2. Problème de précision des calculs d'entiers en Rust
2.1 ordre des opérations
Pour la multiplication et la division ayant la même priorité arithmétique, le changement de l'ordre peut directement affecter le résultat du calcul, entraînant des problèmes de précision dans le calcul des entiers.
rouille
#[test]
fn precision_test_div_before_mul() {
let a: u128 = 1_0000;
let b: u128 = 10_0000;
let c: u128 = 20;
exécution d'un test
le fil 'tests::precision_test_0' a paniqué à 'échec de l'assertion : (left == right)
gauche: 2, droite: 0: ', src/lib.rs:175:9
On peut constater que result_0 = a * c / b et result_1 = (a / b) * c, bien que les formules de calcul soient identiques, les résultats sont différents. La raison en est que la division entière abandonne la précision inférieure au diviseur. Lors du calcul de result_1, le calcul de (a / b) conduit à une perte de précision qui devient 0 ; tandis que lors du calcul de result_0, on calcule d'abord a * c = 20_0000, ce qui est supérieur au diviseur b, évitant ainsi la perte de précision.
2.2 une magnitude trop petite
rouille
#[test]
fn precision_test_decimals() {
let a: u128 = 10;
let b: u128 = 3;
let c: u128 = 4;
let decimal: u128 = 100_0000;
let result_0 = a
.checked_div(b)
.expect("ERR_DIV")
.checked_mul(c)
.expect("ERR_MUL");
let result_1 = a
.checked_mul(decimal)
.expect("ERR_MUL")
.checked_div(b)
.expect("ERR_DIV")
exécution de 1 test
12:13
le fil 'tests::precision_test_decimals' a paniqué à 'assertion échouée : (left == right)
left: 12, right: 13: ', src/lib.rs:214:9
On peut voir que les résultats result_0 et result_1, qui sont équivalents dans le processus de calcul, sont différents, et que result_1 = 13 est plus proche de l'attente réelle de 13.3333...
3. Comment rédiger des smart contracts Rust pour l'évaluation numérique
3.1 Ajuster l'ordre des opérations
Faire en sorte que la multiplication des entiers soit prioritaire par rapport à la division des entiers.
3.2 Augmenter l'ordre de grandeur des entiers
Utiliser des ordres de grandeur plus élevés pour créer des numérateurs plus grands.
3.3 perte de précision des calculs accumulés
Concernant le problème inévitable de la précision des calculs entiers, il est possible de considérer l'enregistrement des pertes de précision cumulées.
rouille
const USER_NUM: u128 = 3;
u128 {
let token_to_distribute = offset + amount;
let per_user_share = token_to_distribute / USER_NUM;
println!("per_user_share {}", per_user_share);
let recorded_offset = token_to_distribute - per_user_share * USER_NUM;
recorded_offset
}
#(
fn record_offset_test)[test] {
let mut offset: u128 = 0;
pour i dans 1..7 {
println!("Round {}", i);
offset = distribute(10_000_000_000_000_000_000_000_000, offset);
println!("Offset {}\n", offset);
}
}
Résultat de l'exécution:
exécuter 1 test
Round 1
per_user_share 3333333333333333333333333
Décalage 1
test tests::record_offset_test ... ok
résultat du test : ok. 1 passé ; 0 échoué ; 0 ignoré ; 0 mesuré ; 9 filtrés ; terminé en 0.00s
( 3.4 Utilisation de la bibliothèque Rust Crate rust-decimal
Cette bibliothèque est adaptée aux calculs financiers décimaux nécessitant une précision efficace et sans erreur d'arrondi.
) 3.5 Considérer le mécanisme d'arrondi
Lors de la conception de smart contracts, le problème de l'arrondi est généralement abordé selon le principe "Je veux profiter, les autres ne doivent pas me tondre". Selon ce principe, si arrondir vers le bas est à mon avantage, alors je fais ça ; si arrondir vers le haut est à mon avantage, alors je fais ça ; l'arrondi traditionnel ne peut pas déterminer à qui cela profite, donc il est très rarement utilisé.
Cette page peut inclure du contenu de tiers fourni à des fins d'information uniquement. Gate ne garantit ni l'exactitude ni la validité de ces contenus, n’endosse pas les opinions exprimées, et ne fournit aucun conseil financier ou professionnel à travers ces informations. Voir la section Avertissement pour plus de détails.
17 J'aime
Récompense
17
5
Partager
Commentaire
0/400
LiquidationKing
· 07-16 06:01
Ce que tu dis à propos de 0,7 n'est pas une grande affaire, il faut un gros krach pour que ce soit intéressant.
Voir l'originalRépondre0
BankruptcyArtist
· 07-16 05:57
Ah, écrire des smart contracts m'a vraiment causé des problèmes avec les nombres à virgule flottante.
Voir l'originalRépondre0
GmGmNoGn
· 07-16 05:55
Ce bug peut me faire perdre toute ma précision.
Voir l'originalRépondre0
NotSatoshi
· 07-16 05:55
Dans les smart contracts, jouer avec des points flottants peut faire exploser votre tête.
Rust smart contracts : pièges des nombres à virgule flottante et optimisation de la précision des entiers
Journal de développement des smart contracts Rust (7) Calculs numériques
1. Problèmes de précision dans les opérations sur les nombres à virgule flottante
Le langage Rust prend en charge nativement les opérations sur les nombres à virgule flottante, mais ces opérations présentent des problèmes de précision de calcul inévitables. Lors de la rédaction de smart contracts, il est déconseillé d'utiliser des opérations sur les nombres à virgule flottante, en particulier lors du traitement de taux ou de taux d'intérêt impliquant des décisions économiques/financières importantes.
Le langage Rust suit la norme IEEE 754 pour les nombres à virgule flottante. Prenons l'exemple du type à virgule flottante double précision f64, sa représentation binaire interne est comme suit :
Les nombres à virgule flottante sont exprimés en notation scientifique à base 2. Par exemple, 0.8125 peut être représenté par le nombre binaire à une longueur finie 0.1101 :
0.8125 = 0.5 * 1 + 0.25 * 1 + 0.125 * 0 + 0.0625 * 1
Cependant, pour un nombre décimal aussi petit que 0.7, il peut y avoir des cas de répétition infinie :
0.7 = 0.1011001100110011...
Cela entraîne une incapacité à représenter avec précision les nombres à virgule flottante de longueur limitée, ce qui provoque un phénomène d'"arrondi".
Prenons l'exemple de la distribution de 0,7 NEAR tokens à dix utilisateurs sur la blockchain NEAR :
rouille #[test] fn precision_test_float() { let amount: f64 = 0.7;
let divisor: f64 = 10.0;
let result_0 = amount / divisor;
println!("La valeur du montant : {:.20}", montant); assert_eq!(result_0, 0.07, ""); }
Résultat d'exécution :
exécuter 1 test La valeur du montant : 0.69999999999999995559 le fil 'tests::precision_test_float' a paniqué à 'assertion échouée : (left == right) gauche: 0.06999999999999999, droite: 0.07: ', src/lib.rs:185:9
On peut voir que la valeur de amount n'est pas exactement 0.7, mais plutôt une valeur approximative de 0.69999999999999995559. De plus, le résultat de la division devient également imprécis à 0.06999999999999999.
Pour résoudre ce problème, on peut envisager d'utiliser des nombres à virgule fixe. Dans le protocole NEAR, on utilise généralement une représentation où 10^24 yoctoNEAR équivaut à 1 jeton NEAR.
Code de test modifié :
rouille #[test] fn precision_test_integer() { let N: u128 = 1_000_000_000_000_000_000_000_000;
let amount: u128 = 700_000_000_000_000_000_000_000; let divisor: u128 = 10;
let result_0 = amount / divisor; assert_eq!(result_0, 70_000_000_000_000_000_000_000, ""); }
Résultat de l'exécution:
exécution d'un test test tests::precision_test_integer ... ok résultat du test : ok. 1 réussi ; 0 échoué ; 0 ignoré ; 0 mesuré ; 8 filtrés ; terminé en 0,00s
2. Problème de précision des calculs d'entiers en Rust
2.1 ordre des opérations
Pour la multiplication et la division ayant la même priorité arithmétique, le changement de l'ordre peut directement affecter le résultat du calcul, entraînant des problèmes de précision dans le calcul des entiers.
rouille #[test] fn precision_test_div_before_mul() { let a: u128 = 1_0000; let b: u128 = 10_0000; let c: u128 = 20;
.checked_mul(c) .expect("ERR_MUL") .checked_div(b) .expect("ERR_DIV");
.checked_div(b) .expect("ERR_DIV") .checked_mul(c) .expect("ERR_MUL");
}
Résultat d'exécution:
exécution d'un test le fil 'tests::precision_test_0' a paniqué à 'échec de l'assertion : (left == right) gauche: 2, droite: 0: ', src/lib.rs:175:9
On peut constater que result_0 = a * c / b et result_1 = (a / b) * c, bien que les formules de calcul soient identiques, les résultats sont différents. La raison en est que la division entière abandonne la précision inférieure au diviseur. Lors du calcul de result_1, le calcul de (a / b) conduit à une perte de précision qui devient 0 ; tandis que lors du calcul de result_0, on calcule d'abord a * c = 20_0000, ce qui est supérieur au diviseur b, évitant ainsi la perte de précision.
2.2 une magnitude trop petite
rouille #[test] fn precision_test_decimals() { let a: u128 = 10; let b: u128 = 3; let c: u128 = 4; let decimal: u128 = 100_0000;
.checked_mul(c) .expect("ERR_MUL") .checked_div(decimal)
.expect("ERR_DIV");
}
Résultat de l'exécution:
exécution de 1 test 12:13 le fil 'tests::precision_test_decimals' a paniqué à 'assertion échouée : (left == right) left: 12, right: 13: ', src/lib.rs:214:9
On peut voir que les résultats result_0 et result_1, qui sont équivalents dans le processus de calcul, sont différents, et que result_1 = 13 est plus proche de l'attente réelle de 13.3333...
3. Comment rédiger des smart contracts Rust pour l'évaluation numérique
3.1 Ajuster l'ordre des opérations
3.2 Augmenter l'ordre de grandeur des entiers
3.3 perte de précision des calculs accumulés
Concernant le problème inévitable de la précision des calculs entiers, il est possible de considérer l'enregistrement des pertes de précision cumulées.
rouille const USER_NUM: u128 = 3;
u128 { let token_to_distribute = offset + amount; let per_user_share = token_to_distribute / USER_NUM; println!("per_user_share {}", per_user_share); let recorded_offset = token_to_distribute - per_user_share * USER_NUM; recorded_offset }
#( fn record_offset_test)[test] { let mut offset: u128 = 0; pour i dans 1..7 { println!("Round {}", i); offset = distribute(10_000_000_000_000_000_000_000_000, offset); println!("Offset {}\n", offset); } }
Résultat de l'exécution:
exécuter 1 test Round 1 per_user_share 3333333333333333333333333 Décalage 1
Round 2 per_user_share 3333333333333333333333333 Offset 2
Round 3 per_user_share 4000000000000000000000000 Offset 0
Round 4 per_user_share 3333333333333333333333333 Offset 1
Round 5 per_user_share 3333333333333333333333333 Offset 2
Round 6 per_user_share 4000000000000000000000000 Décalage 0
test tests::record_offset_test ... ok résultat du test : ok. 1 passé ; 0 échoué ; 0 ignoré ; 0 mesuré ; 9 filtrés ; terminé en 0.00s
( 3.4 Utilisation de la bibliothèque Rust Crate rust-decimal
Cette bibliothèque est adaptée aux calculs financiers décimaux nécessitant une précision efficace et sans erreur d'arrondi.
) 3.5 Considérer le mécanisme d'arrondi
Lors de la conception de smart contracts, le problème de l'arrondi est généralement abordé selon le principe "Je veux profiter, les autres ne doivent pas me tondre". Selon ce principe, si arrondir vers le bas est à mon avantage, alors je fais ça ; si arrondir vers le haut est à mon avantage, alors je fais ça ; l'arrondi traditionnel ne peut pas déterminer à qui cela profite, donc il est très rarement utilisé.
![]###https://img-cdn.gateio.im/webp-social/moments-1933a4a2dd723a847f0059d31d1780d1.webp###