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

see*_*eek 1 c# garbage-collection unity-game-engine string-parsing

我需要解析来自 Android 设备内置 GPS 接收器的 NMEA 数据。我每秒以字符串形式接收此数据几次。我很好奇是否可以在没有垃圾收集分配或解析字符串的情况下做到这一点,这是我可以GC.Collect()问心无愧的时刻之一?

正是我需要调用string.split()和其他一些方法,如Substring()和结果转换为 with 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,4M.5,4M ,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);
Run Code Online (Sandbox Code Playgroud)

Ale*_*kiy 5

2020 年 6 月 2 日更新:从 netstandard2.1 开始,您可以用 ReadOnlySpan 替换 string 并在没有分配的情况下执行任务。请参阅https://docs.microsoft.com/en-us/dotnet/api/system.memoryextensions?view=netcore-3.1


你在问how could I manage strings without allocating space?。这是一个答案:您始终可以使用 在没有 GC 压力的情况下在堆栈stackalloc上分配char[]数组,然后使用 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;
}
Run Code Online (Sandbox Code Playgroud)

好的,让我们假设它仍然很慢,我们想进一步优化它。首先,我们应该替换这个条件:

if (words[3] == "" || words[4] == "" || words[5] == "" || words[6] == "")
        return null;
Run Code Online (Sandbox Code Playgroud)

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

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);
}
Run Code Online (Sandbox Code Playgroud)

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

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

它分配了 2 个临时字符串,但对于 GC 来说应该不是问题。您可能希望ParseCoordinatesStringsreturn(double, double)而不是(string, string),通过使它们不从方法返回的局部变量来最小化latitudeStringlongitudeString的生命周期。在这种情况下,只需移到double.Parse那里即可。