web-dev-qa-db-ja.com

.NETで文字列が不変の場合、SubstringがO(n)時間を要するのはなぜですか?

.NETでは文字列が不変であることを考えると、なぜstring.Substring()O(1)ではなくO(substring.Length)時間かかるように設計されているのでしょうか。

つまり、トレードオフはありましたか?

443
Mehrdad

更新:私はこの質問がとても気に入ったので、ブログに書きました。 文字列、不変性、永続性 を参照してください


短い答えは:O(n)はO(1)であり、nが大きくならない場合。ほとんどの人は小さな部分文字列を抽出します。そのため、複雑さが漸近的にどのように成長するかは、完全に無関係です

長い答えは次のとおりです。

インスタンスでの操作により、コピー(新しい割り当て)の少量(通常O(1)またはO(lg n))だけで元のメモリの再利用が許可されるように構築された不変のデータ構造「永続的な」不変データ構造と呼ばれます。 .NETの文字列は不変です。あなたの質問は本質的に「なぜ彼らは永続的ではないのですか?」

なぜなら、.NETプログラムの文字列に対して通常行われる操作を見ると、関連するあらゆる方法でほとんど悪化しないからですまったく新しい文字列を作成します。 複雑で永続的なデータ構造を構築するための費用と難しさは、それ自体に対価を支払っていません。

人々は通常、「部分文字列」を使用して、短い文字列(たとえば、10文字または20文字)を多少長い文字列(おそらく数百文字)から抽出します。コンマ区切りファイルにテキスト行があり、3番目のフィールド(姓)を抽出したい場合。行の長さは数百文字、名前は数十文字になります。文字列の割り当てと50バイトのメモリコピーは、最新のハードウェアでは驚くほど高速です。既存の文字列の中央へのポインターと長さで構成される新しいデータ構造の作成がalso驚くほど高速であることは無関係です。 「十分に高速」とは、定義上、十分に高速です。

抽出された部分文字列は通常、サイズが小さく、寿命が短いです。ガベージコレクターはすぐにそれらを再利用する予定であり、そもそもヒープの多くのスペースを占有しませんでした。そのため、ほとんどのメモリの再利用を促進する永続的な戦略を使用することもメリットではありません。ガベージコレクターを遅くするだけです。これは、内部ポインターの処理を心配する必要があるためです。

人々が通常文字列に対して行う部分文字列操作が完全に異なる場合、永続的なアプローチをとることは理にかなっています。人々が通常100万文字の文字列を持ち、数十万文字の範囲のサイズの重複する数千の部分文字列を抽出し、それらの部分文字列がヒープ上に長く住んでいた場合、永続的な部分文字列を使用するのは完全に理にかなっていますアプローチ;それは無駄で愚かなことではありません。しかし、ほとんどの基幹業務プログラマーは、これらの種類のもののように漠然とでも何もしません。 .NETは、Human Genome Projectのニーズに合わせて調整されたプラットフォームではありません。 DNA分析プログラマは、これらの文字列の使用特性に関する問題を毎日解決する必要があります。オッズはあなたがしないことは良いことです。 their使用シナリオにほぼ一致する独自の永続データ構造を構築する少数の人。

たとえば、私のチームは、入力時にC#およびVBコードのオンザフライ分析を行うプログラムを作成します。これらのコードファイルの一部は巨大なであるため、O(n)文字列操作を行って部分文字列を抽出したり、文字を挿入または削除したりすることはできません。既存の文字列データの既存のレキシカルの大部分を迅速かつ効率的に再利用できるように、テキストバッファーへの編集を表すための永続的な不変のデータ構造を構築しました。通常の編集時の構文解析。これは解決が難しい問題であり、その解決策はC#およびVBコード編集の特定のドメインに合わせて細かく調整されました。組み込みの文字列型がこの問題を解決することを期待するのは非現実的です。

417
Eric Lippert

正確にbecause文字列は不変なので、.Substringは元の文字列の少なくとも一部のコピーを作成する必要があります。 nバイトのコピーを作成するには、O(n)時間かかります。

constant時間で大量のバイトをコピーするとどう思いますか?


編集:Mehrdadは、文字列をまったくコピーせず、その一部への参照を保持することをお勧めします。

.Netでは、誰かが.SubString(n, n+3)(文字列の中央のnに対して)を呼び出すマルチメガバイト文字列を検討してください。

今、1つの参照が4文字を保持しているという理由だけで、完全な文字列をガベージコレクトすることはできませんか?それはとんでもないスペースの無駄のようです。

さらに、部分文字列(部分文字列の内部にあることもある)への参照を追跡し、GCの無効化を回避するために最適なタイミングでコピーを試みる(上記)ため、この概念は悪夢になります。 .SubStringをコピーして、単純で不変のモデルを維持する方がはるかに簡単で信頼性が高いです。


EDIT:ここに 良い小さな読み取り 大きな文字列内の部分文字列への参照を保持する危険性について。

119
abelenky

Javaは(.NETとは対照的に)Substring()を行う2つの方法を提供します。参照のみを保持するか、部分文字列全体を新しいメモリ位置にコピーするかを検討できます。

単純な.substring(...)は、内部で使用されるchar配列を元のStringオブジェクトと共有します。その後、new String(...)を使用して、必要に応じて新しい配列にコピーできます(元の1)。

この種の柔軟性は開発者にとって最良の選択肢だと思います。

33
sll

Javaは以前より大きな文字列を参照していましたが、次のとおりです。

Javaは動作をcopying に変更し、メモリリークを回避しました。

しかし、それは改善できると感じています:条件付きでコピーするだけではどうですか?

部分文字列が親のサイズの少なくとも半分であれば、親を参照できます。それ以外の場合は、コピーを作成するだけです。これにより、多くのメモリリークを回避しながら、大きな利点を提供します。

12
Mehrdad

ここでの回答は、「ブラケット問題」に対処していません。つまり、.NETの文字列は、BStr(ポインタの「前」にメモリに格納されている長さ)とCStr(文字列が'\ 0')。

したがって、文字列「Hello there」は次のように表されます。

0B 00 00 00 48 00 65 00 6C 00 6F 00 20 00 74 00 68 00 65 00 72 00 65 00 00 00

fixed- statementのchar*に割り当てられている場合、ポインターは0x48を指します。)

この構造により、文字列の長さをすばやく検索でき(多くのコンテキストで有用)、P/InvokeでNULLで終わる文字列を期待するWin32(または他の)APIにポインタを渡すことができます。

Substring(0, 5)」を実行すると、「ああ、でも最後の文字の後にヌル文字があると約束した」というルールは、コピーを作成する必要があると言っています。最後に部分文字列を取得した場合でも、他の変数を破損せずに長さを配置する場所はありません。


ただし、「文字列の途中」について本当に話したい場合がありますが、P/Invokeの動作を必ずしも気にする必要はありません。最近追加されたReadOnlySpan<T>構造を使用して、コピーなしのサブストリングを取得できます。

string s = "Hello there";
ReadOnlySpan<char> hello = s.AsSpan(0, 5);
ReadOnlySpan<char> ell = hello.Slice(1, 3);

ReadOnlySpan<char> "サブストリング"は長さを個別に保存し、値の末尾の後に '\ 0'があることを保証しません。 「文字列のように」さまざまな方法で使用できますが、BStrまたはCStrのどちらの特性も持たないため(これらの両方がはるかに少ない)、「文字列」ではありません。 P/Invokeを(直接)実行しない場合、違いはほとんどありません(呼び出したいAPIにReadOnlySpan<char>オーバーロードがない限り)。

ReadOnlySpan<char>は参照型のフィールドとして使用できないため、ReadOnlyMemory<char>s.AsMemory(0, 5))もあります。これはReadOnlySpan<char>を持つ間接的な方法なので、同じ違いがあります。 -from -stringが存在します。

以前の回答のいくつかの回答/コメントは、5文字について話し続ける間、ガベージコレクターが100万文字の文字列を保持する必要があることは無駄であると述べました。それがまさにReadOnlySpan<char>アプローチで得られる振る舞いです。短い計算をしているだけなら、ReadOnlySpanアプローチの方がおそらく良いでしょう。しばらく保持する必要があり、元の文字列の一部のみを保持する場合は、適切な部分文字列を(余分なデータを削除するために)行うことをお勧めします。中間のどこかに移行ポイントがありますが、それは特定の使用法に依存します。

2
bartonjs