Come evitare ripetute istanze e garbage collection per una classe banale?

1

Sto sviluppando un gioco RPG a mattonelle 2D per desktop e (si spera) per Android. Per rappresentare una posizione del riquadro nel gioco, sto utilizzando una classe chiamata Location , che ha un valore per x , y e room . room si riferisce al Room di Location . Io uso la classe Location per i calcoli come se una tessera sia passabile o invalicabile, la piastrella che un personaggio sta affrontando, ecc. Inizialmente, ho usato x , y e room valori per le classi di personaggi, oggetti di gioco, ecc., ma hanno deciso di sostituirli con istanze di Location .

Da qui , qui , e altrove, ho l'impressione che l'istanziazione di questi oggetti Location più e più volte non sia l'ideale per le prestazioni, specialmente sulle piattaforme Android. Ma sembra anche come mettere in comune (vedi i link) Location s è troppo lavoro da implementare. Ad esempio, posso ottenere un nuovo Location da un Location e un Direction (un enum) chiamando Location.add(Direction) che userei nei calcoli del movimento dei personaggi (a seconda di quale Direction il giocatore sta dirigendo) . Sarebbe estremamente scomodo dover chiamare manualmente "libero" su un oggetto che intendo utilizzare solo in una riga, ad esempio in pseudocodice character.setLocation(character.getLocation().add(character.facingDirection)) versus

var tempLocation = character.getLocation().add(character.facingDirection) // assuming for convenience that the add method uses a global pool of Location s
character.setLocation(tempLocation)
tempLocation.free() // assuming this frees it from the global pool

L'idea di utilizzare una classe Location invece di x , y e room valori in ogni oggetto pertinente è quella di semplificare il processo di codifica, non di renderlo più complesso. (E, naturalmente, mi sembra il modo "corretto" orientato agli oggetti). Esistono altri modi validi per mantenere prestazioni decenti senza evitare la classe Location del tutto? Oppure, in questo caso, anche le prestazioni sono un problema in questo caso?

Per motivi di completezza, sto usando Java, Scala e LibGDX.

    
posta sneelhorses 09.07.2016 - 21:51
fonte

2 risposte

5

Istanziare e collezionare oggetti temporanei, di breve durata, è perfetto. È quello che i moderni spazzini sono bravi a.

I moderni garbage collector (generazionali) sono costruiti su un paio di presupposti: la maggior parte degli oggetti muore giovane, la maggior parte degli oggetti sono piccoli, la maggior parte degli oggetti non sfugge, la maggior parte degli oggetti sono immutabili, gli oggetti più vecchi non contengono riferimenti agli oggetti più giovani. Solo gli oggetti che violano questi presupposti sono costosi, e gli oggetti soddisfano queste ipotesi. L'allocazione di un oggetto nella nuova generazione è semplicemente un puntatore (proprio come l'allocazione dello stack in C, e più economico di malloc ) e la raccolta della nuova generazione è O (#survivors), quindi se la maggior parte degli oggetti è morta, la raccolta è anche economica .

Riutilizzare e raggruppare oggetti OTOH li viola: allunga artificialmente la durata degli oggetti, il pool stesso è grande, i riferimenti diventano non-locali, gli oggetti sono inutilmente mutabili (e mutati inutilmente). A meno che tu abbia specifiche prove contrarie, il riutilizzo degli oggetti e il raggruppamento stanno andando a ferire GC, non aiutarlo.

Potresti notare che le ipotesi sono banalmente vere per la programmazione funzionale. Infatti, anche se la maggior parte dei moderni garbage collector sono sviluppati per le piattaforme OO imperative (ad es. Sun's G1 per HotSpot, IBM Metronome per J9, .NET GC, Mono's scgen, Immix, Azul's Pauseless Collector, ecc.), Finiscono per essere abbastanza buoni a FP. Ad esempio, Azul ha dimostrato di eseguire il famoso Clojure Ant Simulation Example su un core 864 con 768 GiByte di RAM con 700 thread paralleli che generano 20 GiByte / s di garbage, e il GC non ha mai indotto pause evidenti e non ha mai avuto bisogno di più di 20-30 core (o in altre parole mai più del ~ 30% delle CPU disponibili).

    
risposta data 10.07.2016 - 19:30
fonte
0

La raccolta dei rifiuti può diventare un problema di prestazioni nei giochi e potrebbe richiedere approcci speciali. Tuttavia, prima che tu sia sicuro che la raccolta dati obsoleti di istanze temporanee Location sia problematica, rischi di perdere tempo cercando di limitare la quantità di istanze: "L'ottimizzazione prematura è la radice di tutto il male."

Per decidere se seguire questa strada, devi prima sapere se la tua applicazione è "troppo lenta". Stai mirando a un framerate specifico sul tuo hardware di riferimento? Vuoi limitare il consumo di memoria a meno di 1 GB? Qualunque sia il tuo obiettivo, definiscilo esplicitamente, quindi misuralo. Per trovare la fonte della lentezza, possiamo usare gli strumenti del profiler per guidarci verso quelle aree di codice che usano più tempo. Concentrarsi su questi avrà l'effetto maggiore.

In ogni caso, è improbabile che un pool di classi abbia un effetto positivo sulle prestazioni per una semplice classe Location . Sto assumendo una definizione equivalente a questo Java:

final class Location /* no interfaces, no parent classes */ {
  private final int x;
  private final int y;
  ... // methods and constructors
}

Una classe del genere è facilmente ottimizzabile. Se tale Location è usato come variabile locale (come nel tuo esempio) nel codice compilato con JIT, c'è una buona probabilità che l'oggetto non venga mai raccolto in modo esplicito perché può essere allocata a basso costo nello stack e i campi dell'oggetto possono essere allineati. In teoria, questo può essere altrettanto efficiente quanto avere due variabili int location_x, location_y e usare solo metodi statici su di esse.

In ogni caso, la mia intuizione indica che il mantenimento di una cache corretta sarà probabilmente molto più costoso della raccolta di dati inutili, soprattutto se la cache deve essere sincronizzata tra più thread. Certo, la mia intuizione potrebbe essere sbagliata e solo i dati rigidi possono dircelo con certezza - e l'unico modo per scoprirlo è eseguire un benchmark prima / dopo un simile cambiamento.

La tua soluzione proposta che richiede una chiamata manuale free() è abbastanza problematica, poiché reintroduce la possibilità di perdite di memoria. L'esperienza con C e C ++ mostra che le perdite di memoria sono estremamente facili da creare se non sono curative. Nel codice

Location l = ...;
something();
l.free();

se something() genera, la posizione non verrà mai liberata, una perdita di memoria. Questo può essere risolto con un blocco try … finally o con una risorsa try-with, ma è piuttosto ingombrante per i valori temporanei. Il C ++ moderno aiuta i programmatori a limitare questi problemi distinguendo tra oggetti posseduti e presi in prestito e da distruttori deterministici automatici che vengono eseguiti anche quando viene lanciata un'eccezione. Java non ha nessuna di queste funzionalità perché, in quanto linguaggio di raccolta dei rifiuti, ha meno possibilità di utilizzarle.

Se si scopre che in realtà devi evitare di allocare troppe località, puoi semplicemente renderle modificabili. Il tuo Location#add() sembra creare una nuova posizione, ma se si aggiorna la posizione esistente, è possibile evitarlo. Un metodo clone() correttamente implementato consente di copiare una posizione se è necessaria un'istanza separata. Questo è coerente con il modo in cui la maggior parte degli oggetti Java già funziona, sebbene sia un po 'brutto in Scala. Mentre c'è una potenziale fonte di bug nel dimenticare sempre clone() dove necessario, questo è molto più facile da ottenere che ricordare ricordando che tutto è correttamente free() d.

    
risposta data 09.07.2016 - 22:46
fonte