6

始める前に、「時期尚早の最適化」という用語を知っています。ただし、次のスニペットは、改善できる領域であることが証明されています。

大丈夫。現在、文字列ベースのパケットで動作するネットワーク コードがいくつかあります。パケットに文字列を使用するのは愚かで、狂っており、遅いことは承知しています。残念ながら、クライアントを制御できないため、文字列を使用する必要があります。

各パケットはによって終了され\0\r\n、現在、ストリームから個々のパケットを読み取るために StreamReader/Writer を使用しています。主なボトルネックは 2 か所にあります。

まず、文字列の末尾にある厄介な小さな null バイトを削除する必要があります。現在、次のようなコードを使用しています。

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 以上の接続をすべて 1 秒あたり約 25 ~ 40 パケットで通信するため (ゲーム サーバー)、この GC の問題が問題になりつつあります。では、最初の質問です。文字列の末尾にある '\0' を削除するより効率的な方法は何ですか? 効率的とは、速度だけでなく、GC の観点からも意味します (最終的には、新しい文字列オブジェクトを作成せずにそれを取り除く方法が必要です!)。

2 つ目の問題も 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 ユーザーを超えて増加すると、多くのパフォーマンスの問題が発生します (高速 RAM が必要です:3)。

以前にこの種のことを経験した人はいますか、それとも次に何を調べるべきかについての指針を教えてくれますか? たぶん、いくつかの魔法のクラスか、気の利いたポインタの魔法でしょうか?

(PS. StringBuilder は、文字列を作成していないため役に立ちません。通常、文字列を分割しています。)

現在、各パラメータのインデックスと長さを分割するのではなく保存するインデックス ベースのシステムに基づくいくつかのアイデアがあります。考え?

他にもいくつか。mscorlib を逆コンパイルして文字列クラス コードを参照すると、P/Invoke を介して呼び出しが行われているように思えます。IndexOfこれは、呼び出しごとにオーバーヘッドが追加されたことを意味します。間違っている場合は修正してください。配列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;
}

ありがたいことに、サブストリングは高速に見えます (そして効率的です!):

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

ここでこの回答を読んだ後、ポインターベースのソリューションが見つかると思います...

ありがとう。

4

1 に答える 1