# Calcolo numerico

## Compilare un programma

Per eseguire un programma, un computer fa questa cosa: legge le informazioni
dall'input e le mette nella memoria; poi da qui legge le istruzioni
sequenzialmente e se c'è qualche calcolo da fare, lo fa fare alla ALU
(aritmetic logic unit) e memorizza il risultato nella memoria; alla fine passa
tutto in output.  
Per essere eseguibile, un programma deve essere scritto in codice macchina
e per questo si usano i linguaggi di programmazione che, attraverso il
compilatore, vengono tradotti in codice macchina:

- codice sorgente,
- compilatore,
- codice oggetto,
- linker (aggiunge le librerie),
- codice eseguibile.

## Errori di implementazione

Quando si passa dal modello astratto a quello implementato, si distinguono
tre tipi di errori:

- errori analitici: causati dalla necessità di una aritmetica discreta,
  cioè il fatto che non si possa rappresentare una cosa continua e quindi
  la si fa a punti;
- errori inerenti o algoritmici (o di round-off): causati dal numero finito di
  cifre significative. Dipendono dall'algoritmo utilizzato, perché ci sono
  metodi migliori di altri per evitare questo tipo di problemi.

## Rappresentazione di un numero

L'obiettivo è quello di tenere sotto controllo gli errori di round-off.
Per rappresentare un numero, esistono diverse rappresentazioni, tra cui quella
di Von Neumann: si dedicano $n$ cifre significative alla mantissa e poi un tot
anche alla caratteristica, cioè all'ordine di grandezza (che generalmente è in
base 2).  
Con $n$ cifre significative in base 2, il numero più alto rappresentabile è
$2^n -1$ perché con due cifre (0 e 1), ci sono $2^n$ possibili numeri
rappresentabili e, se posti in ordine crescente, ognuno dista dal precedente
un'unità, con il minimo che vale 0 (tutti 0), per cui il numero più alto che si
può rappresentare è $2^n -1$:  

- 0 è l'1,
- 1 e il 2,
- ...$2^n -1$ è il $2^n$-esimo.

questo numero corrisponde a un numero in base dieci di $m$ cifre significative,
per cui:

$$
  m = \log(2^n -1)
$$

quindi per un numero con 24 bit per la mantissa, si hanno circa 7 cifre
significative in base 10.  
In precisione semplice (floating point):

- un bit per il segno,
- 23 bit per la mantissa (quindi 7 cifre significative),
- 8 bits per la caratteristica.

In precisione doppia (duble) si raddoppiano i bite alla mantissa e le cifre
significative diventano 17, però il tempo per le moltiplicazioni è triplicato
e si occupa più memoria, quindi non è che convenga moltissimo.

Esiste anche la rappresentazione fixed-point, in cui c'è un bit per il segno
e i restanti per il numero in sé, in cui da qualche parte c'è la virgola.
Nel caso del fixed pointd, l'errore relativo può variare moltissimo a seconda
del numero rappresentato, mentre per i float è lo stesso per ogni numero: la
densità relativa non è costante.  
Quando un insieme di rappresentazione non è chiuso rispetto ad un'operazione,
il risultato presenta sicuramente errore di round-off e deve essere
approssimato.


## Errori algoritmici

Per numeri troppo grandi o troppo piccoli che non si riesce a
rappresentare, si parla rispettivamente di overflow o underflow. Per questo
motivo, somme e soprattutto differenze di numeri grandi portano grandi errori
dovuti all'arrotondamento: bisogna cercare di evitarlo se possibile. Per
esempio, per calcolare un polinomio, conviene usare il metodi di
Ruffini-Horner perché riduce il numero di operazioni da effettuare:

- metodo semplice:

  $$
    p(x) = a_0 + a_1x + a_2x^2 + ... +a_nx^n
  $$

  che sono n addizioni e n(n+1)/2 moltiplicazioni, quindi $\sim n^2$.
- metodo di R-H:

  $$
  p (x) = (((((a_n)x +a_{n-1})x +a_{n-2}x +...
  $$

  che sono n addizioni e n moltiplicazioni, quindi $\sim n$.

Il problema si verifica anche se si sottraggono due numeri molto simili tra
loro, perché il risultato è un numero molto piccolo che potrebbe finire in
underflow (si parla di "cancellazione catastrofica").

\textcolor{orange}{oss:} Il costo computazionale della risoluzione di un
sistema lineare in $n$ equazioni è asintoticamente uguale al costo del
prodotto di due matrici $nxn$. Esistono algoritmi che non richiedono più di
$k \cdot n^{\alpha}$ operazioni, col il più piccolo valore noto di $\alpha$
pari a 0.2375.

Alcuni algoritmi sono definiti "instabili": accade quando gli errori di
round-off si accumulano fino a portare a risultati completamente errati.  
Esistono problemi detti "mal condizionati" che con qualsiasi algoritmo danno
errori talmente elevati da rendere il risultato privo di significato. In
questi casi, piccole variazioni dei dati iniziali portano a grandi variazioni
nei risultati. Si chiama "numero di condizionamento del problema" il seguente
rapporto:

$$
  \frac{\Delta r}{\Delta \alpha} =
  \frac{\text{\% errore risultato}}{\text{\% errore dato iniziale}}
$$

che può quindi essere espresso in questo modo:

$$
  \Delta \alpha = \frac{x + h - x}{x} = \frac{h}{x}
  \hspace{100pt}
  \Delta r = \frac{f(x + h)-f(x)}{f(x)}
$$
$$
  \frac{\Delta r}{\Delta \alpha} =
  \frac{f(x + h)-f(x)}{h} \cdot \frac{x}{f(x)} =
  f'(x) \cdot \frac{x}{f(x)}
$$

Se questo numero è $<< 1$, allora il problema è poco sensibile ai dati
iniziali.

