Skip to content

Instantly share code, notes, and snippets.

@Commoble
Last active February 3, 2021 22:26
Show Gist options
  • Select an option

  • Save Commoble/ba723f3acb9b16509ca8d8a44a3e5f5a to your computer and use it in GitHub Desktop.

Select an option

Save Commoble/ba723f3acb9b16509ca8d8a44a3e5f5a to your computer and use it in GitHub Desktop.
Memory usage for blocks with lots of blockstates in minecraft forge

Let's say we have a block with lots of blockstates. How much memory does it use?

We can use Java Object Layout to get an estimate of how many bytes an instance of an object uses as described here: https://www.baeldung.com/jvm-measuring-object-sizes

So let's say we create a bunch of blocks with different blockstate properties. Java Object Layout has a graph-walking tool that will find every object reachable from a given instance, but most of these objects we find will be shared by other blocks.

However, Java Object Layout does let us get a graph layout for two objects and subtract the references in one from the other, so we can make two unique copies of each block and discard the duplicate references to get a better estimate of the additional RAM that creating a new block would allocate.

[15:37:09] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 27520 bytes
[15:37:09] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 1 boolean properties needs 2000 bytes
[15:37:10] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 2 boolean properties needs 4504 bytes
[15:37:10] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 3 boolean properties needs 8896 bytes
[15:37:10] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 4 boolean properties needs 17256 bytes
[15:37:10] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 5 boolean properties needs 36608 bytes
[15:37:10] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 6 boolean properties needs 74952 bytes
[15:37:10] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 7 boolean properties needs 159776 bytes
[15:37:11] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 8 boolean properties needs 333928 bytes
[15:37:11] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 9 boolean properties needs 716480 bytes
[15:37:11] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 10 boolean properties needs 1566984 bytes
[15:37:12] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 11 boolean properties needs 3475976 bytes
[15:37:14] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 12 boolean properties needs 7105736 bytes
[15:37:22] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 13 boolean properties needs 14935000 bytes
[15:37:42] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 14 boolean properties needs 30745160 bytes
[15:38:45] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 15 boolean properties needs 64890016 bytes

That first block with 0 properties looks like an outlier, so let's create some more blocks with 0 properties

[15:44:16] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 24960 bytes
[15:44:16] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:16] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:16] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:16] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:17] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:17] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:17] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:17] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:17] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:17] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:17] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:18] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:18] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:18] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes
[15:44:18] [modloading-worker-3/INFO] [STDOUT/]: [commoble.ramtest.RamTest:<init>:59]: Block with 0 boolean properties needs 696 bytes

That makes more sense.

Let's compile that into a nice table as well:

https://docs.google.com/spreadsheets/d/1TYl4gdsHtxcYSXImVDVqswCGzTcjwAzpmn29vNcKhTo/edit?usp=sharing

Why does ram per state increase with the number of states? Minecraft caches a lookup table for state transformations (e.g. calling state.with(someProperty, someValue)), which needs more combinations as the number of blockstate properties increases.

package commoble.ramtest;
import java.util.function.IntFunction;
import org.openjdk.jol.info.GraphLayout;
import net.minecraft.block.AbstractBlock;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.material.Material;
import net.minecraft.state.BooleanProperty;
import net.minecraft.state.StateContainer.Builder;
import net.minecraft.util.Util;
import net.minecraftforge.fml.common.Mod;
@Mod(RamTest.MODID)
public class RamTest
{
public static final String MODID = "ramtest";
public static final int ITERATIONS = 16;
// make a static set of properties so the blocks share property instances
public static final BooleanProperty[] PROPERTIES = Util.make(() ->
{
BooleanProperty[] result = new BooleanProperty[ITERATIONS];
for (int i=0; i < ITERATIONS; i++)
{
result[i] = BooleanProperty.create(String.valueOf(i));
}
return result;
});
public RamTest()
{
AbstractBlock.Properties props = AbstractBlock.Properties.create(Material.ROCK);
// we'll use an anonymous class so we can pass external input from the iterator into fillStateContainer
IntFunction<TestBlock> blockFactory = i -> new TestBlock(props)
{
@Override
public int getPropertyCount()
{
return i;
}
};
// create 16 pairs of blocks
// we'll just create them without registering them
// so we don't get slowed down by missing model spam
// we want to register two copies of each block we want to register
// that way, when we count the references reachable from each block instance,
// we can yeet most of the overhead that's shared by different block instances
for (int i=0; i<ITERATIONS; i++)
{
GraphLayout base = GraphLayout.parseInstance(blockFactory.apply(i));
GraphLayout actual = GraphLayout.parseInstance(blockFactory.apply(i)).subtract(base);
System.out.println(String.format("Block with %d boolean properties needs %d bytes", i, actual.totalSize()));
}
}
public static abstract class TestBlock extends Block
{
public TestBlock(Properties properties)
{
super(properties);
int propertyCount = this.getPropertyCount();
BlockState defaultState = this.getStateContainer().getBaseState();
for (int i=0; i<propertyCount; i++)
{
defaultState = defaultState.with(PROPERTIES[i], false);
}
this.setDefaultState(defaultState);
}
@Override
protected void fillStateContainer(Builder<Block, BlockState> builder)
{
super.fillStateContainer(builder);
int propertyCount = this.getPropertyCount();
for (int i=0; i<propertyCount; i++)
{
builder.add(PROPERTIES[i]);
}
}
public abstract int getPropertyCount();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment