Store binary-encoded simple tags inside `CompoundTag` and `ListTag` instead of useless boxing objects
User code rarely interacts with the likes of IntTag directly. It's also horribly inefficient to box integers this way.
In addition, extra objects means extra work for the GC.
All simple tags (byte, short, int, long, string, byte[], float, double, int[]) could be easily stored as string inside CompoundTag and ListTag, avoiding the need for a ton of unnecessary object allocations. This string would essentially just be chr($tagType) . LittleEndianNbtSerializer::writeHeadless($tagValue).
Brief investigation shows that integer-like tags ByteTag, ShortTag, etc take up a minimum of 80 bytes of memory. Encoded as strings, these would take 32 bytes each instead (the aligned size of zend_string). StringTag, ByteArrayTag and IntTag would be larger (depending on the size of their contained values).
(We could even use int for types smaller than 8 bytes on 64-bit platforms, which would avoid extra allocations completely.)
The only potential caveat to this is that packing the values into a binary form would be kinda slow because the NBT serializers currently have crap performance. However, it's possible that the cost of allocating Tag objects might outweigh this anyway.
There's also the consideration that if CompoundTag->getTag() or getValue() were used, we'd have to convert the strings into tag objects anyway. So this would mostly benefit from use of stuff like CompoundTag->getInt() where we could just directly convert the internal string binary into an int value without allocating any objects.
Another option could be to embed a type info byte into the CompoundTag keys. However, this might be more complex to implement.
This ended up not providing any benefits because the extra complexity had performance costs that mostly negated the gains from not using objects.