[RUBY] Qu'est-ce qu'une virgule flottante?

introduction

Récemment (novembre 2018), Togetter summary [Il a été écrit en BASIC il y a 30 ans que "Si vous ajoutez 0,01 10000 fois, ce sera 100,003", donc quand je l'ai essayé dans l'environnement actuel, le résultat était le même](https //togetter.com/li/1289806) est devenu un sujet brûlant.

Par exemple, dans une histoire similaire, l'ajout de 0,01 100 fois dans Ruby (x86_64-Linux) ne donne pas exactement 1, et il y a une légère erreur (voir l'exemple suivant). La situation sera similaire pour les autres langages de programmation.

Ruby(x86_64-Linux)À 0.Lorsque 01 est ajouté 100 fois


$ irb
irb(main):001:0> 100.times.reduce(0){|s,_| s+0.01 }
=> 1.0000000000000007

Dans le résumé du cas, il y a une opinion que "à cette époque ...", mais ** qu'est-ce qu'une virgule flottante ** Pour ceux qui la connaissent, même si c'est la connaissance d'Atari Mae, sinon ** Cela ressemble à de simples données fractionnaires **, et si vous n'y êtes pas doué, vous avez souvent l'impression qu'il y a une lacune dans le calcul!

… À l'origine, je pense que vous devriez acquérir des connaissances dans des livres d'introduction (car c'est un type de données qui est standard dans la plupart des langues), mais y a-t-il une explication appropriée? Je pense aussi, alors j'ai décidé d'écrire un article approximatif.

Qu'est-ce qu'une virgule flottante?

Virgule fixe et virgule flottante

En un mot, la virgule flottante n'est pas une fraction que la personne moyenne imaginerait, mais des ** données numériques qui supposent que des erreurs se produiront dans le calcul **. Inversement, cela signifie également que ** lorsque vous utilisez des virgules flottantes, sachez qu'il s'agit d'un calcul approximatif et tenez compte de la précision du résultat **.

Vous vous demandez peut-être pourquoi ce n'est pas le cas, mais c'est le sort de ** l'informatique avec des ressources finies ** et le point de compromis. C'est la différence.

Le contraire de la virgule flottante est ** virgule fixe **, qui est généralement appelée données entières. Cela peut toujours être géré par incréments de 1, et tant que vous manipulez des entiers, il n'y aura pas d'erreur, mais ** la plage de nombres pouvant être gérée n'est pas très large **.

En revanche, la virgule flottante peut gérer une très large gamme de nombres ** au prix d'une erreur. Des nombres au niveau nanométrique proches de 0, aux constantes avocat (définitions récemment révisées) et aux nombres à l'échelle spatiale sont également disponibles.

Et dans la plupart des langages de programmation, il est courant de gérer des données fractionnaires en virgule flottante.

Pourquoi des fractions en virgule flottante?

Cela dit, certains peuvent penser cela. "Non, je n'ai pas besoin d'un tel nano ou d'un tel espace. Même si je gère environ 0,01 au maximum, pardonnez-moi toute erreur."

En fait, si vous ne traitez que les fractions qui apparaissent dans les devises et les taux d'imposition, ce sentiment est plausible.

Mais alors, combien de fractions doit-il couvrir en tant qu'ordinateur? Et ici, vous devriez déjà avoir appris à l'école primaire. Par exemple, ** le rapport de circonférence est une fraction infinie ** (3,14 n'est qu'une approximation). Même si vous ne faites pas ressortir le rapport de circonférence, le nombre irrationnel $ \ sqrt {2} $ que M. Pitagoras a essayé de sceller l'existence est aussi une fraction infinie, alors ** combien de chiffres devrais-je regarder **, c'est Il suit toujours dans le calcul des fractions. En fin de compte, comme il ne peut pas être exprimé comme fini, il doit être coupé quelque part, et nous devons tenir compte de la précision et de l'erreur de calcul. Comme pour les calculs dits scientifiques et technologiques, ce problème est inévitable dans les statistiques, le traitement graphique, etc.

Donc, ce n'est que ma supposition, mais il semble que les entiers soient à virgule fixe et les fractions à virgule flottante.

Si vous détestez toujours les erreurs

Cela dit, il peut arriver que vous ne vouliez pas penser aux erreurs. Dans ce cas, il devrait y avoir un langage ou une bibliothèque capable de gérer les points décimaux sans erreur.

Par exemple, dans l'ancien et notoire COBOL, le format décimal compressé permet des calculs sans erreur avec des fractions comprises dans le nombre de chiffres spécifié. On a juste dit que c'était pour la paperasse. Dans d'autres langages, par exemple, vous avez la possibilité d'utiliser BigDecimal pour Java, décimal pour C # et BigDecimal pour Ruby (ou Rational pour les entiers divisés par des entiers).

Ou, par exemple, si vous parlez de multiplier le taux de taxe à la consommation par 8% (bien que les virgules flottantes ne causent pas beaucoup d'erreur), vous pouvez simplement le remplacer par une arithmétique entière. Comme (montant) * 8 / 100 au lieu de (montant) * 0,08. Alternativement, au lieu d'exprimer 1,50 $ comme 1,50 $, vous pouvez concevoir une unité de 150 cents afin qu'elle tienne dans la plage des entiers.

Le point est, ** Ne le traitez pas comme une fraction facilement dans le programme simplement parce que c'est une fraction dans le monde réel, pensez d'abord **.

Mécanisme d'erreur

Représentation binaire

Cependant, d'un autre côté, vous vous demandez peut-être: "Pourquoi une erreur se produit-elle avec un nombre inexistant de 0,01?"

Il est toujours facile de comprendre que l'ajout d'un très grand nombre et d'un petit nombre, comme le suivant, ne correspondait pas à la précision qui peut être exprimée. (Dans Ruby (x86_64-Linux), il est traité comme une virgule flottante double précision, donc la précision est d'environ 16 chiffres en décimal)

Un exemple de Ruby qui ne rentre pas dans la précision et provoque une erreur


$ irb
irb(main):001:0> 100_000_000 + 0.000_000_01 #Convient à la précision
=> 100000000.00000001
irb(main):002:0> 1000_000_000 + 0.000_000_001 #Ne correspond pas
=> 1000000000.0

Cependant, ajouter 0,01 100 fois à 1 ne signifie pas qu'il y a une différence de taille ...?

Dans ce cas où "l'ajout de 0,01 100 fois ne donne pas 1", l'ordinateur traite en interne les virgules flottantes comme des nombres binaires, et ** 0,01 devient une fraction infinie (circulaire) en nombres binaires **. C'est la cause.

Notez qu'il existe une correspondance entre les fractions décimales et les fractions binaires comme suit: $ \begin{eqnarray} 0.5&(10)=&0.1&(2) \\\\ 0.25&(10)=&0.01&(2) \\\\ ~ &~&\vdots&~ \\\\ 0.015625&(10)=&0.000001&(2) \\\\ 0.0078125&(10)=&0.0000001&(2) \\\\ 0.00390625&(10)=&0.00000001&(2) \\\\ 0.001953125&(10)=&0.000000001&(2) \\\\ ~ &~&\vdots&~ \\\\ \end{eqnarray} $

Ensuite, vous pouvez voir (calculer) que 0,01 est une fraction de circulation binaire avec un cycle de 20 chiffres comme suit. $ 0.01(10)=0.000000101000111101011100001010001111010111\cdots(2) $ Par conséquent, sur l'ordinateur, les chiffres sont coupés au milieu et enregistrés. Ce chiffre tronqué affecte comme une erreur lorsque le calcul est répété.

Inversement, ** netteté binaire ** (devient une fraction finie), une fraction dont le dénominateur est une puissance de 2 (par exemple, $ \ frac 1 {128} = 0,0078125 (10) = 0,000000001 (2) $ ) Ne crée pas d'erreur. ** Il faut faire attention à le gérer en supposant qu'il y a une erreur, mais ce n'est pas toujours le cas où une erreur se produira **.

Même si on dit qu'il est coupé, cela ne signifie pas nécessairement que le résultat sera réduit (tronqué) car le traitement des fractions est effectué par "arrondi". La politique dépend du système de traitement, tel que l'arrondissement ou le rapprochement (traitement tel que l'arrondissement).

Démonstration d'erreur et représentation binaire

Maintenant. La virgule flottante est définie aujourd'hui par la norme appelée IEEE754.

Le plus important est la double précision 64 bits, mais à part cela, la précision simple 32 bits, la précision 4x 128 bits et dans l'apprentissage automatique qui est populaire de nos jours, la précision est inférieure à la précision simple (au lieu de cela, la quantité de calcul dépend du matériel). La semi-précision 16 bits est également utilisée.

Pour la représentation interne, [Comprendre les nombres en virgule flottante](/ tobira-code / items / 9a000264260a7ef8ca30) est détaillé, mais dans tous les cas, il est basé sur la représentation binaire suivante. $ (-1)^s\cdot 1.fff\cdots ff(2)\cdot 2^{e-bias} $

J'ai donc créé un programme C qui regarde ce qui se passe en tant que nombre binaire basé sur la représentation interne des points flottants et montre comment les erreurs sont créées par l'écriture. Cette fois, avec une précision unique (car il est difficile de voir s'il y a beaucoup de chiffres). Dans l'environnement de Windows10 / WSL + Ubuntu16 + gcc (x86_64) que j'ai essayé, le type float est équivalent.

Voici comment il se compile et s'exécute. En donnant 100 comme entrée, l'affichage binaire de $ \ frac 1 {100} = 0,01 $ est sorti, puis l'état du calcul de la brosse est sorti. (Sur le côté droit, il y a aussi un affichage binaire lorsque la multiplication équivalente est calculée.) Enfin, le résultat de l'addition est également affiché sous forme de nombre décimal.

float.c Compilez et exécutez


$ gcc -std=c99 -o float float.c
$ ./float <<< 100
in single precision calculation:
 1.0/100 = 0.01(10) =  1.01000111101011100001010(2) * 2^-7

continuous binary column additions

  0.000000101000111101011100001010  0.000000101000111101011100001010 (1/100=0.01)
+ 0.000000101000111101011100001010
 ---------------------------------
  0.00000101000111101011100001010   0.00000101000111101011100001010 (2/100=0.02)
+ 0.000000101000111101011100001010
 ---------------------------------
  0.00000111101011100001010001111   0.00000111101011100001010001111 (3/100=0.03)
+ 0.000000101000111101011100001010
 ---------------------------------
  0.0000101000111101011100001010    0.0000101000111101011100001010 (4/100=0.04)
+ 0.000000101000111101011100001010
 ---------------------------------
  0.0000110011001100110011001100    0.0000110011001100110011001101 (5/100=0.05)
…(Omission)
  0.111110101110000100111101        0.111110101110000101001000 (98/100=0.98)
+ 0.000000101000111101011100001010
 ---------------------------------
  0.111111010111000010011001        0.111111010111000010100100 (99/100=0.99)
+ 0.000000101000111101011100001010
 ---------------------------------
  0.111111111111111111110101        1.00000000000000000000000 (100/100=1)

 1.0/100+1.0/100+...+1.0/100(100times) = 0.99999934

Dans le cas d'une précision simple, la précision est de 24 chiffres binaires. Par conséquent, chaque fois que l'addition est répétée, la différence entre le total maintenu et le nombre d'additions apparaîtra, et l'erreur de coupure s'accumulera. (Vous pouvez voir qu'il y a déjà une erreur dans le dernier chiffre à la 5ème fois) En conséquence, si 0,01 est ajouté 100 fois avec une seule précision, ce sera 0,99999934, ce qui est légèrement inférieur à 1.

Source de démonstration

Vous trouverez ci-dessous la source de la démonstration (C99). Je pense qu'il est également bon de le coller autour de ideone et de l'exécuter.

float.c


#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <assert.h>

void test(int n);

int main(void) {
    int n=0;
    int ret=scanf("%d",&n);
    if ( ret!=1||n<=0 )
        n=100;
    test(n);
}

void binform_s1(char *dst,double d);
int binform_s2(char *dst,double d);

void test(int n) {
    const float t=1.0/n;

    char buf[256];
    binform_s1(buf,t);
    printf("in single precision calculation:\n"
           " 1.0/%d = %g(10) = %s\n\n",n,t,buf);

    float s=0;
    int c1=binform_s2(buf,t);
    char line[256];
    memset(line,'-',c1);
    line[c1]='\0';

    printf("continuous binary column additions\n\n");

    for ( int i=1; i<=n; i++ ) {
        s+=t;
        if ( i>1 )
            printf("+%s\n"
                   " %s\n",buf,line);
        char buf2[256],buf3[256];
        int c2=binform_s2(buf2,s);
        (void)binform_s2(buf3,(double)i/n);
        printf(" %s%*s %s (%d/%d=%g)\n",buf2,c1-c2,"",buf3,i,n,(double)i/n);
    }
    printf("\n"
           " 1.0/%d+1.0/%d+...+1.0/%d(%dtimes) = %.8f\n",n,n,n,n,s);
}

//Ci-dessous, fonctions auxiliaires
//Type d'entier par type de punition(chaîne de bits)Conversion en
static inline int32_t f2i(double d) {
    // type punning
    union {
        float f;
        int32_t i;
    } u = { .f=d };
    return u.i;
}

//Chaîne de représentation binaire à précision unique(mantisse*2^indice)Générer un
void binform_s1(char *dst,double d) {
    int32_t x=f2i(d);
    sprintf(dst,"%c1.%23s(2) * 2^%d",
            x>>31?'-':' ',"",((x>>23)&255)-127);
    for ( int i=22; i>=0; i-- )
        dst[25-i]='0'+(x>>i&1);
}

//Chaîne de représentation binaire à précision unique( 1.xx ... ou 0.Générer xx…)
int binform_s2(char *dst,double d) {
    int32_t x=f2i(d);
    int r=((x>>23)&255)-127;
    // support only small floats
    assert(r<=0);
    dst[0]=x>>31?'-':' ';
    memset(dst+1,'0',1-r);
    dst[2]='.';
    dst[r<0?2-r:1]='1';
    for ( int i=22; i>=0; i-- )
        dst[25-r-i]='0'+((x>>i)&1);
    dst[26-r]='\0';
    return 26-r;
}

Résumé

Enfin, c'est un résumé.

Recommended Posts

Qu'est-ce qu'une virgule flottante?
Qu'est-ce qu'un constructeur
Qu'est-ce qu'un flux
Qu'est-ce qu'un servlet?
Qu'est-ce qu'une classe wrapper?
Qu'est-ce qu'un type booléen?
Qu'est-ce qu'un commentaire significatif?
Qu'est-ce qu'un fichier JAR?
Qu'est-ce qu'une collection Java?
Qu'est-ce qu'une expression lambda?
Qu'est-ce que Fat⁉ enum?
Qu'est-ce qu'un extrait de code en programmation?
Qu'est-ce qu'un type booléen de colonne?
Qu'est-ce qu'une variable de type référence?
Qu'est-ce qu'une expression lambda (Java)
Qu'est-ce qu'un tableau bidimensionnel Ruby?
Qu'est-ce que Cubby
Qu'est-ce qui est nul? ]
Qu'est-ce qu'un fichier .original Spring Boot?
Qu'est-ce que 'java
Qu'est-ce que Keycloak
Qu'est-ce que maven?
Qu'est-ce que Jackson?
[Pour les débutants en programmation] Qu'est-ce qu'une méthode?
Qu'est-ce que soi
Qu'est-ce que Jenkins
Qu'est-ce que ArgumentMatcher?
Qu'est-ce que IM-Juggling?
Qu'est-ce qu'une classe en langage Java (1 /?)
Qu'est-ce que les paramètres
Qu'est-ce que SLF4J?
Qu'est-ce qu'une classe en langage Java (2 /?)
Qu'est-ce que la façade? ??
Qu'est-ce que Java <>?
Qu'est-ce que Gradle?
Qu'est-ce que POJO
Qu'est-ce que 'java
Qu'est-ce que centOS
Qu'est-ce que RubyGem?
[Rails] Qu'est-ce qu'un point (.) Ou un deux-points (:)?
Qu'est-ce que before_action?
Qu'est-ce que Docker
Qu'est-ce que Byte?
Qu'est-ce que Tomcat
Introduction aux fonctions récursives: qu'est-ce qu'une fonction récursive?
Qu'est-ce que l'assemblage Maven?
Qu'est-ce que `docker-compose up`?
Qu'est-ce que vue cli
Qu'est-ce qu'une interface
Qu'est-ce que le moi de Ruby?
Qu'est-ce que le codage en dur?
Qu'est-ce que l'attr_accessor de Ruby?
Qu'est-ce que l'encapsulation Java?
Qu'est-ce qu'une permission refusée?
Qu'est-ce que le contrôle d'instance?
Qu'est-ce qu'un initialiseur?
Qu'est-ce que Spring Tools 4