Perché questo calcolo a virgola mobile fornisce risultati diversi su macchine diverse?

Ho una semplice routine che calcola le proporzioni da un valore in virgola mobile. Quindi per il valore 1.77777779, la routine restituisce la stringa “16: 9”. Ho provato questo sulla mia macchina e funziona bene.

La routine è data come:

public string AspectRatioAsString(float f) { bool carryon = true; int index = 0; double roundedUpValue = 0; while (carryon) { index++; float upper = index * f; roundedUpValue = Math.Ceiling(upper); if (roundedUpValue - upper  20) { carryon = false; } } return roundedUpValue + ":" + index; } 

Ora su un’altra macchina, ottengo risultati completamente diversi. Quindi sulla mia macchina, 1.77777779 dà “16: 9” ma su un’altra macchina ottengo “38:21”.

Ecco un bit interessante della specifica C #, dalla sezione 4.1.6:

Le operazioni in virgola mobile possono essere eseguite con maggiore precisione rispetto al tipo di risultato dell’operazione. Ad esempio, alcune architetture hardware supportano un tipo “esteso” o “lungo doppio” a virgola mobile con intervallo e precisione maggiori rispetto al tipo doppio e eseguono implicitamente tutte le operazioni in virgola mobile utilizzando questo tipo di precisione più elevata. Solo a costi eccessivi in ​​termini di prestazioni possono essere realizzate architetture hardware per eseguire operazioni in virgola mobile con minore precisione, e piuttosto che richiedere un’implementazione per rinunciare a prestazioni e precisione, C # consente di utilizzare un tipo di precisione più elevata per tutte le operazioni in virgola mobile . Oltre a fornire risultati più precisi, questo ha raramente effetti misurabili.

È ansible che questo sia uno degli “effetti misurabili” grazie a quella chiamata a Ceiling. Prendendo il limite di un numero in virgola mobile, come altri hanno notato, ingrandisce una differenza di 0,000000002 di nove ordini di grandezza perché trasforma 15.99999999 in 16 e 16.00000001 in 17. Due numeri che differiscono leggermente prima dell’operazione differiscono enormemente in seguito; la piccola differenza potrebbe essere spiegata dal fatto che macchine diverse possono avere più o meno “precisione extra” nelle loro operazioni in virgola mobile.

Alcuni problemi correlati:

  • C # XNA Visual Studio: Differenza tra le modalità “release” e “debug”?

  • Le ottimizzazioni JIT CLR violano la causalità?

Per risolvere il problema specifico su come calcolare un rapporto aspetto da un float: potrei risolverlo in un modo completamente diverso. Farei una tabella come questa:

 struct Ratio { public int X { get; private set; } public int Y { get; private set; } public Ratio (int x, int y) : this() { this.X = x; this.Y = y; } public double AsDouble() { return (double)X / (double)Y; } } Ratio[] commonRatios = { new Ratio(16, 9), new Ratio(4, 3), // ... and so on, maybe the few hundred most common ratios here. // since you are pinning results to be less than 20, there cannot possibly // be more than a few hundred. }; 

e ora la tua implementazione è

 public string AspectRatioAsString(double ratio) { var results = from commonRatio in commonRatios select new { Ratio = commonRatio, Diff = Math.Abs(ratio - commonRatio.AsDouble())}; var smallestResult = results.Min(x=>x.Diff); return String.Format("{0}:{1}", smallestResult.Ratio.X, smallestResult.Ratio.Y); } 

Si noti come il codice ora sia molto simile all’operazione che si sta tentando di eseguire: da questo elenco di rapporti comuni, scegliere quello in cui la differenza tra il rapporto dato e il rapporto comune è ridotta al minimo.

Non userei numeri in virgola mobile a meno che non dovessi davvero farlo. Sono troppo inclini a questo genere di cose a causa di errori di arrotondamento.

Puoi cambiare il codice per lavorare in doppia precisione? (il decimale sarebbe eccessivo). Se lo fai, dà risultati più coerenti?

Per quanto riguarda il motivo per cui è diverso su macchine diverse, quali sono le differenze tra le due macchine?

  • 32 bit contro 64 bit?
  • Windows 7 vs Vista vs XP?
  • Processore Intel vs AMD? (grazie Oded)

Qualcosa del genere potrebbe essere la causa.

Prova Math.Round anziché Math.Ceiling . Se si finisce con 16.0000001 e si termina, non si scarta questa risposta in modo errato.

Altri suggerimenti:

  • I doppi sono meglio dei galleggianti.
  • (double) 0.1 cast non necessario.
  • Potrebbe volere un’eccezione se non riesci a capire quali sono le proporzioni.
  • Se ritorni immediatamente dopo aver trovato la risposta, puoi carryon variabile carryon .
  • Un controllo forse più accurato sarebbe quello di calcolare le proporzioni per ogni ipotesi e confrontarla con l’input.

Revisionato (non testato):

 public string AspectRatioAsString(double ratio) { for (int height = 1; height <= 20; ++height) { int width = (int) Math.Round(height * ratio); double guess = (double) width / height; if (Math.Abs(guess - ratio) <= 0.01) { return width + ":" + height; } } throw ArgumentException("Invalid aspect ratio", "ratio"); } 

Quando l’indice è 9, ci si aspetterebbe di ottenere qualcosa come upper = 16.0000001 o upper = 15.9999999. Quale si ottiene dipenderà dall’errore di arrotondamento, che potrebbe differire su macchine diverse. Quando è 15.999999, roundedUpValue - upper <= 0.1 è true e il ciclo termina. Quando è 16.0000001, roundedUpValue - upper <= 0.1 è falso e il ciclo continua finché non si arriva index > 20 .

Invece forse dovresti provare a arrotondare la parte superiore al numero intero più vicino e controllare se il valore assoluto della sua differenza rispetto a quello intero è piccolo. In altre parole, usa qualcosa come if (Math.Abs(Math.Round(upper) - upper) <= (double)0.0001 || index > 20)