Minecraft is a block-based game, and due to that interactions with blocks are some of the most fundamental things that you can do. Over time as the game has become more complex, the actual time it costs to lookup the type of a block has substantially increased. This article goes over a few benchmarks to actually measure how much slower this has gotten over a few key versions.

Why this matters?

Block lookups happen a lot, much more than you’d expect. So many basic gameplay features require doing a significant amount of them. Entities like bees for example must scan nearby blocks looking for flowers, and frogs must scan nearby blocks to check that their jumps are unobstructed. Even blocks can often do block lookups, saplings and other plants need to check surrounding blocks before growing to make sure they’ll fit, and redstone needs lookups for a whole bunch of things.

To put it simply, Minecraft is a blocky game and block lookups are at the core of most gameplay features. When it comes to mods and plugins, this can matter even more. Every time something needs to know information about a block, a lookup happens. This means if you’ve got a plugin that handles mine resets when a mine area gets too low, it’s frequently doing a lookup of every single block in the area to check what type it is to determine if it needs to do a reset or not. Basically, these operations can scale very quickly, especially on larger or busier servers.

The benchmark

I’ve created a simple benchmark case that tests two separate things. Firstly, the time it takes to do 50 subsequent block type lookups at the same location within a chunk, at 1000 different locations within the chunk. And secondly, another test that does 50 block type comparisons with only a single lookup at the location, at 1000 different locations within the chunk.

This tests both the time it takes to do many block lookups, as well as allowing us to account for other overhead on each version of the game. We can measure both the cost to look these blocks up, but also the gap between looking it up once at each block or 50 times. These tests are meant to emulate situations where you might be checking a block against many different types. This can be done naively in the Bukkit API with a block.getType() lookup at each check, or more efficiently by storing the block type in a variable and reusing it. For example,

return block.getType() == Material.DIRT
    || block.getType() == Material.GRASS
    || block.getType() == Material.COARSE_DIRT
    || block.getType() == Material.PODZOL
    || block.getType() == Material.MYCELIUM;

or,

Material type = block.getType();
return type == Material.DIRT
    || type == Material.GRASS
    || type == Material.COARSE_DIRT
    || type == Material.PODZOL
    || type == Material.MYCELIUM;

The first code block corresponding to the first test, and the second corresponding to the second test.

Results

The results from the testing were as follows, with the "Multiple" test being the first test that looks up blocks multiple times, and the "Reuse" test being the second test that reuses the same block lookup. The numbers are measured in operations per second, with higher being better. Each "operation" in this context referring to checking whether a single block is one of 50 different block types, either with repeated lookups or reusing the same lookup. I’ve also included a column that shows a gap between the raw time taken for the multiple and reuse run, to show how much faster reusing the lookups was for each version.

All testing was performed on a local Paper server with no other plugins installed, running the final version of Paper for each Minecraft version. The server was running on a Ryzen 9 5950X with 16GB of RAM allocated.

MC VersionMultipleReuseGap %
1.8.82974 ops/s33123 ops/s167.04%
1.12.21889 ops/s28968 ops/s175.50%
1.16.51460 ops/s19872 ops/s172.62%
1.21.4516 ops/s15733 ops/s187.29%

As you can see from these results, Minecraft has gotten substantially slower at block lookups over time. This was actually worse than I had assumed going into this. In Minecraft 1.8, block lookups were fairly fast and you could gain even more speed by reusing a single type lookup. In 1.12 it was a bit slower, but you could get similar speeds to 1.8 by caching it. 1.12 is an interesting case as it appears the game itself hasn’t gotten much slower compared to 1.8, only block lookups. We can tell this by the increase in the gap percentage, but a much smaller difference in the operations per second in the reuse test case.

After the flattening in 1.13, we can see that 1.16.5 is significantly slower. This is a case where the game itself has gotten slower at a much faster rate than the block lookups. As you can see from the gap percentage, it's narrowed compared to 1.12. This indicates that the game si slower across the board. This slowdown continues in 1.21.4, with the highest gap of the entire results. At this point, not reusing block lookups is six times slower than it was on 1.8, and the rest of the code is twice as slow. If you’re not reusing block lookups, you’re missing out on a potential 187% speed boost over 50 lookups.

Raw data
1.8.8
Multiple: 336190ns
Reuse:    30190ns

1.12.2
Multiple: 529220ns
Reuse:    34520ns

1.16.5
Multiple: 684900ns
Reuse:    50320ns

1.21.4
Multiple: 1936920ns
Reuse:    63560ns

Discussion

From my experience developing CraftBook, which is a plugin that contains a large number of various gameplay mechanics that require block lookups, these can actually have an impact at scale. It’s important not to just optimise repeated block lookups like this but also think about different designs or algorithms to reduce the number of block lookups needed in the first place.

For example, CraftBook has a feature that automatically scans an area and harvests crops. Rather than naively iterating over these blocks and doing a lookup to see if it’s a fully grown crop with a random chance to harvest it, instead it randomly checks 10 blocks within the area. You can tweak it to behave practically the same in reality, but now it’s performing magnitudes fewer block updates. Beforehand the random chance calculation was slower than the block lookup, but now it’s faster to generate multiple random numbers just to avoid a block lookup.

It’s also worth noting, that if you need further data such as from the Bukkit BlockState or BlockData types, these both contain a cached copy of the block type. Calling Block#getState(false) and then Block#getType() is actually unnecessary, the BlockState#getType() and BlockData#getMaterial() methods avoid a lookup to the world as that data was already retrieved when you grabbed the BlockState or BlockData from the world in the first place. Notably, BlockData is slower to lookup than just the type, and BlockState is even slower on top of that, so you should always grab the smallest amount of data you need.

Conclusion

We’re no longer in a world where block lookups are cheap, we need to find clever ways around them. Reusing lookups when possible is one thing but reducing them in the first place is more important. Writing performance-sensitive Minecraft plugins is much harder than it used to be, and much more important as Minecraft server admins try to push their hardware to the limits.

About the Author
Maddy Miller

Hi, I'm Maddy Miller, a Senior Software Engineer at Clipchamp at Microsoft. In my spare time I love writing articles, and I also develop the Minecraft mods WorldEdit, WorldGuard, and CraftBook. My opinions are my own and do not represent those of my employer in any capacity. Find out more.