C# - Convert.ToSingle()的更快替代方法

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;
        }
    }
}

5
为什么首选文本文件?考虑使用二进制文件,这样您就可以直接读取浮点数。 - BrokenGlass
1
@Heandel:float.ParseSingle.Parse相同,当给定一个字符串时,Convert.ToSingle调用的就是它。 - David Brown
3
其他字符串解析方法也不会比这个更好。选择二进制格式吧。 - Ekin Koc
2
@IDWMaster:仅供参考,如果没有使用多线程,比我的版本快30倍是“物理上不可能的”。我从代码中删除了所有解析数据,只写了 return 0;,但它仍然无法以30倍的速度击败自己。我认为问题出在你读取文件的方式上,而不是解析问题;如果你正在使用像 StreamReader.ReadLine(或任何其他分配字符串或数组的东西),那么性能会大大降低。你能否发布一些示例代码让我们看看你在做什么? - user541686
我也怀疑问题不在于你的 parseFloat 方法。你可以通过让它返回 0 来测试。我认为问题不在于读取或解析,而是在于随着集合增长而需要重新调整大小。我认为 @Paja 在这方面是正确的。 - Jim Mischel
显示剩余6条评论
5个回答

5
根据您的描述和发布的代码,我猜测您的问题不在于读取、解析或添加到集合中的方式。最可能的问题是您的ObjMesh.Objvertex结构没有覆盖GetHashCode方法。(我假设您使用的代码类似于http://www.opentk.com/files/ObjMesh.cs。)
如果您没有覆盖GetHashCode方法,那么您的objVerticesIndexDictionary将表现得非常像线性列表。这可能是导致您遇到的性能问题的原因。
我建议您考虑为ObjMesh.Objvertex类提供一个良好的GetHashCode方法。
有关值类型默认GetHashCode实现及其不适用于哈希表或字典的信息,请参见Why is ValueType.GetHashCode() implemented like it is?

谢谢!这帮了我很多!现在网格几乎瞬间加载! - bbosak

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

  • 不关心全球化/本地化。您的文件只有一个格式,因此没有必要关心这种东西,因为您可能仍在使用英语进行存储。

看起来您不一定需要这么复杂的解析器,但是请查看您的代码并尝试找出瓶颈所在。似乎既不是读取也不是解析。


我发布了我的代码,用于实际读取文件。完整的Obj导入器代码。请查看我的回答中的pastebin链接。 - bbosak
如何在不使用ReadLine或substring的情况下对其进行解析?加载Obj文件的最快方法是什么,而不使用这些方法? - bbosak
@IDWMaster:我没有用任何一个; 请看我的编辑。你应该在整个过程中只使用一个 StringBuilder,并且不要在循环中创建新字符串。 - user541686
问题不在于读取文件。我创建了一个包含100万行,每行有10个双精度浮点数的文件。生成的文件大小为170 MB。在我的机器上(2.0 GHz Core 2处理器),它可以在8.2秒内读取和解析该文件。它每秒解析120万个双精度浮点数。问题不在于解析或读取。我的测试使用double.Parse - Jim Mischel
@Jim:我猜可能还有其他原因,但那是我的直觉。如果你没有其他低效的代码(我猜这里可能不是这种情况,但我从未运行过他的代码进行检查),我猜你可以尝试找出真正的瓶颈,但我相当确定使用string.SplitStreamReader.ReadLine与解析相比一个瓶颈。 但是,我真的应该被踩吗?:( - user541686
显示剩余9条评论

2

你是否已经确定速度问题确实是由Convert.ToSingle引起的?

在你提供的代码中,我看到你像这样创建列表和字典:

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

当你读取文件时,需要逐个添加到集合中。其中一个可能的优化是在文件开头保存法线、纹理坐标、索引和所有内容的总数,然后通过这些数字初始化这些集合。这将预分配集合使用的缓冲区,因此向它们添加项目将非常快速。
因此,集合创建应该像这样:
// 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)


用substring替换ReadLine - bbosak
假设我在文件开头就知道总数,那么这个方法是可行的。但是根据Obj规范,我不知道文件开头有多少个顶点、纹理坐标、法线、四边形等等。 - bbosak
@Jim, @Paja:看看我的修改版。注意到我从不在任何循环内创建新对象,以及速度的巨大提升(即总共几毫秒)。问题不在于这是一个文本文件还是二进制文件,问题在于它创建了太多新对象,仅此而已。 - user541686
1
但我真的建议测量解析器中发生的一切,并查看性能瓶颈所在。尝试使用分析器。 - Paya
1
@Mehrdad:实际上,使用分析器比多次暂停程序更省事(至少对我来说),而且更可靠。 - Paya
显示剩余6条评论

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

0

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

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


内存映射对于这个问题来说有些过头了。 - David Heffernan
尝试过了。使用ReadToEnd()将整个文件加载到内存中,然后解析该文本的子字符串。 - bbosak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接