JavaのStringクラスがより効率的なindexOf()を実装しないのはなぜですか?


9

スタックオーバーフローに関する以下の質問の後に

/programming/5564610/fast-alernative-for-stringindexofstring-str

なぜjava(少なくとも6つ)がより効率的な実装を使用しないのかと疑問に思いましたか?

コードは次のとおりです。

java.lang.String#indexOf(String str)

1762    static int indexOf(char[] source, int sourceOffset, int sourceCount,
1763                       char[] target, int targetOffset, int targetCount,
1764                       int fromIndex) {
1765        if (fromIndex >= sourceCount) {
1766            return (targetCount == 0 ? sourceCount : -1);
1767        }
1768        if (fromIndex < 0) {
1769            fromIndex = 0;
1770        }
1771        if (targetCount == 0) {
1772            return fromIndex;
1773        }
1774
1775        char first  = target[targetOffset];
1776        int max = sourceOffset + (sourceCount - targetCount);
1777
1778        for (int i = sourceOffset + fromIndex; i <= max; i++) {
1779            /* Look for first character. */
1780            if (source[i] != first) {
1781                while (++i <= max && source[i] != first);
1782            }
1783
1784            /* Found first character, now look at the rest of v2 */
1785            if (i <= max) {
1786                int j = i + 1;
1787                int end = j + targetCount - 1;
1788                for (int k = targetOffset + 1; j < end && source[j] ==
1789                         target[k]; j++, k++);
1790
1791                if (j == end) {
1792                    /* Found whole string. */
1793                    return i - sourceOffset;
1794                }
1795            }
1796        }
1797        return -1;
1798    }

3
これは一般的にJava 6ではなく、OpenJDKコードであることに注意してください。
ペーテルTörök

1
@PéterTörök、真実ですが、jdk1.6.0_23のsrc.zipを解凍してString.javaファイルを見ると、同じ正確なコードが表示されます
Yaneeve

1
私は、Oracleの弁護士だったら@Yaneeveは、うーん、面白い...、私は確かにこの:-)に関するいくつかの考えを持っているでしょう
ペーテルTörök

2
このルーチンは、SSE4.2命令を介して(利用可能な場合)カバーの下で最適化されます-ハードウェアがサポートする場合は、適切なJVMフラグでサポートを有効にします。
Nim

2
@ピーター-なぜ?彼はJava 6コードをコピーしていないか、営業秘密/機密保持契約に違反していません。彼は、この領域では2つのファイルは同じであると述べました。
スティーブンC

回答:


26

「効率」はすべてトレードオフに関するものであり、「最良の」アルゴリズムは多くの要因に依存します。の場合、indexOf()これらの要因の1つは予想される文字列のサイズです。

JDKのアルゴリズムは、既存の文字配列への単純なインデックス付き参照に基づいています。参照するKnuth-Morris-Prattはint[]、入力文字列と同じサイズの新しいものを作成する必要があります。Boyer-Mooreの場合、いくつかの外部テーブルが必要ですが、そのうちの少なくとも1つは2次元です(私が思うに、BMを実装したことはありません)。

したがって、問題は次のようになります。追加のオブジェクトの割り当てとルックアップテーブルの作成は、アルゴリズムのパフォーマンスの向上によって相殺されますか?覚えておいてください、私たちはO(N 2)からO(N)への変更について話しているのではなく、単に各Nに対して取られるステップ数の削減について話しているのです。

そして、JDKデザイナーが「X文字未満の文字列の場合、単純なアプローチの方が速く、それより長い文字列の通常の使用は期待できません。長い文字列を使用する人は、最適化の方法を知っています。彼らの検索。」


11

誰もが知っている標準的な効率的な文字列検索アルゴリズムはBoyer-Mooreです。特に、文字セットと同じサイズの移行テーブルを作成する必要があります。ASCIIの場合、これは256エントリの配列です。これは、長い文字列に見合った一定のオーバーヘッドであり、小さな文字列をだれでも気にするほど遅くすることはありません。ただし、Javaは2バイト文字を使用するため、テーブルのサイズは64Kになります。通常の使用では、このオーバーヘッドはBoyer-Mooreからの予想されるスピードアップを超えるため、Boyer-Mooreは価値がありません。

もちろん、そのテーブルのほとんどは同じエントリを持つので、例外を効率的な方法で格納し、例外にないものにはデフォルトを提供できると考えるかもしれません。残念ながら、これを行う方法にはルックアップのオーバーヘッドが伴い、コストがかかりすぎて効率的ではありません。(1つの問題として、予期しない分岐が発生した場合にパイプラインがストールし、コストが高くなる傾向があることに注意してください。)

Unicodeでは、この問題はエンコーディングに大きく依存することに注意してください。Javaが作成されたとき、Unicodeは64 K以内に収まるため、Javaは1文字あたり2バイトを使用し、文字列の長さは単にバイト数を2で割ったものでした(このエンコーディングはUCS-2と呼ばれていました)。特定の文字にジャンプするか、特定の部分文字列を抽出します。indexOf()問題ではありませんでした。残念ながら、Unicodeはその後成長しているため、Unicode文字は常にJava文字に収まりません。これにより、Javaは彼らが回避しようとしていたサイズの問題に陥りました。(現在、エンコーディングはUTF-16です。)下位互換性のために、Java文字のサイズを変更することはできませんでしたが、Unicode文字とJava文字が同じものであるというミームがあります。そうではありませんが、それを知っているJavaプログラマーはほとんどいませんし、日常生活で遭遇する可能性はさらに低いものです。(同じ理由で、Windowsと.NETは同じパスをたどったことに注意してください。)

他の一部の言語および環境では、代わりにUTF-8が使用されます。ASCIIは有効なUnicodeであり、Boyer-Mooreは効率的であるという優れた特性があります。トレードオフは、可変バイトの問題に注意を払わないと、UTF-16の場合よりも明らかに影響が大きいことです。


IMOは、64Kの割り当てが「予想されるスピードアップを超えている」と主張しているのは意味がありません。1つはメモリサイズで、もう1つはCPUサイクルです。それらは直接比較することはできません。
ジェリーコフィン

1
@ jerry-coffin:直接比較するのは妥当です。データを割り当てて64Kデータ構造を初期化するには、無視できないCPUサイクルが必要です。
btilly

1
ボイヤー・ムーアのコストの詳細な説明については+1
kdgregory

初期化はサイズに対して明らかに線形ですが、少なくとも典型的なケースでは、割り当てはほぼ一定の速度です。
ジェリーコフィン

1

それは主にこれに帰着します:最も明白な改善はボイヤー・ムーア、またはそのいくつかの変種によるものです。ただし、BMとバリアントは完全に異なるインターフェースを必要としています。

特に、Boyer-Mooreと導関数は実際には2つのステップで機能します。最初に初期化を行います。これにより、検索している文字列だけに基づいてテーブルが作成さます。これにより、その文字列を必要なだけ検索するために使用できるテーブルが作成されます。

テーブルをメモし、同じターゲット文字列の後続の検索に使用することで、これを既存のインターフェイスに適合させることができます。これは、Sunの本来の目的であるこの機能にはあまり適合しないと思います。それは、他のほとんどに依存しない低レベルのビルディングブロックであることです。他のインフラストラクチャにかなり依存する高レベルの関数にすることは、(とりわけ)使用するメモ化インフラストラクチャが部分文字列検索を使用できないようにする必要があることを意味します。

その最も可能性の高い結果は、このようなもの(つまり、スタンドアロンの検索ルーチン)を別の名前で単純に再実装し、より高いレベルのルーチンを既存の名前で再実装することだと思います。すべてを考慮すると、新しい名前で新しい高レベルのルーチンを作成する方がおそらく意味があると思います。

それに対する明白な代替策は、メモ化のある種の簡略化されたバージョンを使用することです。たとえば、静的に1つのテーブルのみを保存し、ターゲット文字列がテーブルの作成に使用されたものと同一である場合にそれを再利用します。 。それは確かに可能ですが、多くのユースケースでは最適とは言えません。スレッドセーフにすることも簡単ではありません。

別の可能性は、BM検索の2ステップの性質を明示的に公開することです。私は誰もがそのアイデアを本当に好きだとは思いません-それはかなり高いコスト(不器用さ、親しみの欠如)を持ち、多くのユースケースにはほとんどまたはまったくメリットがありません(この件に関するほとんどの研究は、平均文字列長が20文字)。


1
BMの2ステップの性質を公開したとしても、64Kのジャンプテーブルはレベル1のCPUキャッシュに収まらないため、良いパフォーマンスが得られるとは思えません。より遅いキャッシュをヒットする必要があるというコストは、必要な操作が少ないという事実を上回る可能性があります。
btilly

@btilly:テーブル全体を実際に使用する可能性が高い場合は、大きな違いが生じますが、少なくとも一般的なケースでは、約1Kのテーブルがキャッシュに置かれ、残りはテーブルの間にのみアクセスされます。初期化。
ジェリーコフィン

@ jerry-coffin:あなたは明らかにアジアのテキストを処理できることを気にしません。
btilly

1
@btilly:そうではありません-私が気にしないということではありません。少なくとも多くのユーザーにとって、それはあまり一般的ではないことを私は知っています。アジアのテキストを扱う場合でも、韓国語 3種類の日本語の文字 2種類の中国語の文字などを含む単一の文字列を検索することはほとんどありません。はい、アジアのアルファベットは英語よりも大きいですが、文字列にはまだ何万もの固有の文字が含まれていません。たとえば、20文字の文字列の場合、テーブルのキャッシュ行が20を超える必要はありません。
ジェリーコフィン

最悪の場合、検索文字列の一意の文字ごとに1つのキャッシュラインを使用します。
ジェリーコフィン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.