Handling heavy load on the GC with Scala Case Clas

2019-08-02 00:22发布

问题:

I'm developing a simulation in Scala inside a game. I would like to use only immutable objects to define the game logic even if it's inefficient. Why? Because I can, so I will.

Now I'm stressing a bit the main game loop with pushing the simulation with a huge load. Basically I have these nested case classes and I operate with .copy() to define the following state of a given entity inside the simulation. The problem is that all these operations generate a lot of unreferenced objects and so at the end of every step, I trigger a Full GC. This is bad for the game performances.

So I started some aggressive optimization: first of all now the simulation step is running in parallel behind the game, computing a "next state" that basically covers a day in the game. So the player will be shown the state from the current day while the next one is being computed. This works because basically every major entity in the game (cities, basically) is assumed to evolve in isolation. If some agent (the player or other AI agent traveling between cities) will get in contact with the city, I will recompute the "next state" according to the actions operated by the agent. This is not relevant now anyway.

So I have these parallel entities evolving behind the scene and when a day is over (a day is defined as 5 steps of the player in the world map) I use Await.result(nextWorldState, 5 seconds) as a rendez-vous point to update the current state of the simulation. This is not necessarily how the game should run but I want to test a rendez-vous point when the rest of the game wait for the next state to be computed, because this will be probably necessary.

My problem is that after this, I dereference a lot of objects all at once and this trigger the GC collection. I tried some solutions but there's always a point when I access the result of the Future and replace the current state with it and this always trigger the GC.

Assuming that I don't want to go for regular classes (probably I will) and assuming that I don't want to split the state in multiple pieces and process them separately (I will try this but it looks unmaintanable), is there any clever solution to this problem?

Here is some not-so-pseudo code from the actual function that handle this logic:

class WorldSimulationManager(var worldState: WorldState) {

  private var nextWorldState: Future[WorldState] =
    Future.successful(worldState)


  def doImmutableStep(gameMap:GameMap,movedCountToday:Int):Unit = {

    nextWorldState =
      nextWorldState.flatMap(_.doStep(gameMap))

    if (movedCountToday >= Constants.tileMovementsToDay) {
      worldState = Await.result(nextWorldState, 5 seconds)

    }
  }

}

回答1:

To reduce the pain of a Full GC I suggest using G1 or CMS rather than parallel collector, and increaing the young space to reduce the number of objects promoted to tenured space however nothing beats creating less work in the first place.

The more garbage you create, the more work you do and the more work the gc has to do to clean up the objects. Creating objects faster with more CPUs won't make the GC less painful.