Efficiently loading large arrays or lists

Originally posted to Shawn Hargreaves Blog on MSDN, Monday, September 27, 2010

Automatic XNB serialization is convenient, but not always the fastest way to load large amounts of data. Its overhead is insignificant when your content files are small, or for the common situation of game content that has a limited amount of custom data followed by large objects of built-in types such as VertexBuffer or Texture2D (which have efficient readers). But the overhead can quickly add up when serializing large collections. For instance, here is the built-in ContentTypeReader for array of T:

    protected override T[] Read(ContentReader input, T[] existingInstance)
    {
        int count = input.ReadInt32();
        T[] array = new T[count];

        for (int i = 0; i < count i++)
        {
            array[i] = input.ReadObject(this.elementReader);
        }

        return array;
    }

This is nice and flexible: thanks to the ReadObject call, it can serialize arrays of any type. In spite of its flexibility, the ReadObject method is quite efficient, but if you repeat something enough times, even small costs add up to a big overhead. So load times can sometimes be optimized by providing more specialized readers for the specific collection types you use.

Disclaimer: as always, you should only apply this optimization after profiling your loading code to confirm that array or list reading really is a significant cost for your game!

As an example, I will walk through how to speed up loading the animation data in our Skinned Model sample. This uses several custom data types, starting with the SkinningData class, which contains a couple of lists of matrices plus a dictionary of AnimationClip objects. These collections are not large enough to care about, though, as the largest part of the animation data is the list of Keyframe objects stored within each AnimationClip.

A nice thing about optimizing content loading is that we don’t have to provide custom writers and readers for all our custom types. It is fine to mix and match, so we can optimize just this one list type while continuing to use automatic serialization for everything else.

Remember how it is important to separate build time from runtime code when using the Content Pipeline. We will start by adding a custom reader for List<Keyframe> objects, which must be placed in an assembly that is available at runtime (for this sample, add it to the SkinnedModelWindows project):

    public class KeyframeListReader : ContentTypeReader<List<Keyframe>>
    {
        protected override List<Keyframe> Read(ContentReader input, List<Keyframe> existingInstance)
        {
            int size = input.ReadInt32();
            List<Keyframe> list = new List<Keyframe>(size);

            for (int i = 0; i < size; i++)
            {
                int bone = input.ReadInt32();
                TimeSpan time = TimeSpan.FromTicks(input.ReadInt64());
                Matrix transform = input.ReadMatrix();

                list.Add(new Keyframe(bone, time, transform));
            }

            return list;
        }
    }

Now we add the corresponding writer class, which must be placed in an assembly that is available at build time (for this sample, add it to the SkinnedModelPipeline project):

    [ContentTypeWriter]
    public class KeyframeListWriter : ContentTypeWriter<List<Keyframe>>
    {
        protected override void Write(ContentWriter output, List<Keyframe> value)
        {
            output.Write(value.Count);

            foreach (Keyframe keyframe in value)
            {
                output.Write(keyframe.Bone);
                output.Write(keyframe.Time.Ticks);
                output.Write(keyframe.Transform);
            }
        }

        public override string GetRuntimeReader(TargetPlatform targetPlatform)
        {
            return typeof(KeyframeListReader).AssemblyQualifiedName;
        }
    }

And we’re done! The sample behaves the same as before, but its lists of keyframe objects load more quickly. On my Windows laptop, this speeds up loading the dude model by 25%. That’s not a huge difference, but bear in mind that much of this time is spent loading textures, which was already well optimized. The improvement would be more dramatic if the model had smaller textures or more animations.

Blog index   -   Back to my homepage