提高字符串解析性能

时间:2013-09-09 06:54:22

标签: c# string performance parsing garbage-collection

在我们开始之前,我知道“过早优化”一词。但是,以下片段已被证明是可以进行改进的领域。

好的。我们目前有一些网络代码可以使用基于字符串的数据包。我知道使用字符串包是愚蠢的,疯狂的和慢的。遗憾的是,我们对客户端没有任何控制权,因此必须使用字符串。

每个数据包都以\0\r\n终止,我们目前使用StreamReader / Writer从流中读取单个数据包。我们的主要瓶颈来自两个地方。

首先:我们需要在字符串末尾修剪那个令人讨厌的小空字节。我们目前使用以下代码:

line = await reader.ReadLineAsync();
line = line.Replace("\0", ""); // PERF this allocates a new string
if (string.IsNullOrWhiteSpace(line))
    return null;
var packet = ClientPacket.Parse(line, cl.Client.RemoteEndPoint);

正如您可以通过那个可爱的小评论看到的那样,我们在修剪'\ 0'时会出现GC性能问题。有许多不同的方法可以修剪一个字符串末尾的'\ 0',但是所有这些都会导致我们获得相同的GC锤击。因为所有字符串操作都是不可变的,所以它们会导致创建一个新的字符串对象。由于我们的服务器处理1000多个连接,所有连接都以每秒25-40个数据包(一个游戏服务器)进行通信,因此这个GC问题正成为一个问题。所以这是我的第一个问题:什么是一种更有效的方法来修剪字符串末尾的'\ 0'?通过高效,我不仅意味着速度,而且还意味着GC明智(最终我想要一种摆脱它而不创建新的字符串对象的方法!)。

我们的第二个问题也源于GC土地。我们的代码看起来有点像:

private static string[] emptyStringArray = new string[] { }; // so we dont need to allocate this
public static ClientPacket Parse(string line, EndPoint from)
{
    const char seperator = '|';

    var first_seperator_pos = line.IndexOf(seperator);
    if (first_seperator_pos < 1)
    {
        return new ClientPacket(NetworkStringToClientPacketType(line), emptyStringArray, from);
    }
    var name = line.Substring(0, first_seperator_pos);
    var type = NetworkStringToClientPacketType(name);
    if (line.IndexOf(seperator, first_seperator_pos + 1) < 1)
        return new ClientPacket(type, new string[] { line.Substring(first_seperator_pos + 1) }, from);
    return new ClientPacket(type, line.Substring(first_seperator_pos + 1).Split(seperator), from);
}

(其中NetworkStringToClientPacketType只是一个大的开关案例块)

正如您所看到的,我们已经做了一些事情来处理GC。我们重用一个静态的“空”字符串,我们检查没有参数的数据包。我唯一的问题是我们正在使用Substring,甚至在子串的末尾链接一个Split。这导致(对于平均分组)创建了近20个新的字符串对象,并且12个被丢弃了每个分组。当负载增加超过400个用户时,这会导致很多性能问题(我们得到快速ram:3)

有没有人曾经有过这类事情的经验,或者可以给我们一些关于下一步的内容的指示?也许是一些神奇的类或一些漂亮的指针魔法?

(PS.CtringBuilder没有帮助,因为我们没有构建字符串,我们通常将它们分开。)

我们目前有一些基于索引的系统的想法,我们存储每个参数的索引和长度,而不是分割它们。想法?

其他一些事情。反编译mscorlib并浏览字符串类代码,在我看来,IndexOf调用是通过P / Invoke完成的,这意味着他们为每次调用增加了开销,如果我错了,请纠正我?使用IndexOf数组手动实施char[]会不会更快?

public int IndexOf(string value, int startIndex, int count, StringComparison comparisonType)
{
    ...
    return TextInfo.IndexOfStringOrdinalIgnoreCase(this, value, startIndex, count);
    ...
}

internal static int IndexOfStringOrdinalIgnoreCase(string source, string value, int startIndex, int count)
{
    ...
    if (TextInfo.TryFastFindStringOrdinalIgnoreCase(4194304, source, startIndex, value, count, ref result))
    {
        return result;
    }
    ...
}

...

[DllImport("QCall", CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool InternalTryFindStringOrdinalIgnoreCase(int searchFlags, string source, int sourceCount, int startIndex, string target, int targetCount, ref int foundIndex);

然后我们到达String.Split,最终调用Substring本身(沿线的某处):

// string
private string[] InternalSplitOmitEmptyEntries(int[] sepList, int[] lengthList, int numReplaces, int count)
{
    int num = (numReplaces < count) ? (numReplaces + 1) : count;
    string[] array = new string[num];
    int num2 = 0;
    int num3 = 0;
    int i = 0;
    while (i < numReplaces && num2 < this.Length)
    {
        if (sepList[i] - num2 > 0)
        {
            array[num3++] = this.Substring(num2, sepList[i] - num2);
        }
        num2 = sepList[i] + ((lengthList == null) ? 1 : lengthList[i]);
        if (num3 == count - 1)
        {
            while (i < numReplaces - 1)
            {
                if (num2 != sepList[++i])
                {
                    break;
                }
                num2 += ((lengthList == null) ? 1 : lengthList[i]);
            }
            break;
        }
        i++;
    }
    if (num2 < this.Length)
    {
        array[num3++] = this.Substring(num2);
    }
    string[] array2 = array;
    if (num3 != num)
    {
        array2 = new string[num3];
        for (int j = 0; j < num3; j++)
        {
            array2[j] = array[j];
        }
    }
    return array2;
}

值得庆幸的是,Substring看起来很快(而且效率很高!):

private unsafe string InternalSubString(int startIndex, int length, bool fAlwaysCopy)
{
    if (startIndex == 0 && length == this.Length && !fAlwaysCopy)
    {
        return this;
    }
    string text = string.FastAllocateString(length);
    fixed (char* ptr = &text.m_firstChar)
    {
        fixed (char* ptr2 = &this.m_firstChar)
        {
            string.wstrcpy(ptr, ptr2 + (IntPtr)startIndex, length);
        }
    }
    return text;
}

在阅读了这个答案here之后,我在想一个基于指针的解决方案可以找到......想法?

感谢。

1 个答案:

答案 0 :(得分:2)

您可以“作弊”并在Encoder级别工作......

public class UTF8NoZero : UTF8Encoding
{
    public override Decoder GetDecoder()
    {
        return new MyDecoder();
    }
}

public class MyDecoder : Decoder
{
    public Encoding UTF8 = new UTF8Encoding();

    public override int GetCharCount(byte[] bytes, int index, int count)
    {
        return UTF8.GetCharCount(bytes, index, count);
    }

    public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
    {
        int count2 = UTF8.GetChars(bytes, byteIndex, byteCount, chars, charIndex);
        int i, j;

        for (i = charIndex, j = charIndex; i < charIndex + count2; i++)
        {
            if (chars[i] != '\0')
            {
                chars[j] = chars[i];
                j++;
            }
        }

        for (int k = j; k < charIndex + count2; k++)
        {
            chars[k] = '\0';
        }

        return count2 + (i - j);
    }
}

请注意,此作弊基于StreamReader.ReadLineAsync仅使用GetChars()这一事实。我们删除char[]使用的临时缓冲区StreamReader.ReadLineAsync缓冲区中的'\ 0'。