Skip to content

[1.12.2] Optimize wire system powered checks#4700

Merged
AlexIIL merged 6 commits into
BuildCraft:8.0.x-1.12.2from
bepis-server:8.0.x-1.12.2
Aug 4, 2023
Merged

[1.12.2] Optimize wire system powered checks#4700
AlexIIL merged 6 commits into
BuildCraft:8.0.x-1.12.2from
bepis-server:8.0.x-1.12.2

Conversation

@DaMatrix
Copy link
Copy Markdown
Contributor

This PR optimizes WireManager#isPowered(EnumWirePart) and methods used by it in the following ways:

  • The very expensive WireSystem#hashCode() is pre-computed and stored in a field
    • This makes the HashMap lookups when accessing WorldSavedDataWireSystems#wireSystems significantly faster (HashMap#get(Object) previously took around 30% of WireManager#isPowered(EnumWirePart)'s total CPU time according to my profiling, and now doesn't even get picked up by VisualVM any more)
  • Added a Map<WireSystem.WireElement, List<WireSystem>> to WorldSavedDataWireSystems which makes it possible to implement WorldSavedDataWireSystems#getWireSystemsWithElement(WireSystem.WireElement) efficiently
    • The previous implementation iterated over every WireSystem.WireElement in every WireSystem on every call to WorldSavedDataWireSystems#getWireSystemsWithElement(WireSystem.WireElement), which doesn't scale very well (especially when you consider that it gets called once per tick, per gate trigger, per pipe wire attached to the same pipe as the gate)
  • Replaced stream API usage in various hot spots in WireManager/WorldSavedDataWireSystems with the fully written-out "normal" code, as the very deep call chains caused by using streams were badly limiting the JVM's ability to inline things, and also filling up the heap very quickly with a lot of garbage (no inlining -> no escape analysis -> objects actually land on the heap -> incalculable pain and suffering)
    • No, this isn't just micro-optimization, it actually gave me a ~2.5x boost by itself
  • (Not an optimization) Made WireSystem immutable, as modifying it would have affected the hash code changing (would have been annoying to deal with invalidating the cached hash code), not to mention that allowing it to be modified kinda conflicts with its primary role as a HashMap key in WorldSavedDataWireSystems. IMO this is much cleaner and will certainly prevent anyone from trying to use it incorrectly, however the rest of the optimizations can still function without this change if necessary.

Extra bonus fixes and improvements:

  • Used Map#replaceAll(BiFunction) in WorldSavedDataWireSystems#tick() to avoid a pointless put for every WireSystem every time a gate changes
  • Used World#markChunkDirty(BlockPos, TileEntity) in TileBC_Neptune#markChunkDirty() rather than explicitly getting the chunk and calling markDirty(), this ensures the tile entity actually gets saved when using Cubic Chunks is being used

All together, this reduced the total CPU time spent in WireManager#isPowered(EnumWirePart) by as much as 200x (times, not percent!) on one of my larger worlds, shaving off a solid 30ms per tick.

Not sure about code style, let me know if I should change anything :)

this avoids collecting the intermediate result from WorldSavedDataWireSystems#getWireSystemsWithElement(WireSystem.WireElement) into a List just to get a stream over the same list again, and instead evaluates the entire expression directly.

that said, WorldSavedDataWireSystems#getWireSystemsWithElement(WireSystem.WireElement) should still be able to be optimized a lot further (it's currently O(N), lol)
this makes WireSystem#hashCode() cache its return value, which previously accounted for most of WireManager#isPowered's CPU time.

...and this can still be optimized even further to avoid iterating over every wire system in the world on every operation!
this will make it much safer to implement the next optimizations to WorldSavedDataWireSystems#getWireSystemsWithElement(WireSystem.WireElement)
we now use an index in WorldSavedDataWireSystems to keep track of which WireSystem each WireSystem.WireElement  belongs to, which allows us to avoid iterating over every element of every wire system in the world for every pipe gate every tick.
…ity) instead of explicitly marking the chunk as dirty

this will make it play nicer with cubic chunks
if (chunk != null) {
chunk.markDirty();
}
world.markChunkDirty(this.pos, this);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Funnily enough this itself was an optimization - going through our cache was cheaper then calling BlockEntity.markDirty. I'm not sure if I tried using World.markChunkDirty though - since it does less work.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I guessed that was the point, however I figured it wasn't much of a performance concern as it wasn't showing up in profiling results at all, and the vanilla version of World.markChunkDirty isn't too much different than the existing one (hashtable lookup through ChunkProviderServer instead of accessing the cached chunk instance, but presumably negligible impact if I already couldn't measure it before).

However in Cubic Chunks worlds World.markChunkDirty actually marks the cube as dirty, which is important because otherwise the cube might not get saved. (I actually saw this happen in practice - a BuildCraft quarry in a Cubic Chunks world piping items into a crate from another mod which was also not using World.markChunkDirty resulted in the mined blocks not being saved, even though the blocks broken by the quarry were!)

If you're not certain about this change I can totally move it into a separate PR. I just threw it in here since I figured it wouldn't make too much difference, but wouldn't mind profiling it in more depth if you'd like.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Back in my day, what we would do is put certain optimizations behind compatibility flags (f.e. we had an important optimization in pipes that broke on Cauldron-based servers). Maybe this would be the way to go here?

@AlexIIL
Copy link
Copy Markdown
Member

AlexIIL commented Apr 11, 2023

Thanks for looking into this! I haven't actually tested it yet but it all looks good.

@AlexIIL
Copy link
Copy Markdown
Member

AlexIIL commented Aug 4, 2023

Sorry for the delay - this works great, and I'll push it out soon

@AlexIIL AlexIIL merged commit 1027c9c into BuildCraft:8.0.x-1.12.2 Aug 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants