Krythic
Krythic

Reputation: 4364

Why is Object Deserialization Slower Than Raw Re-Initialization?

Today I was looking for ways to store my Wavefront Models, and hopefully increase performance, too. I wanted to look into serialization, mainly because I had never used it before. In my mind, serialization/deserialization should be faster than parsing and reinitializing a Wavefront model, however, my benchmark shows otherwise.

Here is the code for my benchmark:

using System;
using System.Diagnostics;
using GrimoireTactics.Framework.OpenGL.Modeling;
using GrimoireTactics.Framework.Utilities;

namespace GrimoireDevelopmentKit.DevelopmentKit
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {

            WavefrontModel model;
            Stopwatch benchmark = new Stopwatch();
            //
            // Benchmark Deserialization
            //

            // Do a warm up 
            for (int i = 0; i < 500; i++)
            {
                model = ResourceCompiler.ReadFromBinaryFile<WavefrontModel>("C:/Users/Krythic/Desktop/Compiled.sfg"); // Sfg is an extension I wanted to use
            }
            benchmark.Start();
            model = ResourceCompiler.ReadFromBinaryFile<WavefrontModel>("C:/Users/Krythic/Desktop/Compiled.sfg"); // Sfg is an extension I wanted to use
            benchmark.Stop();
            Console.WriteLine("Deserialization: "+ benchmark.Elapsed);
            benchmark.Reset();


            //
            // Benchmark Plain Old Initialization
            //
            model = new WavefrontModel();

            // Do a Warm up
            for (int i = 0; i < 500; i++)
            {
                model.Load("C:/Users/Krythic/Desktop/Closet.obj");
            }
            benchmark.Start();
            model.Load("C:/Users/Krythic/Desktop/Closet.obj");
            benchmark.Stop();
            Console.WriteLine("Plain Old Initialization: " + benchmark.Elapsed);
            Console.Read();
        }
    }
}

And here is the output:

enter image description here

Here is the code for Serialization and Deserialization(Which I found on Stackoverflow:

using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace GrimoireTactics.Framework.Utilities
{
    public class ResourceCompiler
    {
        /// <summary>
        /// Writes the given object instance to a binary file.
        /// <para>Object type (and all child types) must be decorated with the [Serializable] attribute.</para>
        /// <para>To prevent a variable from being serialized, decorate it with the [NonSerialized] attribute; cannot be applied to properties.</para>
        /// </summary>
        /// <typeparam name="T">The type of object being written to the XML file.</typeparam>
        /// <param name="filePath">The file path to write the object instance to.</param>
        /// <param name="objectToWrite">The object instance to write to the XML file.</param>
        /// <param name="append">If false the file will be overwritten if it already exists. If true the contents will be appended to the file.</param>
        public static void WriteToBinaryFile<T>(string filePath, T objectToWrite, bool append = false)
        {
            using (Stream stream = File.Open(filePath, append ? FileMode.Append : FileMode.Create))
            {
                BinaryFormatter binaryFormatter = new BinaryFormatter();
                binaryFormatter.Serialize(stream, objectToWrite);
            }
        }

        /// <summary>
        /// Reads an object instance from a binary file.
        /// </summary>
        /// <typeparam name="T">The type of object to read from the XML.</typeparam>
        /// <param name="filePath">The file path to read the object instance from.</param>
        /// <returns>Returns a new instance of the object read from the binary file.</returns>
        public static T ReadFromBinaryFile<T>(string filePath)
        {
            using (Stream stream = File.Open(filePath, FileMode.Open))
            {
                BinaryFormatter binaryFormatter = new BinaryFormatter();
                return (T)binaryFormatter.Deserialize(stream);
            }
        }
    }
}

And here is my WavefrontModel class:

using System;
using System.Collections.Generic;
using System.IO;
using GrimoireTactics.Framework.OpenGL.Texturing;
using OpenTK;
using OpenTK.Graphics.OpenGL;

namespace GrimoireTactics.Framework.OpenGL.Modeling
{
    [Serializable]
    public class WavefrontModel
    {
        public Vector3[] Vertices;
        public Vector2[] TexCoords;
        public Vector3[] Normals;
        public Face[] Faces;
        public string ModelSource;
        public string Name;
        public Material Material;
        /// <summary>
        /// A static buffer used by all models when they are loaded.
        /// </summary>
        private static readonly string[] FileBuffer = new string[15];
        /// <summary>
        /// A static buffer used by all models when they are loaded.
        /// </summary>
        private static readonly string[] IndiceBuffer = new string[3];
        /// <summary>
        /// A static buffer used by all models when they are loaded.
        /// </summary>
        private static readonly FaceIndices[] VerticesIndexBuffer = new FaceIndices[3];

        /// <summary>
        /// The Triangle Count of this model.
        /// </summary>
        public int TriCount
        {
            get
            {
                return Faces.Length;
            }
        }

        public WavefrontModel()
        {

        }

        public WavefrontModel(string modelPath, Material material)
        {
            this.ModelSource = modelPath;
            this.Material = material;
        }

        public WavefrontModel(string modelPath)
        {
            this.ModelSource = modelPath;
            this.Material = null;
        }

        public WavefrontModel(string[] data)
        {
            this.ModelSource = String.Empty;
            this.Material = null;
            Load(data);
        }

        public WavefrontModel(string[] data, Material material)
        {
            this.ModelSource = String.Empty;
            this.Material = material;
            Load(data);
        }

        /// <summary>
        /// Loads a model from the desired Wavefront.obj source given
        /// at constructor initialization.
        /// </summary>
        public void Load()
        {
            Load(this.ModelSource);
        }

        /// <summary>
        /// Loads a model from a Wavefront.obj located on disk.
        /// </summary>
        /// <param name="file"></param>
        public void Load(string file)
        {
            Parse(File.ReadAllLines(file), this);
        }

        /// <summary>
        /// Initializes this model with the data provided.
        /// </summary>
        /// <param name="data"></param>
        public void Load(string[] data)
        {
            Parse(data, this);
        }

        /// <summary>
        /// Current Benchmarked time(Warm boot)
        /// </summary>
        /// <param name="data"></param>
        /// <param name="model"></param>
        public static void Parse(string[] data, WavefrontModel model)
        {
            // Create Header
            int totalVertices = 0;
            int totalNormals = 0;
            int totalTextureCoordinates = 0;
            int totalFaces = 0;
            for (int i = 0; i < data.Length; i++)
            {
                switch (data[i][0])
                {
                    case 'v': // Geometric Parameter
                        switch (data[i][1])
                        {
                            case ' ': // Detect Vertices
                                totalVertices++;
                                break;
                            case 't': // Detect TexCoords
                                totalTextureCoordinates++;
                                break;
                            case 'n': // Detect Normals
                                totalNormals++;
                                break;
                        }
                        break;
                    case 'f':
                        totalFaces++;
                        break;
                }
            }
            // Create the Buffers
            model.Vertices = new Vector3[totalVertices];
            model.Normals = new Vector3[totalNormals];
            model.TexCoords = new Vector2[totalTextureCoordinates];
            model.Faces = new Face[totalFaces];
            // Load the Data
            // Iterators
            int verticesIterator = 0;
            int normalsIterator = 0;
            int textureCoordinatesIterator = 0;
            int facesIterator = 0;
            for (int line = 0; line < data.Length; line++)
            {
                string[] lineTokens = SplitStringFast(data[line], ' ', FileBuffer);
                switch (lineTokens[0])
                {
                    case "v": // Vector
                        Vector3 vertex = new Vector3
                        {
                            X = ParseFloatFast(lineTokens[1]),
                            Y = ParseFloatFast(lineTokens[2]),
                            Z = ParseFloatFast(lineTokens[3])
                        };
                        model.Vertices[verticesIterator] = vertex;
                        verticesIterator++;
                        break;
                    case "vt": // Texture Coordinate
                        Vector2 textureCoordinate = new Vector2
                        {
                            X = ParseFloatFast(lineTokens[1]), // U
                            Y = -ParseFloatFast(lineTokens[2]) // V (Inverted)
                        };
                        model.TexCoords[textureCoordinatesIterator] = textureCoordinate;
                        textureCoordinatesIterator++;
                        break;
                    case "vn": // Normal
                        Vector3 normal = new Vector3
                        {
                            X = ParseFloatFast(lineTokens[1]),
                            Y = ParseFloatFast(lineTokens[2]),
                            Z = ParseFloatFast(lineTokens[3])
                        };
                        model.Normals[normalsIterator] = normal;
                        normalsIterator++;
                        break;
                    case "f": // Face (Triangle indices)
                        for (int i = 0; i < 3; i++)
                        {
                            string[] parameters = SplitStringFast(lineTokens[i + 1], '/', IndiceBuffer);
                            FaceIndices indices = new FaceIndices
                            {
                                Vertex = ParseUInt32Fast(parameters[0]) - 1,
                                Texture = ParseUInt32Fast(parameters[1]) - 1,
                                Normal = ParseUInt32Fast(parameters[2]) - 1
                            };
                            VerticesIndexBuffer[i] = indices;
                        }
                        model.Faces[facesIterator] = new Face(VerticesIndexBuffer[0], VerticesIndexBuffer[1], VerticesIndexBuffer[2]);
                        facesIterator++;
                        break;
                }
            }
        }

        /// <summary>
        /// A custom implementation of Int32.Parse. This
        /// function is, on average, 5-6x faster than the one
        /// offered by .NET. This function assumes that the string
        /// given will yield a positive integer.
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        private static int ParseUInt32Fast(string value)
        {
            int result = 0;
            for (int i = 0; i < value.Length; i++)
            {
                result = 10 * result + (value[i] - 48);
            }
            return result;
        }

        /// <summary>
        /// A custom implementation of String.Split(). Realistically, this 
        /// function is not much faster than what .NET offers; it gains speed
        /// more from a preset buffer mechanism.
        /// </summary>
        /// <param name="value"></param>
        /// <param name="delimiter"></param>
        /// <param name="buffer"></param>
        /// <returns></returns>
        private static string[] SplitStringFast(string value, char delimiter, string[] buffer)
        {
            int resultIndex = 0;
            int startIndex = 0;
            for (int i = 0; i < value.Length; i++)
            {
                if (value[i] == delimiter)
                {
                    buffer[resultIndex] = value.Substring(startIndex, i - startIndex);
                    resultIndex++;
                    startIndex = i + 1;
                }
            }
            buffer[resultIndex] = value.Substring(startIndex, value.Length - startIndex);
            return buffer;
        }

        /// <summary>
        /// A custom implementation of Float.Parse. This
        /// function is, on average, 5-6x faster than the one
        /// offered by .NET
        /// </summary>
        /// <param name="inputData">The inputData.</param>
        /// <returns></returns>
        private static float ParseFloatFast(string inputData)
        {
            float result = 0;
            int position = 0;
            int inputLength = inputData.Length;
            char firstCharacter = inputData[0];
            float negativeSign = 1;
            if (firstCharacter == '-')
            {
                negativeSign = -1;
                ++position;
            }
            while (true)
            {
                if (position >= inputLength)
                {
                    return negativeSign * result;
                }
                firstCharacter = inputData[position++];
                if (firstCharacter < '0' || firstCharacter > '9')
                {
                    break;
                }
                result = (float)((result * 10.0) + (firstCharacter - '0'));
            }
            float exponent = 0.1f;
            while (position < inputLength)
            {
                firstCharacter = inputData[position++];
                result += (firstCharacter - '0') * exponent;
                exponent *= 0.1f;
            }
            return negativeSign * result;
        }

        /// <summary>
        /// Renders the Model using deprecated immediate mode. This
        /// function exists only for testing purposes.
        /// </summary>
        public void Render()
        {
            GL.Enable(EnableCap.Texture2D);
            GL.Color3(Material.AmbientColor);
            GL.BindTexture(TextureTarget.Texture2D, Material.Diffuse);
            GL.Begin(PrimitiveType.Triangles);
            for (int i = 0; i < Faces.Length; i++)
            {
                for (int index = 0; index < Faces[i].Indices.Length; index++)
                {
                    Vector3 v = Vertices[Faces[i].Indices[index].Vertex];
                    Vector3 n = Normals[Faces[i].Indices[index].Normal];
                    Vector2 tc = TexCoords[Faces[i].Indices[index].Texture];
                    GL.Normal3(n.X, n.Y, n.Z);
                    GL.TexCoord2(tc.X, tc.Y);
                    GL.Vertex3(v.X, v.Y, v.Z);
                }
            }
            GL.End();
        }
    }
}

Sorry for all the code, I just wanted to show you guys everything I was doing. Now, in my mind, deserialization SHOULD be faster than reparsing the model, creating the arrays, etc, etc, etc. So my question is really: why is deserializtaion NOT faster? Is there something that I could do better to make the roles shift, so that Deserialization becomes faster?

Upvotes: 1

Views: 824

Answers (1)

Slugart
Slugart

Reputation: 4680

For starters try taking the creation of the binary formatter out of the inner loop. Note that "creating the arrays etc, etc, etc" will also be done by the BinaryFormatter when deserialising - there's no magic way to avoid isntantiating the object graph.

Also you could look into faster serialisers such as Protobuf-net. Check out http://maxondev.com/serialization-performance-comparison-c-net-formats-frameworks-xmldatacontractserializer-xmlserializer-binaryformatter-json-newtonsoft-servicestack-text/

Upvotes: 1

Related Questions