5

我正在开发一个从文本文件中读取数百万个浮点数的程序。该程序在我正在设计的游戏中运行,因此我需要它快速(我正在加载一个 obj 文件)。到目前为止,加载一个相对较小的文件大约需要一分钟(没有预编译),因为 Convert.ToSingle() 的速度很慢。有没有更快的方法来做到这一点?

编辑:这是我用来解析 Obj 文件的代码

http://pastebin.com/TfgEge9J

using System;
using System.IO;
using System.Collections.Generic;
using OpenTK.Math;
using System.Drawing;
using PlatformLib;

public class ObjMeshLoader
{
    public static StreamReader[] LoadMeshes(string fileName)
    {
        StreamReader mreader = new StreamReader(PlatformLib.Platform.openFile(fileName));
        MemoryStream current = null;
        List<MemoryStream> mstreams = new List<MemoryStream>();
        StreamWriter mwriter = null;

        if (!mreader.ReadLine().Contains("#"))
        {
            mreader.BaseStream.Close();
            throw new Exception("Invalid header");
        }

        while (!mreader.EndOfStream)
        {
            string cmd = mreader.ReadLine();
            string line = cmd;
            line = line.Trim(splitCharacters);
            line = line.Replace("  ", " ");

            string[] parameters = line.Split(splitCharacters);
            if (parameters[0] == "mtllib")
            {
                loadMaterials(parameters[1]);
            }

            if (parameters[0] == "o")
            {
                if (mwriter != null)
                {
                    mwriter.Flush();
                    current.Position = 0;
                }

                current = new MemoryStream();
                mwriter = new StreamWriter(current);
                mwriter.WriteLine(parameters[1]);
                mstreams.Add(current);
            }
            else
            {
                if (mwriter != null)
                {
                    mwriter.WriteLine(cmd);
                    mwriter.Flush();
                }
            }
        }

        mwriter.Flush();
        current.Position = 0;
        List<StreamReader> readers = new List<StreamReader>();

        foreach (MemoryStream e in mstreams)
        {
            e.Position = 0;
            StreamReader sreader = new StreamReader(e);
            readers.Add(sreader);
        }

        return readers.ToArray();
    }

    public static bool Load(ObjMesh mesh, string fileName)
    {
        try
        {
            using (StreamReader streamReader = new StreamReader(Platform.openFile(fileName)))
            {
                Load(mesh, streamReader);
                streamReader.Close();
                return true;
            }
        }
        catch { return false; }
    }

    public static bool Load2(ObjMesh mesh, StreamReader streamReader, ObjMesh prevmesh)
    {
        if (prevmesh != null)
        {
            //mesh.Vertices = prevmesh.Vertices;
        }

        try
        {
            //streamReader.BaseStream.Position = 0;
            Load(mesh, streamReader);
            streamReader.Close();
#if DEBUG
            Console.WriteLine("Loaded "+mesh.Triangles.Length.ToString()+" triangles and"+mesh.Quads.Length.ToString()+" quadrilaterals parsed, with a grand total of "+mesh.Vertices.Length.ToString()+" vertices.");
#endif
            return true;
        }
        catch (Exception er) { Console.WriteLine(er); return false; }
    }

    static char[] splitCharacters = new char[] { ' ' };
    static List<Vector3> vertices;
    static List<Vector3> normals;
    static List<Vector2> texCoords;
    static Dictionary<ObjMesh.ObjVertex, int> objVerticesIndexDictionary;
    static List<ObjMesh.ObjVertex> objVertices;
    static List<ObjMesh.ObjTriangle> objTriangles;
    static List<ObjMesh.ObjQuad> objQuads;
    static Dictionary<string, Bitmap> materials = new Dictionary<string, Bitmap>();

    static void loadMaterials(string path)
    {
        StreamReader mreader = new StreamReader(Platform.openFile(path));
        string current = "";
        bool isfound = false;

        while (!mreader.EndOfStream)
        {
            string line = mreader.ReadLine();
            line = line.Trim(splitCharacters);
            line = line.Replace("  ", " ");

            string[] parameters = line.Split(splitCharacters);

            if (parameters[0] == "newmtl")
            {
                if (materials.ContainsKey(parameters[1]))
                {
                    isfound = true;
                }
                else
                {
                    current = parameters[1];
                }
            }

            if (parameters[0] == "map_Kd")
            {
                if (!isfound)
                {
                    string filename = "";
                    for (int i = 1; i < parameters.Length; i++)
                    {
                        filename += parameters[i];
                    }

                    string searcher = "\\" + "\\";

                    filename.Replace(searcher, "\\");
                    Bitmap mymap = new Bitmap(filename);
                    materials.Add(current, mymap);
                    isfound = false;
                }
            }
        }
    }

    static float parsefloat(string val)
    {
        return Convert.ToSingle(val);
    }

    int remaining = 0;

    static string GetLine(string text, ref int pos)
    {
        string retval = text.Substring(pos, text.IndexOf(Environment.NewLine, pos));
        pos = text.IndexOf(Environment.NewLine, pos);
        return retval;
    }

    static void Load(ObjMesh mesh, StreamReader textReader)
    {
        //try {
        //vertices = null;
        //objVertices = null;
        if (vertices == null)
        {
            vertices = new List<Vector3>();
        }

        if (normals == null)
        {
            normals = new List<Vector3>();
        }

        if (texCoords == null)
        {
            texCoords = new List<Vector2>();
        }

        if (objVerticesIndexDictionary == null)
        {
            objVerticesIndexDictionary = new Dictionary<ObjMesh.ObjVertex, int>();
        }

        if (objVertices == null)
        {
            objVertices = new List<ObjMesh.ObjVertex>();
        }

        objTriangles = new List<ObjMesh.ObjTriangle>();
        objQuads = new List<ObjMesh.ObjQuad>();

        mesh.vertexPositionOffset = vertices.Count;

        string line;
        string alltext = textReader.ReadToEnd();
        int pos = 0;

        while ((line = GetLine(alltext, pos)) != null)
        {
            if (line.Length < 2)
            {
                break;
            }

            //line = line.Trim(splitCharacters);
            //line = line.Replace("  ", " ");

            string[] parameters = line.Split(splitCharacters);

            switch (parameters[0])
            {

                case "usemtl":
                    //Material specification
                    try
                    {
                        mesh.Material = materials[parameters[1]];
                    }
                    catch (KeyNotFoundException)
                    {
                        Console.WriteLine("WARNING: Texture parse failure: " + parameters[1]);
                    }

                    break;
                case "p": // Point
                    break;
                case "v": // Vertex
                    float x = parsefloat(parameters[1]);
                    float y = parsefloat(parameters[2]);
                    float z = parsefloat(parameters[3]);
                    vertices.Add(new Vector3(x, y, z));
                    break;
                case "vt": // TexCoord
                    float u = parsefloat(parameters[1]);
                    float v = parsefloat(parameters[2]);
                    texCoords.Add(new Vector2(u, v));
                    break;
                case "vn": // Normal
                    float nx = parsefloat(parameters[1]);
                    float ny = parsefloat(parameters[2]);
                    float nz = parsefloat(parameters[3]);
                    normals.Add(new Vector3(nx, ny, nz));
                    break;
                case "f":
                    switch (parameters.Length)
                    {
                        case 4:
                            ObjMesh.ObjTriangle objTriangle = new ObjMesh.ObjTriangle();
                            objTriangle.Index0 = ParseFaceParameter(parameters[1]);
                            objTriangle.Index1 = ParseFaceParameter(parameters[2]);
                            objTriangle.Index2 = ParseFaceParameter(parameters[3]);
                            objTriangles.Add(objTriangle);
                            break;
                        case 5:
                            ObjMesh.ObjQuad objQuad = new ObjMesh.ObjQuad();
                            objQuad.Index0 = ParseFaceParameter(parameters[1]);
                            objQuad.Index1 = ParseFaceParameter(parameters[2]);
                            objQuad.Index2 = ParseFaceParameter(parameters[3]);
                            objQuad.Index3 = ParseFaceParameter(parameters[4]);
                            objQuads.Add(objQuad);
                            break;
                    }
                    break;
            }
        }
        //}catch(Exception er) {
        //  Console.WriteLine(er);
        //  Console.WriteLine("Successfully recovered. Bounds/Collision checking may fail though");
        //}
        mesh.Vertices = objVertices.ToArray();
        mesh.Triangles = objTriangles.ToArray();
        mesh.Quads = objQuads.ToArray();
        textReader.BaseStream.Close();
    }

    public static void Clear()
    {
        objVerticesIndexDictionary = null;
        vertices = null;
        normals = null;
        texCoords = null;
        objVertices = null;
        objTriangles = null;
        objQuads = null;
    }

    static char[] faceParamaterSplitter = new char[] { '/' };

    static int ParseFaceParameter(string faceParameter)
    {
        Vector3 vertex = new Vector3();
        Vector2 texCoord = new Vector2();
        Vector3 normal = new Vector3();

        string[] parameters = faceParameter.Split(faceParamaterSplitter);

        int vertexIndex = Convert.ToInt32(parameters[0]);

        if (vertexIndex < 0) vertexIndex = vertices.Count + vertexIndex;
        else vertexIndex = vertexIndex - 1;

        //Hmm. This seems to be broken.
        try
        {
            vertex = vertices[vertexIndex];
        }
        catch (Exception)
        {
            throw new Exception("Vertex recognition failure at " + vertexIndex.ToString());
        }

        if (parameters.Length > 1)
        {
            int texCoordIndex = Convert.ToInt32(parameters[1]);

            if (texCoordIndex < 0) texCoordIndex = texCoords.Count + texCoordIndex;
            else texCoordIndex = texCoordIndex - 1;

            try
            {
                texCoord = texCoords[texCoordIndex];
            }
            catch (Exception)
            {
                Console.WriteLine("ERR: Vertex " + vertexIndex + " not found. ");
                throw new DllNotFoundException(vertexIndex.ToString());
            }
        }

        if (parameters.Length > 2)
        {
            int normalIndex = Convert.ToInt32(parameters[2]);

            if (normalIndex < 0) normalIndex = normals.Count + normalIndex;
            else normalIndex = normalIndex - 1;

            normal = normals[normalIndex];
        }

        return FindOrAddObjVertex(ref vertex, ref texCoord, ref normal);
    }

    static int FindOrAddObjVertex(ref Vector3 vertex, ref Vector2 texCoord, ref Vector3 normal)
    {
        ObjMesh.ObjVertex newObjVertex = new ObjMesh.ObjVertex();
        newObjVertex.Vertex = vertex;
        newObjVertex.TexCoord = texCoord;
        newObjVertex.Normal = normal;

        int index;

        if (objVerticesIndexDictionary.TryGetValue(newObjVertex, out index))
        {
            return index;
        }
        else
        {
            objVertices.Add(newObjVertex);
            objVerticesIndexDictionary[newObjVertex] = objVertices.Count - 1;
            return objVertices.Count - 1;
        }
    }
}
4

5 回答 5

5

根据您的描述和您发布的代码,我敢打赌,您的问题不在于阅读、解析或您将内容添加到集合中的方式。最可能的问题是您的ObjMesh.Objvertex结构没有覆盖GetHashCode. (我假设您正在使用类似于http://www.opentk.com/files/ObjMesh.cs的代码。

如果你没有覆盖GetHashCode,那么你objVerticesIndexDictionary的表现将非常像线性列表。这将解释您遇到的性能问题。

我建议你考虑为你的班级提供一个好的GetHashCode方法。ObjMesh.Objvertex

请参阅为什么按原样实现 ValueType.GetHashCode()?有关GetHashCode值类型的默认实现以及为什么它不适合在哈希表或字典中使用的信息。

于 2011-04-24T04:47:26.390 回答
3

编辑3:问题不在于解析。

这与您阅读文件的方式有关。如果你阅读得当,它会更快;但是,您的阅读速度似乎异常缓慢。我最初的怀疑是这是因为分配过多,但您的代码似乎也可能存在其他问题,因为这并不能解释整个减速。

不过,这是我编写的一段代码,它完全避免了所有对象分配:

static void Main(string[] args)
{
    long counter = 0;
    var sw = Stopwatch.StartNew();
    var sb = new StringBuilder();
    var text = File.ReadAllText("spacestation.obj");
    for (int i = 0; i < text.Length; i++)
    {
        int start = i;
        while (i < text.Length &&
            (char.IsDigit(text[i]) || text[i] == '-' || text[i] == '.'))
        { i++; }
        if (i > start)
        {
            sb.Append(text, start, i - start); //Copy data to the buffer

            float value = Parse(sb); //Parse the data

            sb.Remove(0, sb.Length); //Clear the buffer
            counter++;
        }
    }
    sw.Stop();
    Console.WriteLine("{0:N0}", sw.Elapsed.TotalSeconds); //Only a few ms
}

使用这个解析器:

const int MIN_POW_10 = -16, int MAX_POW_10 = 16,
    NUM_POWS_10 = MAX_POW_10 - MIN_POW_10 + 1;
static readonly float[] pow10 = GenerateLookupTable();
static float[] GenerateLookupTable()
{
    var result = new float[(-MIN_POW_10 + MAX_POW_10) * 10];
    for (int i = 0; i < result.Length; i++)
        result[i] = (float)((i / NUM_POWS_10) *
                Math.Pow(10, i % NUM_POWS_10 + MIN_POW_10));
    return result;
}
static float Parse(StringBuilder str)
{
    float result = 0;
    bool negate = false;
    int len = str.Length;
    int decimalIndex = str.Length;
    for (int i = len - 1; i >= 0; i--)
        if (str[i] == '.')
        { decimalIndex = i; break; }
    int offset = -MIN_POW_10 + decimalIndex;
    for (int i = 0; i < decimalIndex; i++)
        if (i != decimalIndex && str[i] != '-')
            result += pow10[(str[i] - '0') * NUM_POWS_10 + offset - i - 1];
        else if (str[i] == '-')
            negate = true;
    for (int i = decimalIndex + 1; i < len; i++)
        if (i != decimalIndex)
            result += pow10[(str[i] - '0') * NUM_POWS_10 + offset - i];
    if (negate)
        result = -result;
    return result;
}

它发生在几分之一秒内

当然,这个解析器的测试很差,并且有这些当前的限制(以及更多):

  • 不要尝试解析比数组中提供的更多的数字(十进制和整数)。

  • 没有任何错误处理。

  • 只解析小数解析指数!即它可以解析1234.56但不能1.23456E3

  • 不关心全球化/本地化。你的文件只有一种格式,所以没有必要关心那种东西,因为无论如何你可能都在使用英语来存储它。

似乎您不一定需要这么多的矫枉过正,但请查看您的代码并尝试找出瓶颈。它似乎既不是阅读也不是解析。

于 2011-04-23T22:56:41.060 回答
2

您是否测量过速度问题真的是由 引起的Convert.ToSingle

在您包含的代码中,我看到您创建了这样的列表和字典:

normals = new List<Vector3>();
texCoords = new List<Vector2>();
objVerticesIndexDictionary = new Dictionary<ObjMesh.ObjVertex, int>();

然后,当您阅读文件时,您一次将一项添加到集合中。可能的优化之一是在文件开头保存法线、texCoords、索引和所有内容的总数,然后用这些数字初始化这些集合。这将预先分配集合使用的缓冲区,因此向它们添加项目将非常快。

所以集合创建应该是这样的:

// These values should be stored at the beginning of the file
int totalNormals = Convert.ToInt32(textReader.ReadLine());
int totalTexCoords = Convert.ToInt32(textReader.ReadLine());
int totalIndexes = Convert.ToInt32(textReader.ReadLine());

normals = new List<Vector3>(totalNormals);
texCoords = new List<Vector2>(totalTexCoords);
objVerticesIndexDictionary = new Dictionary<ObjMesh.ObjVertex, int>(totalIndexes);

请参阅List<T> 构造函数 (Int32)Dictionary<TKey, TValue> 构造函数 (Int32)

于 2011-04-23T23:15:57.447 回答
0

这个相关的问题是针对 C++ 的,但绝对值得一读。

为了尽可能快地阅读,您可能希望将文件映射到内存中,然后使用一些自定义浮点解析器进行解析,特别是如果您知道数字始终采用特定格式(即您是生成的首先是输入文件)。

于 2011-04-23T23:02:43.183 回答
0

我测试了一次.Net 字符串解析,解析文本的最快函数是旧的 VB Val() 函数。您可以从 Microsoft.VisualBasic.Conversion Val(string) 中提取相关部分

Converting String to numbers

Comparison of relative test times (ms / 100000 conversions)
Double  Single  Integer    Int(w/ decimal point)
14      13      6          16                 Val(Str)
14      14      6          16                 Cxx(Val(Str)) e.g., CSng(Val(str))
22      21      17          e!                Convert.To(str)
23      21      16          e!                XX.Parse(str) e.g. Single.Parse()
30      31      31         32                 Cxx(str)

Val: fastest, part of VisualBasic dll, skips non-numeric,
ConvertTo and Parse: slower, part of core, exception on bad format (including decimal point)
Cxx: slowest (for strings), part of core, consistent times across formats
于 2011-05-11T03:51:15.963 回答