Kerekítési hibák: C és Rust

 ( hg2ecz | 2018. május 22., kedd - 7:53 )

Belefutottam ismét egy érdekességbe. Kerekítési hibábát találtam egy C-ben írt jelfeldolgozó szoftverben. A kerekítési hiba leegyszerűsítve az alábbi példát lefordítva, lefuttatva látható:

#include <stdio.h>

int main() {
    const float bw_tort = 1./300.;

    int d1 = (int) (4./bw_tort);
    int d2 = (int) (4 /bw_tort);
    fprintf(stderr, "dec_taps: %d %d\n", d1, d2);

    return 0;
}

Megnéztem hogy a Rust mit csinál rá. Rust esetén az eredményt nem tudtam kerekítési hibára futtatni.

fn main() {
    let bw_tort: f32 = 1./300.;

    let d1: i32 = (4./bw_tort) as i32;
    let d2: i32 = (4 as f32/bw_tort) as i32;
    eprintln!("dec_taps: {} {}", d1, d2);
}

Update: időközben megnéztem, amennyiben argumentumként futásidőben kapta az 1-et és a 300-at a program, a C ugyanazt a kerekítési hibát produkálta, Rust esetén pedig szintén nem találtam lehetőséget kerekítési hiba előállítására.

A (float)4. és 4.f -es ötletet köszönöm.

A C továbbra is kedves marad, hiszen tapasztalat hogy az architektúrafüggő assembly-t leszámítva bármely más programnyelvnél gyorsabban futó jelfeldolgozó és egyéb kódot lehet benne írni. Viszont hibázni ahogy a fenti példa is mutatja, nagyon könnyen lehet benne.

Hozzászólás megjelenítési lehetőségek

A választott hozzászólás megjelenítési mód a „Beállítás” gombbal rögzíthető.

... es ha atirod a C-ben a float-ot double-ra akkor... akkor jo lesz :)

De az nem lenne ekvivalens kód.

Igen, mert ahogy lentebb is irjatok, a "4." az egy double. Ha a kicsereled a "4."-ot (float)4-re, akkor jo lesz:

apal@laptop:~$ cat x.c
#include 

int main() {
    const float bw_tort = 1./300.;

    int d1 = (int) ((float)4/bw_tort);
    int d2 = (int) (4 /bw_tort);
    fprintf(stderr, "dec_taps: %d %d\n", d1, d2);

    return 0;
}
apal@laptop:~$ gcc -Wall -pedantic -ansi -O3 -o x x.c
apal@laptop:~$ ./x
dec_taps: 1200 1200
apal@laptop:~$ 

A C program nálam is ezt adja ki, tehát reprodukálja a jelenséget:

"dec_taps: 1199 1200"

A C speckót most nem néztem meg, csak rákerestem, de az első stackoverflow-os találat:

"Casting to int truncates a floating-point number, that is, it drops the fractional part. The round function returns the nearest integer. Halfway cases are rounded away from zero, for example, round(-1.5) is -2 and round(1.5) is 2 ."

Az persze jó kérdés, hogy a 4-gyel és 4.0-val végzett művelet miért tér el, arra tippelnék, hogy az egyik 32 biten, a másik 64-en hajtódik végre. Összességében azt gyanítom, hogy nem lesz itt specifikációtól eltérés, egyszerűen a C aritmetika az ilyen funky módon működik.

A hardveres különbségek miatt a lebegőpontosok használata eleve kizárja a reprodukálható eredményt, fixpontos, vagy szoftverben megvalósított lebegőpontos libet kell használni, ha bitre reprodukálható eredmény kell.

Az egész aritmetikánál is a C nyelv tele van nem specifikált működésekkel, ezért ha azt akarjuk, hogy egy program mindenhol ugyanúgy működjön (az esetek 99.999%-ában erre kell törekedni), akkor szigorú szabályokat betartva lehet olyan kódot írni. Például a MISRA-C szabvány foglalkozik ezzel a kérdéskörrel is.

Megnéztem a generált ASM-et a gcc -S pelda.c paranccsal. Az egyik osztást 32 biten, a másikat 64 biten hajtja végre.

A Rust-ot nem ismerem.

"const float bw_tort = 1./300.;"
Erre: 'initializing': truncation from 'double' to 'float'

Csereld le erre:

const float bw_tort = 1.f / 300.f;

int d1 = (int)(4.f / bw_tort);
int d2 = (int)(4 / bw_tort);
fprintf(stderr, "dec_taps: %d %d\n", d1, d2);

+1
Rustban f32 a floating point literal alapértelmezett típusa, C-ben double.

Ha a különböző hardvereid ugyanazt a lebegőpontos formátumot valósítják meg (pl. IEEE 754 Single Precision), akkor reprodukálhatónak kell lennie.

És mi a hiba a C-programban? És mit értesz azon, hogy kerekítés?

$ ./hg2ecz 
dec_taps: 1199 1200

Szerk: ja, rájöttem, a lebegőpontos számábrázolás pontatlanságára gondolsz. Hát, az már bizony pontatlan.

A két program nem ekvivalens, ahogy kmkk is írta, így nincs értelme összehasonlítani. Amúgy a C és Rust csontra pontosan ugyanazokat az implicit-explicit castolásokat végzi el és ugyanúgy 0 felé kerekít a lebegőpontosról egészre történő konverziókor.

A különbség abban van, hogy a lebegőpontos literálok típusa Rustban f32 alapban, míg C-ben double. Így C-nél történik kerekítés, míg Rustban nem.

Szóval nem ott keresendő a hiba, hogy a Rust jobb, egyszerűen tudni kell, hogy mit írsz le, és az máshogy viselkedik.
Nem is értem azt a kifejezést itt, hogy "kerekítési hiba". Jól definiált minden egyes kifejezés értéke mindkét nyelvben, ami le van írva. Az, hogy más eredményt ad, az a nyelv szemantikájából adódik, és semmi köze ahhoz, hogy a Rust szigorúan veszi a típusokat, vagy nem.

A nem ennyire erősen típusos nyelveknek is szájbarághatod, pl.

      program kerekit
      implicit none
      integer n, i1, i2
      parameter( n=300 )
      real x1
      double precision x2
      x1 = real(1) / n
      x2 = dble(1) / n
      write(*, 1003) n
      i1 = 4 / x1
      i2 = 4 / x2
      write(*, 1001) i1
      write(*, 1002) i2
      stop
 1001 format('With single precision x1: 4 / x1 = ', i4)
 1002 format('With double precision x2: 4 / x2 = ', i4)
 1003 format('Real x1 and double x2 are both set to 1 / ',i3,'.')
      end

vagy

#include <stdio.h>

main(){
  int n = 300, i1, i2;
  float x1;
  double x2;
  x1 = ((float) 1) / n;
  x2 = ((double)1) / n;
  i1 = 4 / x1;
  i2 = 4 / x2;
  printf("x1 float, x2 double, both set to 1/%d.\n",n); 
  printf("4/x1 = %d, 4/x2 = %d\n", i1, i2);
}