是否可以在没有GC分配的情况下解析字符串?

时间:2017-05-02 20:45:36

标签: c# unity3d garbage-collection string-parsing

我需要解析Android设备中内置GPS接收器的NMEA数据。我每秒都会收到几次这样的数据作为字符串。我很奇怪是否可以在没有垃圾收集分配的情况下执行此操作或解析字符串是我可以用良心打电话给GC.Collect()的时刻之一?

我需要调用string.split()和其他一些方法,例如Substring(),结果会转换为double.Parse()

我试图通过转换为char[]来做到这一点,但GC分配的方式更大。

GPS NMEA数据有很多句子,我需要每秒解析2-3个句子。下面是解析其中一个句子的示例代码 - $ GPRMC

例句:

  

$ GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E * 62   $ GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,男,46.9,M,* 47   $ GPGSA,A,3,32,27,03,193,29,23,19,16,21,31,14,1.18,0.51,1.07 * 35

        // Divide the sentence into words
        string[] Words = sSentence.split(',');
        // Do we have enough values to describe our location?
        if (Words[3] != "" & Words[4] != "" &
            Words[5] != "" & Words[6] != "")
        {
            // example 5230.5900,N
            // 52°30.5900\N

            // Yes. Extract latitude and longitude


            //Latitude decimal

            double DegreesLat = double.Parse(Words[3].Substring(0, 2), NmeaCultureInfo);
            string[] tempLat = Words[3].Substring(2).ToString ().Split ('.');
            double MinutesLat = double.Parse (tempLat[0], NmeaCultureInfo);
            string SecLat = "0";
            if (tempLat.Length >= 2) {
                SecLat = "0."+tempLat[1];
            }
            double SecondsLat = double.Parse (SecLat, NmeaCultureInfo)*60;

            double Latitude = (DegreesLat + (MinutesLat / 60) + (SecondsLat/3600));


            //Longitude decimal

            double DegreesLon = double.Parse(Words[5].Substring(0, 3), NmeaCultureInfo);
            string[] tempLon = Words[5].Substring(3).ToString ().Split ('.');
            double MinutesLon = double.Parse (tempLon[0], NmeaCultureInfo);
            string SecLon = "0";
            if (tempLon.Length >= 2) {
            SecLon = "0."+tempLon[1];
            }
            double SecondsLon = double.Parse (SecLon, NmeaCultureInfo)*60;

            double Longitude = (DegreesLon + (MinutesLon / 60) + (SecondsLon/3600));

            // Notify the calling application of the change
            if (PositionReceived != null)
                PositionReceived(Latitude, Longitude);

2 个答案:

答案 0 :(得分:4)

你在问how could I manage strings without allocating space?。这是一个答案:您always can use stackalloc在没有GC压力的情况下在堆栈上分配char[]数组,然后创建最终字符串(如果需要)using char*构造函数。但要小心,因为它不安全,你不可能只分配一个共同的char[]StringBuilder,因为gen0的集合几乎没有成本。

你有大量的代码,如Words[3].Substring(2).ToString ().Split ('.'),这些内存非常重。只要解决它,你就会变得金黄。但如果它对您没有帮助,您必须拒绝使用Substring和其他分配内存的方法,并使用您自己的解析器。

让我们开始优化。首先,我们可以修复所有其他分配。你说你已经做过了,但这是我的变种:

private static (double Latitude, double Longitude)? GetCoordinates(string input)
{
    // Divide the sentence into words
    string[] words = input.Split(',');
    // Do we have enough values to describe our location?
    if (words[3] == "" || words[4] == "" || words[5] == "" || words[6] == "")
        return null;

    var latitude = ParseCoordinate(words[3]);
    var longitude = ParseCoordinate(words[5]);

    return (latitude, longitude);
}

private static double ParseCoordinate(string coordinateString)
{
    double wholeValue = double.Parse(coordinateString, NmeaCultureInfo);

    int integerPart = (int) wholeValue;
    int degrees = integerPart / 100;
    int minutes = integerPart % 100;
    double seconds = (wholeValue - integerPart) * 60;

    return degrees + minutes / 60.0 + seconds / 3600.0;
}

好的,我们假设它仍然很慢,我们希望进一步优化它。首先,我们应该取代这个条件:

if (words[3] == "" || words[4] == "" || words[5] == "" || words[6] == "")
        return null;

我们在这做什么?我们只想知道string是否包含某些值。我们可以在不解析字符串的情况下研究它。如果出现问题,我们将通过进一步的优化来解析字符串。它可能看起来像:

private static (string LatitudeString, string LongitudeString)? ParseCoordinatesStrings(string input)
{
    int latitudeIndex = -1;
    for (int i = 0; i < 3; i++)
    {

        latitudeIndex = input.IndexOf(',', latitudeIndex + 1);
        if (latitudeIndex < 0)
            return null;
    }
    int latitudeEndIndex = input.IndexOf(',', latitudeIndex + 1);
    if (latitudeEndIndex < 0 || latitudeEndIndex - latitudeIndex <= 1)
        return null; // has no latitude
    int longitudeIndex = input.IndexOf(',', latitudeEndIndex + 1);
    if (longitudeIndex < 0)
        return null;
    int longitudeEndIndex = input.IndexOf(',', longitudeIndex + 1);
    if (longitudeEndIndex < 0 || longitudeEndIndex - longitudeIndex <= 1)
        return null; // has no longitude
    string latitudeString = input.Substring(latitudeIndex + 1, latitudeEndIndex - latitudeIndex - 1);
    string longitudeString = input.Substring(longitudeIndex + 1, longitudeEndIndex - longitudeIndex - 1);
    return (latitudeString, longitudeString);
}

现在,将它们组合在一起:

using System;
using System.Globalization;

namespace SO43746933
{
    class Program
    {
        private static readonly CultureInfo NmeaCultureInfo = CultureInfo.InvariantCulture;

        static void Main(string[] args)
        {
            string input =
                "$GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62 $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47 $GPGSA,A,3,32,27,03,193,29,23,19,16,21,31,14,,1.18,0.51,1.07*35";
            var newCoordinates = GetCoordinatesNew(input);
            var oldCoorinates = GetCoordinatesOld(input);
            if (newCoordinates == null || oldCoorinates == null)
            {
                throw new InvalidOperationException("should never throw");
            }
            Console.WriteLine("Latitude: {0}\t\tLongitude:{1}", newCoordinates.Value.Latitude, newCoordinates.Value.Longitude);
            Console.WriteLine("Latitude: {0}\t\tLongitude:{1}", oldCoorinates.Value.Latitude, oldCoorinates.Value.Longitude);
        }

        private static (double Latitude, double Longitude)? GetCoordinatesNew(string input)
        {
            // Divide the sentence into words
            var coordinateStrings = ParseCoordinatesStrings(input);
            // Do we have enough values to describe our location?
            if (coordinateStrings == null)
                return null;

            var latitude = ParseCoordinate(coordinateStrings.Value.LatitudeString);
            var longitude = ParseCoordinate(coordinateStrings.Value.LongitudeString);

            return (latitude, longitude);
        }

        private static (string LatitudeString, string LongitudeString)? ParseCoordinatesStrings(string input)
        {
            int latitudeIndex = -1;
            for (int i = 0; i < 3; i++)
            {

                latitudeIndex = input.IndexOf(',', latitudeIndex + 1);
                if (latitudeIndex < 0)
                    return null;
            }
            int latitudeEndIndex = input.IndexOf(',', latitudeIndex + 1);
            if (latitudeEndIndex < 0 || latitudeEndIndex - latitudeIndex <= 1)
                return null; // has no latitude
            int longitudeIndex = input.IndexOf(',', latitudeEndIndex + 1);
            if (longitudeIndex < 0)
                return null;
            int longitudeEndIndex = input.IndexOf(',', longitudeIndex + 1);
            if (longitudeEndIndex < 0 || longitudeEndIndex - longitudeIndex <= 1)
                return null; // has no longitude
            string latitudeString = input.Substring(latitudeIndex + 1, latitudeEndIndex - latitudeIndex - 1);
            string longitudeString = input.Substring(longitudeIndex + 1, longitudeEndIndex - longitudeIndex - 1);
            return (latitudeString, longitudeString);
        }

        private static double ParseCoordinate(string coordinateString)
        {
            double wholeValue = double.Parse(coordinateString, NmeaCultureInfo);

            int integerPart = (int) wholeValue;
            int degrees = integerPart / 100;
            int minutes = integerPart % 100;
            double seconds = (wholeValue - integerPart) * 60;

            return degrees + minutes / 60.0 + seconds / 3600.0;
        }

        private static (double Latitude, double Longitude)? GetCoordinatesOld(string input)
        {
            // Divide the sentence into words
            string[] Words = input.Split(',');
            // Do we have enough values to describe our location?
            if (!(Words[3] != "" && Words[4] != "" &
                  Words[5] != "" && Words[6] != ""))
                return null;
            // example 5230.5900,N
            // 52°30.5900\N

            // Yes. Extract latitude and longitude


            //Latitude decimal

            var wholeLat = double.Parse(Words[3], NmeaCultureInfo);

            int integerPart = (int)wholeLat;
            int DegreesLat = integerPart / 100;
            string[] tempLat = Words[3].Substring(2).Split('.');
            int MinutesLat = integerPart % 100;
            string SecLat = "0";
            if (tempLat.Length >= 2)
            {
                SecLat = "0." + tempLat[1];
            }
            double SecondsLat = double.Parse(SecLat, NmeaCultureInfo) * 60;

            double Latitude = (DegreesLat + (MinutesLat / 60.0) + (SecondsLat / 3600.0));


            //Longitude decimal

            double DegreesLon = double.Parse(Words[5].Substring(0, 3), NmeaCultureInfo);
            string[] tempLon = Words[5].Substring(3).ToString().Split('.');
            double MinutesLon = double.Parse(tempLon[0], NmeaCultureInfo);
            string SecLon = "0";
            if (tempLon.Length >= 2)
            {
                SecLon = "0." + tempLon[1];
            }
            double SecondsLon = double.Parse(SecLon, NmeaCultureInfo) * 60;

            double Longitude = (DegreesLon + (MinutesLon / 60) + (SecondsLon / 3600));
            return (Latitude, Longitude);
        }
    }
}

它分配了2个临时字符串,但它不应该是GC的问题。您可能希望ParseCoordinatesStrings返回(double, double)而不是(string, string),从而最大限度地缩短latitudeStringlongitudeString的生命周期,方法是使它们不会从方法。在这种情况下,只需移动double.Parse

答案 1 :(得分:0)

关于GC和Unity中的解析,有两种处理方法:

传统方式

Unity方式

两者都很有效,但是听起来很简单,实际上,这确实很简单。

传统方式包括使用C#和C ++书中的许多技巧之一,这些技巧通常在其他软件中使用。在其他答案中已经被其他人多次覆盖,因此,尽管听起来很便宜,但我在这里不再介绍。

统一方式是Unity Technologies开发人员解释的正式方式。 (通常是在他们在GDC上的年度展览中解释的。我将解释的方式是在Unity GDC 2016期间进行的,即使在今天,仍然是在Unity中以最优化的方式进行的方式。

在解释如何使用Unity方式之前,我需要先解释一下Unity GC的工作原理,因为即使在今天,许多人仍然不清楚。 GC就像是从头开始构建的块系统,只有在关闭应用程序或软件时才将其清空。 (在PC / Mac上,与在移动设备上有细微的差别,但在PC / Mac上应用它的确会有所不同。)每次使用任何可生成任何类型参数的函数时,它都会创建一个新块在GC中。只要新数据小于以前的数据,就可以覆盖一个块,但是只要应用程序/软件正在运行,就不能将其删除。换句话说,此系统要求您避免嵌套太多数据,而且还要求您尽可能嵌套数据。

这听起来像是一个矛盾,但事实并非如此。这只是意味着您必须了解要嵌套的内容,以便可以根据需要一次嵌套很少的内容。嵌套是避免填充GC的关键。

解决这里问题的最简单的方法是,在APP的开头,生成一个通用的嵌套脚本(您可以使用DontDestroyOnLoad();保留它)。我通常在初始启动画面期间执行此操作。这就是为什么我不使用Unity的预制徽标启动屏幕,而是在自己的场景中构建自己的场景的原因,这样我就可以启动整个应用程序所需的所有扭曲并预先需要静态属性。我通常用最初的假数据块填充这些静态属性,以使它们的块足够大,可以容纳我扔在那里的任何东西。例如,如果需要一个数组,则保留一个由512个整数,浮点数或字符串组成的数组,并用一个足够大的假示例(特别是字符串)填充它们以容纳您的实际数据。

在此“通用”嵌套脚本中,添加应包含原始GPS数据(字符串)及其分割部分(包括字符串数组或转换后的数据(例如浮点数)的参数)的参数。每当您读取GPS数据(原始字符串)时,就始终将其存储在通用嵌套脚本中并覆盖前一个脚本。 (如果要保留以前的数据,建议您只保留转换后的数据,而不保留原始GPS数据。为什么还是要重做转换,对吗?)

理想地,您将所有转换调用和数据保存在通用嵌套脚本中。您只需要记住以线性方式工作(即避免使用多个脚本在单个帧中更改嵌套值),通常情况下,是使用具有处理所有请求(停止/忽略重复请求)的mastermind函数。

为什么要这样做?这样,您将GC填充到最低限度,并一次又一次地重复使用其相同的内存块。那些内存块不需要被GC清除,因为它们一直在使用。几乎没有块的浪费,并且块的大小恰好是您需要的大小,并且没有随机性(这意味着无需为更大的数据创建新的更大的块)。

以下是指向欧洲GDC 2016期间Unity最佳化展示的链接(带有用于准确观察有关内存管理和GC的解释的时间戳):https://youtu.be/j4YAY36xjwE?t=1432

如果您想知道,是的,我本人,请在通用嵌套脚本中保留一堆整数,这些整数在我什至只是进行for()调用来替换foreach()时都会使用(因为foreach()会产生一些不能重复使用的块,每次使用后总是扔给GC。)