Java och C

Man måste tänka sig lite för hur man skriver numeriska program i Java och C. Här följer ett enkelt summaexempel. Antag att vi skall beräkna summan (med flyttal)

-1/4 - 1/3 -1/2 - 1 + 1 + 1/2 + 1/3 + 1/4

i Java. Summan borde bli noll, men på grund av avrundningsfel kommer den sannolikt inte att bli det. Här följer några olika loopar som beräknar summan:

    int    n = 4;
    float  s;
    double d;

    s = 0.0f;
    for(int k = -n; k <= n; k++)
      if (k != 0) s += 1.0 / k;

Jag börjar med att sätta s till noll, 0.0f är en enkelprecisionsnolla. Att i stället använda 0.0 (dubbel precision) går inte bra eftersom javac då ger felmeddelandet: "possible loss of precision" eftersom s är en float-variabel. n har värdet fyra och vi hoppar över k = 0 i loopen. Värdet på s blir 7.4505806E-8. Det är viktigt att notera att 1.0 är i dubbel precision, k konverteras då automatiskt till double, divisionen och additionen utförs i double, men partialsumman avrundas till float. Det kan man lätt kontrollera genom att studera bytekoden , eller lite mindre utförligt (koden produceras med javap -c Program). Den intressanta delen ser ut så här:

  14 fload_2    hämta s
  15 f2d        s -> double  (ty s är ju float)
  16 dconst_1   1.0 (täljaren)
  17 iload_3    hämta k
  18 i2d        k -> double
  19 ddiv       double division
  20 dadd       double add
  21 d2f        s -> float
  22 fstore_2   lagra s

I följande exempel tvingar jag täljaren att vara en float-etta (1.0f eller (float) 1.0).

    s = 0.0f;
    for(int k = -n; k <= n; k++)
      if (k != 0) s += 1.0f / k;

Så här blir bytekoden:

  14 fload_2    hämta s
  15 fconst_1   1.0f
  16 iload_3    hämta k
  17 i2f        k -> float
  18 fdiv       floating divide
  19 fadd       floating add
  20 fstore_2   lagra s

I detta fall görs alla beräkningar således i enkel precision och resultatet blir ett annat, -1.4901161E-7.

Det skall också uppföra sig på detta vis (enligt "The Java language specification" avsnitt 4.2.4).

Följande loop använder en double-variabel d för summan och resultatet , ungefär 2.8E-16, blir ju mycket bättre eftersom hela beräkningen utförs i dubbel precision.

    d = 0.0d;
    for(int k = -n; k <= n; k++)
      if (k != 0) d += 1.0 / (double) k;  // 1.0 / k går också bra

Observera att följande inte ger något bättre resultat:

    s = 0.0f;
    for(int k = -n; k <= n; k++)
      if (k != 0) s += 1.0 / k;
    d = s;  // konvertera till double
    

Eftersom vi mellanavrundar summan till enkel precision kan vi ju inte förvänta oss att konverteringen från float till double skall ge något. Vi får samma resultat som i första exemplet på denna sida. Ett enkelprecisionsresultat mitt i en dubbelprecisionsberäkning kan förstöra resultatet.


C uppför sig på samma sätt förutsatt att man inte tvingar kompilatorn att följa K & R-versionen av C (definitionen av C i boken av Brian Kernighan och Dennis Ritchie, "The C Programming Language", Prentice-Hall 1978). Så här står det i manualbladet för cc (Suns C-kompilator)

-X[a|c|s|t]

Specifies the degree of conformance to the ANSI/ISO C standard. Specifies one of the following:

a (ANSI)
ANSI C plus K&R C compatibility extensions, with semantic changes required by ANSI C. Where K&R C and ANSI C specify different semantics for the same construct, the compiler will issue warnings about the conflict and use the ANSI C interpretation. This is the default compiler mode.

c (conformance)
Strictly conformant ANSI/ISO C, without K&R C compatibility extensions. The compiler will issue errors and warnings for programs that use non-ANSI/ISO C constructs.

s (K&R C)
The compiled language includes all features compatible with (pre-ANSI) K&R C. The compiler tries to warn about all language constructs that have differing behavior between Sun ANSI/ISO C and the K&R C. Invokes cpp for processing. __STDC__ is not defined in this mode. (See the C Transition Guide for differences between ANSI/ISO and K&R C.)

Vi kompilerar de två looparna:

  int    n = 4;
  int    k;
  float  s;

  s = 0.0f;
  for(k = -n; k <= n; k++)
    if(k) s += 1.0 / k;
  printf("s = %13.6e\n", s);

  s = 0.0f;
  for(k = -n; k <= n; k++)
    if(k) s += 1.0f / k;    /* OBS 1.0f */
  printf("s = %13.6e\n", s);

% cc -Xc prog.c     cc prog.c ger samma resultat
% a.out             exekvera
s =  7.450581e-08   utskrifter
s = -1.490116e-07

Vi kompilerar sedan så att vi kan studera assemblerkoden (jag har bara skrivit ut flyttalsoperationerna). %f2 etc. är flyttalsregister i FPUn.

% cc -S -Xc prog.c  generera assemblerkod på filen fil.s

Första loopen

        fitod   %f2,%f2       int -> double
        fdivd   %f4,%f2,%f2   divide double
        faddd   %f6,%f2,%f2   add double
        fdtos   %f2,%f2       double -> single

Andra loopen

        fitos   %f2,%f2       int -> double
        fdivs   %f3,%f2,%f2   divide single
        fadds   %f4,%f2,%f2   add single

Nu kompilerar vi enligt K & R.

% cc -Xs prog.     kompilera enligt K & R
% a.out            exekvera
s =  7.450581e-08  samma
s =  7.450581e-08

Om man ser på assemblerkoden visar det sig att båda looparna ser ut som första loopen ovan. I K & R C gjordes alla beräkningar i double precision, men detta har ändrats i ANSI-standarden. Ett program som fungerade för några år sedan (eller med en gammal kompilator) behöver alltså inte fungera nu.

Back