Gestire i numeri in virgola mobile in modo deterministico
Il virgola mobile è deterministico. Bene, dovrebbe essere. È complicato.
C'è molta letteratura sui numeri in virgola mobile:
E come sono problematici:
Per astratto. Almeno, su un singolo thread, le stesse operazioni, con gli stessi dati, avvenute nello stesso ordine, dovrebbero essere deterministiche. Quindi, possiamo iniziare preoccupandoci degli input e riordinando.
Uno di questi input che causa problemi è il tempo.
Prima di tutto, dovresti sempre calcolare lo stesso timestep. Non sto dicendo di non misurare il tempo, sto dicendo che non passerai il tempo alla simulazione fisica, perché le variazioni nel tempo sono una fonte di rumore nella simulazione.
Perché si misura il tempo se non lo si passa alla simulazione fisica? Si desidera misurare il tempo trascorso per sapere quando deve essere chiamato un passaggio di simulazione e - supponendo che si stia utilizzando il sonno - quanto tempo per dormire.
Così:
- Misura tempo: Sì
- Usa il tempo nella simulazione: No
Ora, riordino delle istruzioni.
Il compilatore potrebbe decidere che f * a + b
è lo stesso di b + f * a
, tuttavia che potrebbe avere un risultato diverso. Potrebbe anche compilare in fmadd , oppure potrebbe decidere di prendere più righe come quelle che accadono insieme e scriverle con SIMD , o qualche altra ottimizzazione a cui non riesco a pensare in questo momento. E ricorda che vogliamo che le stesse operazioni avvengano nello stesso ordine, è ovvio che vogliamo controllare quali operazioni avvengono.
E no, usare double non ti salverà.
È necessario preoccuparsi del compilatore e della sua configurazione, in particolare per sincronizzare i numeri in virgola mobile attraverso la rete. Devi fare in modo che le build accettino di fare la stessa cosa.
Probabilmente, scrivere l'assemblea sarebbe l'ideale. In questo modo decidi quale operazione fare. Tuttavia, ciò potrebbe rappresentare un problema per il supporto di più piattaforme.
Così:
Il caso dei numeri a virgola fissa
A causa del modo in cui i float sono rappresentati in memoria, i valori di grandi dimensioni perderanno precisione. È ovvio che mantenere i valori piccoli (clamp) mitiga il problema. Quindi, nessuna velocità enorme e nessuna stanza grande. Ciò significa anche che puoi usare la fisica discreta perché hai meno rischi di tunnel.
D'altra parte, si accumuleranno piccoli errori. Quindi, troncare. Voglio dire, ridimensionare e trasmettere a un tipo intero. In questo modo sai che non sta succedendo nulla. Ci saranno operazioni che puoi fare rimanendo con il tipo intero. Quando devi tornare in virgola mobile, esegui il cast e annulla il ridimensionamento.
Nota dico scala. L'idea è che 1 unità sarà effettivamente rappresentata come una potenza di due (16384 per esempio). Qualunque cosa sia, rendila una costante e usala. In pratica lo stai usando come numero in virgola fissa. In effetti, se è possibile utilizzare molto meglio i numeri a virgola fissa di una libreria affidabile.
Sto dicendo troncato. Per quanto riguarda il problema degli arrotondamenti, significa che non puoi fidarti dell'ultimo bit di qualunque valore tu abbia ottenuto dopo il cast. Quindi, prima della scala del cast per ottenere un po 'più del necessario e troncarlo in seguito.
Così:
- Mantenere i valori piccoli: Sì
- Arrotondamento attento: Sì
- Numeri in punti fissi quando possibile: Sì
Aspetta, perché hai bisogno di virgola mobile? Non potresti lavorare solo con un tipo intero? Oh giusto. Trigonometria e radicazione. Puoi calcolare le tabelle per la trigonometria e la radicazione e farle cuocere nella tua fonte. In alternativa, è possibile implementare gli algoritmi utilizzati per calcolarli con il numero in virgola mobile, ad eccezione dell'utilizzo di numeri in virgola fissa. Sì, è necessario bilanciare memoria, prestazioni e precisione. Tuttavia, potresti rimanere fuori dai numeri in virgola mobile e rimanere deterministico.
Sapevi che hanno fatto cose del genere per la PlayStation originale? Per favore, incontra il mio cane, patch .
A proposito, non sto dicendo di non usare la virgola mobile per la grafica. Solo per la fisica. Voglio dire, certo, le posizioni dipenderanno dalla fisica. Tuttavia, come sai, un collider non deve abbinare un modello. Non vogliamo vedere i risultati del troncamento dei modelli.
Pertanto: UTILIZZARE NUMERI DI PUNTI FISSI.
Per essere chiari, se puoi usare un compilatore che ti consente di specificare come funzionano i punti mobili e che è abbastanza per te, allora puoi farlo. Questa non è sempre un'opzione. Inoltre, lo stiamo facendo per determinismo. I numeri a virgola fissa non significano che non ci siano errori, dopo tutto hanno una precisione limitata.
Non credo che "i numeri in virgola fissa siano difficili" è una buona ragione per non usarli. E se vuoi una buona ragione per usarli, è il determinismo, in particolare il determinismo su tutte le piattaforme.
Guarda anche:
Addendum : sto suggerendo di mantenere piccole le dimensioni del mondo. Detto questo, sia OP che Jibb Smart evidenziano il fatto che allontanarsi dai galleggianti di origine ha meno precisione. Ciò avrà un effetto sulla fisica, uno che sarà visto molto prima del limite del mondo. I numeri a virgola fissa, beh, hanno una precisione fissa, saranno ugualmente buoni (o cattivi, se preferisci) ovunque. Il che è positivo se vogliamo determinismo. Voglio anche menzionare che il modo in cui normalmente facciamo la fisica ha la proprietà di amplificare piccole variazioni. Vedi The Butterfly Effect - Deterministic Physics in The Incredible Machine and Contraption Maker .
Un altro modo di fare fisica
Ho pensato, il motivo per cui il piccolo errore di precisione nei numeri in virgola mobile si amplifica è perché stiamo facendo iterazioni su quei numeri. Ogni passaggio di simulazione prendiamo i risultati dell'ultimo passaggio di simulazione e facciamo cose su di essi. Accumulo di errori in cima ad errori. Questo è il tuo effetto farfalla.
Non credo che vedremo un singolo build utilizzando un singolo thread sulla stessa macchina che genererà output diversi dallo stesso input. Eppure, su un'altra macchina potrebbe, o potrebbe essere una build diversa.
C'è un argomento per testare lì. Se decidiamo esattamente come dovrebbero funzionare le cose e possiamo testare sull'hardware di destinazione, non dovremmo creare build con un comportamento diverso.
Tuttavia, c'è anche un argomento per non lavorare in trasferta che accumula così tanti errori. Forse questa è un'opportunità per fare fisica in modo diverso.
Come forse saprai, esiste una fisica continua e discreta, entrambe lavorano su quanto ciascun oggetto avanzerebbe nel momento in cui si verifica la data. Tuttavia, la fisica continua ha i mezzi per capire l'istante della collisione invece di sondare diversi istanti possibili per vedere se si è verificata una collisione.
Pertanto, sto proponendo quanto segue: utilizzare le tecniche della fisica continua per capire quando accadrà la prossima collisione di ciascun oggetto, con un grande intervallo di tempo, molto più grande di quello di un singolo passaggio di simulazione. Quindi prendi l'istante di collisione più vicino e scopri dove sarà tutto in quell'istante.
Sì, questo è molto lavoro di un singolo passaggio di simulazione. Ciò significa che la simulazione non si avvierà all'istante ...
... Tuttavia, è possibile simulare i passaggi di simulazione successivi senza controllare ogni volta la collisione, poiché si sa già quando si verificherà la collisione successiva (o che non si verifica alcuna collisione nel timestep di grandi dimensioni). Inoltre, gli errori accumulati in quella simulazione sono irrilevanti perché una volta che la simulazione raggiunge il grande timestep, posizioniamo semplicemente le posizioni calcolate in precedenza.
Ora, possiamo usare il budget temporale che avremmo usato per verificare le collisioni in ogni fase della simulazione per calcolare la collisione successiva dopo quella trovata. Cioè possiamo simulare in anticipo usando il timestep di grandi dimensioni. Supponendo che un mondo di portata limitata (questo non funzionerà per giochi enormi), ci dovrebbe essere una coda di stati futuri per la simulazione, e quindi ogni fotogramma che interpoli dall'ultimo stato a quello successivo.
Vorrei discutere per l'interpolazione. Tuttavia, dato che ci sono accelerazioni, non possiamo semplicemente interpolare tutto allo stesso modo. Invece dobbiamo interpolare tenendo conto dell'accelerazione di ciascun oggetto. Per questo motivo, potremmo semplicemente aggiornare la posizione nello stesso modo in cui facciamo per il timestep di grandi dimensioni (il che significa anche che è meno soggetto a errori perché non utilizzeremmo due implementazioni diverse per lo stesso movimento).
Nota : se stiamo eseguendo questi numeri in virgola mobile, questo approccio non risolve il problema degli oggetti che si comportano in modo diverso tanto più lontano dall'origine che sono. Tuttavia, mentre è vero che la precisione si perde quanto più si allontana dall'origine, ciò è ancora deterministico. In effetti, questo è il motivo per cui non è stato nemmeno sollevato inizialmente.
appendice
Da OP nel commento :
L'idea è che i giocatori saranno in grado di salvare le loro macchine in un formato (come xml o json), in modo che la posizione e la rotazione di ogni pezzo vengano registrate. Quel file xml o json verrà quindi utilizzato per riprodurre la macchina sul computer di un altro lettore.
Quindi, nessun formato binario, giusto? Ciò significa che dobbiamo anche preoccuparci che i numeri in virgola mobile recuperati corrispondano o meno all'originale. Vedi: Float Precision Revisited: Portabilità del galleggiante a nove cifre