web-dev-qa-db-ja.com

String.Substring()はこのコードをボトルネックにしているようです

はじめに

私はかなり前に作成したこのお気に入りのアルゴリズムを使用しており、新しいプログラミング言語、プラットフォームなどで何らかのベンチマークとして常に作成および再作成しています。私の主なプログラミング言語はC#ですが、コードを文字通りコピー&ペーストして構文をわずかに変更し、Javaでビルドし、1000倍高速で実行できることがわかりました。

コード

かなりのコードがありますが、主な問題であると思われるこのスニペットのみを紹介します。

for (int i = 0; i <= s1.Length; i++) 
{
    for (int j = i + 1; j <= s1.Length - i; j++)
    {
        string _s1 = s1.Substring(i, j);
        if (tree.hasLeaf(_s1))
         ...

データ

この特定のテストの文字列s1の長さは1ミリオン文字(1MB)であることを指摘することが重要です。

測定値

Visual Studioでのコード実行のプロファイルを作成しました。これは、ツリーの構築方法またはツリーの横断方法が最適ではないと考えたためです。結果を調べると、string _s1 = s1.Substring(i, j);行が実行時間の90%以上に対応しているようです。

追加の観察

私が気づいたもう1つの違いは、私のコードはシングルスレッドJavaであるにもかかわらず、Parallel.For()およびマルチスレッドテクニックを使用していても、8つのコアすべて(CPU使用率100%)を使用してコードを実行することですコードはせいぜい35〜40%しか使用しません。アルゴリズムはコア数(および周波数)に比例してスケーリングするため、これを補正しましたが、Javaのスニペットは100〜1000倍の速度で実行されます。

推論

これが起こっている理由は、C#の文字列が不変であるため、String.Substring()がコピーを作成する必要があるという事実と関係があると推測します。これは、多くの反復を持つネストされたforループ内にあるため、多くのコピーとガベージコレクションは進行中ですが、JavaでSubstringがどのように実装されているのかわかりません。

質問

この時点で私のオプションは何ですか?部分文字列の数と長さを回避する方法はありません(これはすでに最大限に最適化されています)。この問題を解決できる方法(またはデータ構造)を知らない方法はありますか?

要求された最小限の実装(コメントから)

接尾辞ツリーの実装は省略しました。これは、構築中はO(n)であり、走査中はO(log(n))です

public static double compute(string s1, string s2)
{
    double score = 0.00;
    suffixTree stree = new suffixTree(s2);
    for (int i = 0; i <= s1.Length; i++) 
    {
        int longest = 0;
        for (int j = i + 1; j <= s1.Length - i; j++)
        {
            string _s1 = s1.Substring(i, j);
            if (stree.has(_s1))
            {
                score += j - i;
                longest = j - i;
            }
            else break;
         };

        i += longest;
    };
    return score;
}

プロファイラーのスクリーンショットスニペット

これは300.000文字のサイズの文字列s1でテストされていることに注意してください。何らかの理由で、C#で1ミリオン文字が終了することはありませんが、Javaでは0.75秒しかかかりません。メモリ消費量とガベージコレクションの数は、メモリの問題を示していないようです。ピークは約400 MBでしたが、巨大なサフィックスツリーを考慮すると、これは正常なようです。奇妙なガベージコレクションパターンも見つかりませんでした。

CPU profiler

Memory profiler

74
Ilhan

発行元

2日間と3晩続いた栄光の戦いを経て(そして、コメントからの素晴らしいアイデアと考え)、私はついにこの問題を修正することができました!

文字列が大きすぎて、string.Substring(i, j)によってコピーを行う余裕がないため、string.Substring(i, j)関数が文字列のサブ文字列を取得するための許容可能な解決策ではない同様の問題に直面している人に答えを投稿したいと思いますC#文字列は不変であり、回避できないためコピーする必要があります)またはstring.Substring(i, j)が同じ文字列上で膨大な回数呼び出されているため(ネストされたforループのように)、ガベージコレクタに苦労します私の場合両方!

試行

StringBuilderStreamsIntptrおよびMarshalを使用したアンマネージメモリ割り当てなど、多くの推奨事項を試しました。 unsafe{}ブロック、さらにIEnumerableとyieldを作成しても、指定された位置内の参照によって文字が返されます。これらの試みはすべて、最終的に失敗しました。パフォーマンスを危険にさらすことなく、文字ごとにツリーを移動する簡単な方法がないため、何らかの形でデータを結合する必要があったためです。配列内の複数のメモリアドレスに一度にまたがる方法しかなかった場合、C++でポインタ演算を使用してできるようになります。.がある場合を除きます(@Ivan Stoevのコメントによる)

ソリューション

ソリューションはSystem.ReadOnlySpan<T>(文字列が不変であるためSystem.Span<T>にはできません)を使用していたため、特に、コピーを作成せずに既存の配列内のメモリアドレスのサブ配列を読み取ることができました。

この投稿されたコードの一部:

string _s1 = s1.Substring(i, j);
if (stree.has(_s1))
{
    score += j - i;
    longest = j - i;
}

次のように変更されました。

if (stree.has(i, j))
{
    score += j - i;
    longest = j - i;
}

ここでstree.has()は2つの整数(部分文字列の位置と長さ)を取り、次のことを行います。

ReadOnlySpan<char> substr = s1.AsSpan(i, j);

substr変数は、コピーではなく、最初のs1配列の文字のサブセットへの参照であることに注意してください! (s1変数は、この関数からアクセス可能になりました)

これを書いている時点では、C#7.2および.NET Framework 4.6.1を使用していることに注意してください。つまり、スパン機能を取得するには、[プロジェクト]> [NuGetパッケージの管理]に移動し、[プレリリースを含める]チェックボックスをオンにして、メモリをインストールします。

最初のテスト(長さ1ミリオンの文字列、つまり1MB)を再実行すると、速度は2+分(2分後に待機をあきらめた)から〜86ミリ秒に増加しました!!

85
Ilhan