提高字符串解析性能

6
在开始之前,我知道“过早优化”的术语。然而,以下代码段已经证明是可以改进的领域。
好吧。我们目前有一些与基于字符串的数据包相关的网络代码。我知道使用字符串来传输数据包是愚蠢、疯狂和缓慢的。不幸的是,我们无法控制客户端,因此必须使用字符串。
每个数据包以 \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,甚至在Substring的末尾链接Split。平均来说,这会导致几乎每个包创建大约20个新字符串对象并处理12个。当负载超过400个用户时,这会导致很多性能问题(我们有快速的内存 :3)

有人有过类似的经验或者可以给我们一些指针来指出下一步该看什么吗?也许有一些神奇的类或巧妙的指针技巧?

(PS. StringBuilder对我们没有帮助,因为我们不是在构建字符串,我们通常是分割它们。)

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

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

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

阅读了这里的回答后,我认为可以找到基于指针的解决方案...你有什么想法吗?
谢谢。

2
你有没有考虑过将消息处理为 IEnumerable<char>char 数组,而不是字符串? - Daniel Hilgarth
@DanielHilgarth - 好主意...我也有类似的想法(尽管我还没有想到IEnumerable)。 - Tim
@DanielHilgarth 有这个想法,但大部分的代码也是基于字符串解析的(例如,int.Parse(TheStringParamHere)),所以我们最终仍需将其转换为字符串。那么您针对 IEnumerable 的想法是什么呢?它会被如何使用? - jduncanator
所有的分析都指向了巨大的时间花费在GC上,而且都集中在使用字符串方法上。(巨大的意思是几乎占据了总CPU时间的60%) - jduncanator
1
只是好奇,你是否正在使用服务器GC模式? - Mike Zboray
显示剩余10条评论
1个回答

2
你可以“作弊”,在编码器级别上工作...
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() 的事实。我们从 StreamReader.ReadLineAsync 使用的临时缓冲区 char [] 中删除 '\0'。

正如所说,我们使用了一个大的 switch-case 语句块,而不是嵌套的 if 语句。我们需要修剪 '\0' 的原因是它可能是参数中某个地方的一部分,而不是数据包类型的一部分。 - jduncanator
聪明,非常聪明 :) 这可能行得通!在浏览 mscorlib 源代码后,似乎也可以使用基于指针的解决方案 ;) 我会告诉你这对性能有什么影响 :) 我可以问一下第二个 for 循环是干什么用的吗? - jduncanator

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