文字列内のすべての文字を反復する最速の方法


163

Javaでは、文字列内のすべての文字を反復処理する最速の方法は次のとおりです:

String str = "a really, really long string";
for (int i = 0, n = str.length(); i < n; i++) {
    char c = str.charAt(i);
}

またはこれ:

char[] chars = str.toCharArray();
for (int i = 0, n = chars.length; i < n; i++) {
    char c = chars[i];
}

編集:

私が知りたいのはcharAt、長い反復中にメソッドを繰り返し呼び出すコストがtoCharArray、最初にへの単一の呼び出しを実行してから反復中に配列に直接アクセスするコストよりも少ないか、または多い場合です。

への2つの呼び出しの違いだけでなく、JITウォームアップ時間、JVM起動時間などを念頭に置いて、さまざまな文字列の長さに対して堅牢なベンチマークを提供できればすばらしいと思いますSystem.currentTimeMillis()


18
どうしたのfor (char c : chars)
dasblinkenlight 2012年

理論的には、最初の方が高速であり、とにかく文字列が文字配列です。
Keagan Ladds、2012年

Googleは、多くの場合、良いリソースです:mkyong.com/java/...
ヨハン・シェーベルイ

2
この質問は、イテレータを使用するパフォーマンスを要求するものではありません。私が知りたいことはあるか繰り返し呼び出しのコストcharAtに単一の呼び出しを実行するコストよりも、どちらか未満かそれ以上されて終わるのtoCharArray
オスカル・ロペス

回答:


352

FIRST UPDATE:あなたが最初にこれを読んで、(推奨されません)本番環境で今までにこれを試してくださいする前に:http://www.javaspecialists.eu/archive/Issue237.html Javaの9から始めて、説明したように、溶液はもう動作しません。これは、Javaがデフォルトで文字列をbyte []として格納するためです。

2回目の更新:2016-10-25現在、AMDx64 8コアとソース1.8では、「charAt」とフィールドアクセスの使用に違いはありません。jvmは 'string.charAt(n)'呼び出しをインライン化および合理化するのに十分最適化されているようです。

それはすべてString検査されている長さに依存します。質問が言うように、それが長い文字列に対するものである場合、文字列を検査する最も速い方法は、リフレクションを使用してchar[]文字列のバッキングにアクセスすることです。

9つの異なる手法(下記を参照)を備えた64 AMD Phenom II 4コア955 @ 3.2 GHZ(クライアントモードとサーバーモードの両方)でJDK 8(win32およびwin64)を使用した完全にランダム化されたベンチマークString.charAt(n)は、文字列とreflection、文字列バッキング配列へのアクセスに使用する文字列は、大きな文字列の場合、ほぼ2倍高速です。

実験

  • 9つの異なる最適化手法が試されます。

  • 文字列の内容はすべてランダム化されます

  • テストは、0、1、2、4、8、16などで始まる2の倍数の文字列サイズに対して行われます。

  • テストは文字列サイズごとに1,000回行われます

  • テストは毎回ランダムな順序でシャッフルされます。つまり、テストは毎回ランダムな順序で、1000回以上行われます。

  • テストスイート全体が前方および後方に実行され、最適化と時間に対するJVMウォームアップの影響が示されます。

  • スイート全体が2回実行されます。1回は-clientモードで、もう1回はモード-serverです。

結論

-クライアントモード(32ビット)

長さが1〜256文字の文字列の場合、string.charAt(i)1秒あたり平均1340万〜5億8800万文字の処理で呼び出しが成功します。

また、次のように全体的に5.5%高速(クライアント)および13.9%(サーバー)です。

    for (int i = 0; i < data.length(); i++) {
        if (data.charAt(i) <= ' ') {
            doThrow();
        }
    }

次のように、ローカルの最終的な長さ変数を使用します。

    final int len = data.length();
    for (int i = 0; i < len; i++) {
        if (data.charAt(i) <= ' ') {
            doThrow();
        }
    }

512〜256K文字の長さの長い文字列の場合、リフレクションを使用して文字列のバッキング配列にアクセスするのが最も高速です。この手法は、String.charAt(i)のほぼ2倍の速度です(178%高速)。この範囲の平均速度は、1秒間に11億1,100万文字でした。

フィールドは事前に取得する必要があり、その後、ライブラリで別の文字列に再利用できます。興味深いことに、上記のコードとは異なり、フィールドアクセスでは、ループチェックで 'chars.length'を使用するよりも、ローカルの最終的な長さ変数を使用する方が9%高速です。以下に、フィールドアクセスを最速でセットアップする方法を示します。

   final Field field = String.class.getDeclaredField("value");
   field.setAccessible(true);

   try {
       final char[] chars = (char[]) field.get(data);
       final int len = chars.length;
       for (int i = 0; i < len; i++) {
           if (chars[i] <= ' ') {
               doThrow();
           }
       }
       return len;
   } catch (Exception ex) {
       throw new RuntimeException(ex);
   }

-serverモードに関する特別なコメント

AMD 64マシンの64ビットJavaマシンのサーバーモードで、32文字の長さの文字列の後にフィールドアクセスが勝ち始めます。これは、クライアントモードで512文字長になるまで見られませんでした。

また、注目に値するのは、サーバーモードでJDK 8(32ビットビルド)を実行していた場合、全体的なパフォーマンスは、大きい文字列と小さい文字列の両方で7%遅くなったことです。これは、JDK 8の早期リリースの2013年12月121ビルドでした。したがって、今のところ、32ビットサーバーモードは32ビットクライアントモードよりも遅いようです。

とはいえ、呼び出しに値する唯一のサーバーモードは64ビットマシン上にあるようです。そうしないと、実際にパフォーマンスが低下します。

-server modeAMD64で実行されている32ビットビルドでは、次のように言うことができます。

  1. String.charAt(i)は、全体として明らかな勝者です。サイズは8〜512文字ですが、「新規」、「再利用」、「フィールド」の勝者がいました。
  2. String.charAt(i)がクライアントモードで45%高速化
  3. クライアントモードの大きな文字列の場合、フィールドアクセスは2倍高速です。

言うまでもありませんが、String.chars()(ストリームとパラレルバージョン)はバストです。他のどの方法よりも遅い。StreamsAPIは、一般的な文字列操作を実行するにはかなり遅い方法です。

ウィッシュリスト

Java Stringは、contains(predicate)、forEach(consumer)、forEachWithIndex(consumer)などの最適化されたメソッドを受け入れる述語を持つことができます。したがって、ユーザーが文字列メソッドの長さや繰り返し呼び出しを知る必要がない場合、これらはライブラリの解析のbeep-beep beep高速化に役立ちます。

夢を見続ける :)

ハッピーストリングス!

〜SH

このテストでは、次の9つの方法で、文字列に空白があるかどうかをテストしました。

"charAt1"-通常の方法で文字列の内容を確認します。

int charAtMethod1(final String data) {
    final int len = data.length();
    for (int i = 0; i < len; i++) {
        if (data.charAt(i) <= ' ') {
            doThrow();
        }
    }
    return len;
}

"charAt2"-上記と同じですが、長さの最終ローカルintを作成する代わりにString.length()を使用してください

int charAtMethod2(final String data) {
    for (int i = 0; i < data.length(); i++) {
        if (data.charAt(i) <= ' ') {
            doThrow();
        }
    }
    return data.length();
}

"stream"-新しいJAVA-8文字列のIntStreamを使用して、チェックを実行するための予測を渡す

int streamMethod(final String data, final IntPredicate predicate) {
    if (data.chars().anyMatch(predicate)) {
        doThrow();
    }
    return data.length();
}

"streamPara"-上記と同じですが、OH-LA-LA-平行してください!!!

// avoid this at all costs
int streamParallelMethod(final String data, IntPredicate predicate) {
    if (data.chars().parallel().anyMatch(predicate)) {
        doThrow();
    }
    return data.length();
}

"reuse"-再利用可能なchar []を文字列コンテンツで再充填

int reuseBuffMethod(final char[] reusable, final String data) {
    final int len = data.length();
    data.getChars(0, len, reusable, 0);
    for (int i = 0; i < len; i++) {
        if (reusable[i] <= ' ') {
            doThrow();
        }
    }
    return len;
}

"new1"-文字列からchar []の新しいコピーを取得

int newMethod1(final String data) {
    final int len = data.length();
    final char[] copy = data.toCharArray();
    for (int i = 0; i < len; i++) {
        if (copy[i] <= ' ') {
            doThrow();
        }
    }
    return len;
}

"new2"-上記と同じですが、 "FOR-EACH"を使用します

int newMethod2(final String data) {
    for (final char c : data.toCharArray()) {
        if (c <= ' ') {
            doThrow();
        }
    }
    return data.length();
}

"field1"-ファンシー!! 文字列の内部char []にアクセスするための取得フィールド

int fieldMethod1(final Field field, final String data) {
    try {
        final char[] chars = (char[]) field.get(data);
        final int len = chars.length;
        for (int i = 0; i < len; i++) {
            if (chars[i] <= ' ') {
                doThrow();
            }
        }
        return len;
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }
}

"field2"-上記と同じですが、 "FOR-EACH"を使用します

int fieldMethod2(final Field field, final String data) {
    final char[] chars;
    try {
        chars = (char[]) field.get(data);
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }
    for (final char c : chars) {
        if (c <= ' ') {
            doThrow();
        }
    }
    return chars.length;
}

クライアント-clientモードの複合結果(フォワードテストとバックワードテストを組み合わせたもの)

注:Java 32ビットを使用する-clientモードとJava 64ビットを使用する-serverモードは、AMD64マシンでは以下と同じです。

Size     WINNER  charAt1 charAt2  stream streamPar   reuse    new1    new2  field1  field2
1        charAt    77.0     72.0   462.0     584.0   127.5    89.5    86.0   159.5   165.0
2        charAt    38.0     36.5   284.0   32712.5    57.5    48.3    50.3    89.0    91.5
4        charAt    19.5     18.5   458.6    3169.0    33.0    26.8    27.5    54.1    52.6
8        charAt     9.8      9.9   100.5    1370.9    17.3    14.4    15.0    26.9    26.4
16       charAt     6.1      6.5    73.4     857.0     8.4     8.2     8.3    13.6    13.5
32       charAt     3.9      3.7    54.8     428.9     5.0     4.9     4.7     7.0     7.2
64       charAt     2.7      2.6    48.2     232.9     3.0     3.2     3.3     3.9     4.0
128      charAt     2.1      1.9    43.7     138.8     2.1     2.6     2.6     2.4     2.6
256      charAt     1.9      1.6    42.4      90.6     1.7     2.1     2.1     1.7     1.8
512      field1     1.7      1.4    40.6      60.5     1.4     1.9     1.9     1.3     1.4
1,024    field1     1.6      1.4    40.0      45.6     1.2     1.9     2.1     1.0     1.2
2,048    field1     1.6      1.3    40.0      36.2     1.2     1.8     1.7     0.9     1.1
4,096    field1     1.6      1.3    39.7      32.6     1.2     1.8     1.7     0.9     1.0
8,192    field1     1.6      1.3    39.6      30.5     1.2     1.8     1.7     0.9     1.0
16,384   field1     1.6      1.3    39.8      28.4     1.2     1.8     1.7     0.8     1.0
32,768   field1     1.6      1.3    40.0      26.7     1.3     1.8     1.7     0.8     1.0
65,536   field1     1.6      1.3    39.8      26.3     1.3     1.8     1.7     0.8     1.0
131,072  field1     1.6      1.3    40.1      25.4     1.4     1.9     1.8     0.8     1.0
262,144  field1     1.6      1.3    39.6      25.2     1.5     1.9     1.9     0.8     1.0

サーバー-serverモードの複合結果(フォワードテストとバックワードテストを組み合わせたもの)

注:これは、AMD64でサーバーモードで実行されているJava 32ビットのテストです。Java 64ビットのサーバーモードは、フィールドアクセスが32文字のサイズの後に勝ち始めることを除いて、クライアントモードのJava 32ビットと同じでした。

Size     WINNER  charAt1 charAt2  stream streamPar   reuse    new1    new2  field1  field2
1        charAt     74.5    95.5   524.5     783.0    90.5   102.5    90.5   135.0   151.5
2        charAt     48.5    53.0   305.0   30851.3    59.3    57.5    52.0    88.5    91.8
4        charAt     28.8    32.1   132.8    2465.1    37.6    33.9    32.3    49.0    47.0
8          new2     18.0    18.6    63.4    1541.3    18.5    17.9    17.6    25.4    25.8
16         new2     14.0    14.7   129.4    1034.7    12.5    16.2    12.0    16.0    16.6
32         new2      7.8     9.1    19.3     431.5     8.1     7.0     6.7     7.9     8.7
64        reuse      6.1     7.5    11.7     204.7     3.5     3.9     4.3     4.2     4.1
128       reuse      6.8     6.8     9.0     101.0     2.6     3.0     3.0     2.6     2.7
256      field2      6.2     6.5     6.9      57.2     2.4     2.7     2.9     2.3     2.3
512       reuse      4.3     4.9     5.8      28.2     2.0     2.6     2.6     2.1     2.1
1,024    charAt      2.0     1.8     5.3      17.6     2.1     2.5     3.5     2.0     2.0
2,048    charAt      1.9     1.7     5.2      11.9     2.2     3.0     2.6     2.0     2.0
4,096    charAt      1.9     1.7     5.1       8.7     2.1     2.6     2.6     1.9     1.9
8,192    charAt      1.9     1.7     5.1       7.6     2.2     2.5     2.6     1.9     1.9
16,384   charAt      1.9     1.7     5.1       6.9     2.2     2.5     2.5     1.9     1.9
32,768   charAt      1.9     1.7     5.1       6.1     2.2     2.5     2.5     1.9     1.9
65,536   charAt      1.9     1.7     5.1       5.5     2.2     2.4     2.4     1.9     1.9
131,072  charAt      1.9     1.7     5.1       5.4     2.3     2.5     2.5     1.9     1.9
262,144  charAt      1.9     1.7     5.1       5.1     2.3     2.5     2.5     1.9     1.9

完全に実行可能なプログラムコード

(Java 7以前でテストするには、2つのストリームテストを削除します)

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.function.IntPredicate;

/**
 * @author Saint Hill <http://stackoverflow.com/users/1584255/saint-hill>
 */
public final class TestStrings {

    // we will not test strings longer than 512KM
    final int MAX_STRING_SIZE = 1024 * 256;

    // for each string size, we will do all the tests
    // this many times
    final int TRIES_PER_STRING_SIZE = 1000;

    public static void main(String[] args) throws Exception {
        new TestStrings().run();
    }

    void run() throws Exception {

        // double the length of the data until it reaches MAX chars long
        // 0,1,2,4,8,16,32,64,128,256 ... 
        final List<Integer> sizes = new ArrayList<>();
        for (int n = 0; n <= MAX_STRING_SIZE; n = (n == 0 ? 1 : n * 2)) {
            sizes.add(n);
        }

        // CREATE RANDOM (FOR SHUFFLING ORDER OF TESTS)
        final Random random = new Random();

        System.out.println("Rate in nanoseconds per character inspected.");
        System.out.printf("==== FORWARDS (tries per size: %s) ==== \n", TRIES_PER_STRING_SIZE);

        printHeadings(TRIES_PER_STRING_SIZE, random);

        for (int size : sizes) {
            reportResults(size, test(size, TRIES_PER_STRING_SIZE, random));
        }

        // reverse order or string sizes
        Collections.reverse(sizes);

        System.out.println("");
        System.out.println("Rate in nanoseconds per character inspected.");
        System.out.printf("==== BACKWARDS (tries per size: %s) ==== \n", TRIES_PER_STRING_SIZE);

        printHeadings(TRIES_PER_STRING_SIZE, random);

        for (int size : sizes) {
            reportResults(size, test(size, TRIES_PER_STRING_SIZE, random));

        }
    }

    ///
    ///
    ///  METHODS OF CHECKING THE CONTENTS
    ///  OF A STRING. ALWAYS CHECKING FOR
    ///  WHITESPACE (CHAR <=' ')
    ///  
    ///
    // CHECK THE STRING CONTENTS
    int charAtMethod1(final String data) {
        final int len = data.length();
        for (int i = 0; i < len; i++) {
            if (data.charAt(i) <= ' ') {
                doThrow();
            }
        }
        return len;
    }

    // SAME AS ABOVE BUT USE String.length()
    // instead of making a new final local int 
    int charAtMethod2(final String data) {
        for (int i = 0; i < data.length(); i++) {
            if (data.charAt(i) <= ' ') {
                doThrow();
            }
        }
        return data.length();
    }

    // USE new Java-8 String's IntStream
    // pass it a PREDICATE to do the checking
    int streamMethod(final String data, final IntPredicate predicate) {
        if (data.chars().anyMatch(predicate)) {
            doThrow();
        }
        return data.length();
    }

    // OH LA LA - GO PARALLEL!!!
    int streamParallelMethod(final String data, IntPredicate predicate) {
        if (data.chars().parallel().anyMatch(predicate)) {
            doThrow();
        }
        return data.length();
    }

    // Re-fill a resuable char[] with the contents
    // of the String's char[]
    int reuseBuffMethod(final char[] reusable, final String data) {
        final int len = data.length();
        data.getChars(0, len, reusable, 0);
        for (int i = 0; i < len; i++) {
            if (reusable[i] <= ' ') {
                doThrow();
            }
        }
        return len;
    }

    // Obtain a new copy of char[] from String
    int newMethod1(final String data) {
        final int len = data.length();
        final char[] copy = data.toCharArray();
        for (int i = 0; i < len; i++) {
            if (copy[i] <= ' ') {
                doThrow();
            }
        }
        return len;
    }

    // Obtain a new copy of char[] from String
    // but use FOR-EACH
    int newMethod2(final String data) {
        for (final char c : data.toCharArray()) {
            if (c <= ' ') {
                doThrow();
            }
        }
        return data.length();
    }

    // FANCY!
    // OBTAIN FIELD FOR ACCESS TO THE STRING'S
    // INTERNAL CHAR[]
    int fieldMethod1(final Field field, final String data) {
        try {
            final char[] chars = (char[]) field.get(data);
            final int len = chars.length;
            for (int i = 0; i < len; i++) {
                if (chars[i] <= ' ') {
                    doThrow();
                }
            }
            return len;
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    // same as above but use FOR-EACH
    int fieldMethod2(final Field field, final String data) {
        final char[] chars;
        try {
            chars = (char[]) field.get(data);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        for (final char c : chars) {
            if (c <= ' ') {
                doThrow();
            }
        }
        return chars.length;
    }

    /**
     *
     * Make a list of tests. We will shuffle a copy of this list repeatedly
     * while we repeat this test.
     *
     * @param data
     * @return
     */
    List<Jobber> makeTests(String data) throws Exception {
        // make a list of tests
        final List<Jobber> tests = new ArrayList<Jobber>();

        tests.add(new Jobber("charAt1") {
            int check() {
                return charAtMethod1(data);
            }
        });

        tests.add(new Jobber("charAt2") {
            int check() {
                return charAtMethod2(data);
            }
        });

        tests.add(new Jobber("stream") {
            final IntPredicate predicate = new IntPredicate() {
                public boolean test(int value) {
                    return value <= ' ';
                }
            };

            int check() {
                return streamMethod(data, predicate);
            }
        });

        tests.add(new Jobber("streamPar") {
            final IntPredicate predicate = new IntPredicate() {
                public boolean test(int value) {
                    return value <= ' ';
                }
            };

            int check() {
                return streamParallelMethod(data, predicate);
            }
        });

        // Reusable char[] method
        tests.add(new Jobber("reuse") {
            final char[] cbuff = new char[MAX_STRING_SIZE];

            int check() {
                return reuseBuffMethod(cbuff, data);
            }
        });

        // New char[] from String
        tests.add(new Jobber("new1") {
            int check() {
                return newMethod1(data);
            }
        });

        // New char[] from String
        tests.add(new Jobber("new2") {
            int check() {
                return newMethod2(data);
            }
        });

        // Use reflection for field access
        tests.add(new Jobber("field1") {
            final Field field;

            {
                field = String.class.getDeclaredField("value");
                field.setAccessible(true);
            }

            int check() {
                return fieldMethod1(field, data);
            }
        });

        // Use reflection for field access
        tests.add(new Jobber("field2") {
            final Field field;

            {
                field = String.class.getDeclaredField("value");
                field.setAccessible(true);
            }

            int check() {
                return fieldMethod2(field, data);
            }
        });

        return tests;
    }

    /**
     * We use this class to keep track of test results
     */
    abstract class Jobber {

        final String name;
        long nanos;
        long chars;
        long runs;

        Jobber(String name) {
            this.name = name;
        }

        abstract int check();

        final double nanosPerChar() {
            double charsPerRun = chars / runs;
            long nanosPerRun = nanos / runs;
            return charsPerRun == 0 ? nanosPerRun : nanosPerRun / charsPerRun;
        }

        final void run() {
            runs++;
            long time = System.nanoTime();
            chars += check();
            nanos += System.nanoTime() - time;
        }
    }

    // MAKE A TEST STRING OF RANDOM CHARACTERS A-Z
    private String makeTestString(int testSize, char start, char end) {
        Random r = new Random();
        char[] data = new char[testSize];
        for (int i = 0; i < data.length; i++) {
            data[i] = (char) (start + r.nextInt(end));
        }
        return new String(data);
    }

    // WE DO THIS IF WE FIND AN ILLEGAL CHARACTER IN THE STRING
    public void doThrow() {
        throw new RuntimeException("Bzzzt -- Illegal Character!!");
    }

    /**
     * 1. get random string of correct length 2. get tests (List<Jobber>) 3.
     * perform tests repeatedly, shuffling each time
     */
    List<Jobber> test(int size, int tries, Random random) throws Exception {
        String data = makeTestString(size, 'A', 'Z');
        List<Jobber> tests = makeTests(data);
        List<Jobber> copy = new ArrayList<>(tests);
        while (tries-- > 0) {
            Collections.shuffle(copy, random);
            for (Jobber ti : copy) {
                ti.run();
            }
        }
        // check to make sure all char counts the same
        long runs = tests.get(0).runs;
        long count = tests.get(0).chars;
        for (Jobber ti : tests) {
            if (ti.runs != runs && ti.chars != count) {
                throw new Exception("Char counts should match if all correct algorithms");
            }
        }
        return tests;
    }

    private void printHeadings(final int TRIES_PER_STRING_SIZE, final Random random) throws Exception {
        System.out.print("  Size");
        for (Jobber ti : test(0, TRIES_PER_STRING_SIZE, random)) {
            System.out.printf("%9s", ti.name);
        }
        System.out.println("");
    }

    private void reportResults(int size, List<Jobber> tests) {
        System.out.printf("%6d", size);
        for (Jobber ti : tests) {
            System.out.printf("%,9.2f", ti.nanosPerChar());
        }
        System.out.println("");
    }
}

1
このテストはサーバーJVMまたはクライアントJVMで実行されましたか?最適な最適化はサーバーJVMでのみ行われます。デフォルトの32ビットJVMを使用して引数なしで実行した場合は、クライアントモードで実行しました。
ceklock 2013年

2
部分文字列、またはString(char []、int、int)を使用して作成された文字列の場合、バッファ全体を取得するため(少なくともAndroidでは)、バッキングバッファの取得は問題になりますが、インデックスはゼロベースになります。ただし、部分文字列がないことがわかっている場合は、正常に機能します。
事前に2014

5
「for(int i = 0; i <data.length(); i ++)」がdata.length()を最終的なローカル変数として定義するよりも速い理由は何ですか?
スカイイン2014年

2
変数を定義するには、メソッドバイトコードでスタック操作を行う必要があります。しかし、最適化は、アルゴリズムの認識から、変数の割り当てによるオーバーヘッドなしに、実際のマシンコードでその繰り返し操作を高速に追跡できます。このような最適化は、バイトコードコンパイラに存在する場合と存在しない場合があります。それはすべて、jvmが十分にスマートであるかどうかに依存します:-)
コーディネーター

2
@DavidS数値は、検査される文字あたりの速度(ナノ秒単位)です。小さいほど良い。
コーディネーター

14

これは、あなたが心配してはいけない、単なるマイクロ最適化です。

char[] chars = str.toCharArray();

str文字配列のコピーを返します(JDKではを呼び出して文字のコピーを返しますSystem.arrayCopy)。

それ以外は、str.charAt()インデックスが本当に境界内にあるかどうかをチェックし、配列インデックス内の文字を返します。

最初のものは、JVMに追加のメモリを作成しません。


質問には答えません。この質問はパフォーマンスについてです。ご存じのとおり、OPはストリングの反復がアプリケーションの主要なコストであることを発見した可能性があります。
rghome

9

ちょうど好奇心のために、そしてセントヒルの答えと比較するために。

重いデータを処理する必要がある場合は、クライアントモードでJVMを使用しないでください。クライアントモードは最適化のために作成されていません。

クライアントモードとサーバーモードでJVMを使用した@Saint Hillベンチマークの結果を比較してみましょう。

Core2Quad Q6600 G0 @ 2.4GHz
JavaSE 1.7.0_40

「java -server」と「java -client」の実際の違いも参照してください


クライアントモード:

len =      2:    111k charAt(i),  105k cbuff[i],   62k new[i],   17k field access.   (chars/ms) 
len =      4:    285k charAt(i),  166k cbuff[i],  114k new[i],   43k field access.   (chars/ms) 
len =      6:    315k charAt(i),  230k cbuff[i],  162k new[i],   69k field access.   (chars/ms) 
len =      8:    333k charAt(i),  275k cbuff[i],  181k new[i],   85k field access.   (chars/ms) 
len =     12:    342k charAt(i),  342k cbuff[i],  222k new[i],  117k field access.   (chars/ms) 
len =     16:    363k charAt(i),  347k cbuff[i],  275k new[i],  152k field access.   (chars/ms) 
len =     20:    363k charAt(i),  392k cbuff[i],  289k new[i],  180k field access.   (chars/ms) 
len =     24:    375k charAt(i),  428k cbuff[i],  311k new[i],  205k field access.   (chars/ms) 
len =     28:    378k charAt(i),  474k cbuff[i],  341k new[i],  233k field access.   (chars/ms) 
len =     32:    376k charAt(i),  492k cbuff[i],  340k new[i],  251k field access.   (chars/ms) 
len =     64:    374k charAt(i),  551k cbuff[i],  374k new[i],  367k field access.   (chars/ms) 
len =    128:    385k charAt(i),  624k cbuff[i],  415k new[i],  509k field access.   (chars/ms) 
len =    256:    390k charAt(i),  675k cbuff[i],  436k new[i],  619k field access.   (chars/ms) 
len =    512:    394k charAt(i),  703k cbuff[i],  439k new[i],  695k field access.   (chars/ms) 
len =   1024:    395k charAt(i),  718k cbuff[i],  462k new[i],  742k field access.   (chars/ms) 
len =   2048:    396k charAt(i),  725k cbuff[i],  471k new[i],  767k field access.   (chars/ms) 
len =   4096:    396k charAt(i),  727k cbuff[i],  459k new[i],  780k field access.   (chars/ms) 
len =   8192:    397k charAt(i),  712k cbuff[i],  446k new[i],  772k field access.   (chars/ms) 

サーバーモード:

len =      2:     86k charAt(i),   41k cbuff[i],   46k new[i],   80k field access.   (chars/ms) 
len =      4:    571k charAt(i),  250k cbuff[i],   97k new[i],  222k field access.   (chars/ms) 
len =      6:    666k charAt(i),  333k cbuff[i],  125k new[i],  315k field access.   (chars/ms) 
len =      8:    800k charAt(i),  400k cbuff[i],  181k new[i],  380k field access.   (chars/ms) 
len =     12:    800k charAt(i),  521k cbuff[i],  260k new[i],  545k field access.   (chars/ms) 
len =     16:    800k charAt(i),  592k cbuff[i],  296k new[i],  640k field access.   (chars/ms) 
len =     20:    800k charAt(i),  666k cbuff[i],  408k new[i],  800k field access.   (chars/ms) 
len =     24:    800k charAt(i),  705k cbuff[i],  452k new[i],  800k field access.   (chars/ms) 
len =     28:    777k charAt(i),  736k cbuff[i],  368k new[i],  933k field access.   (chars/ms) 
len =     32:    800k charAt(i),  780k cbuff[i],  571k new[i],  969k field access.   (chars/ms) 
len =     64:    800k charAt(i),  901k cbuff[i],  800k new[i],  1306k field access.   (chars/ms) 
len =    128:    1084k charAt(i),  888k cbuff[i],  633k new[i],  1620k field access.   (chars/ms) 
len =    256:    1122k charAt(i),  966k cbuff[i],  729k new[i],  1790k field access.   (chars/ms) 
len =    512:    1163k charAt(i),  1007k cbuff[i],  676k new[i],  1910k field access.   (chars/ms) 
len =   1024:    1179k charAt(i),  1027k cbuff[i],  698k new[i],  1954k field access.   (chars/ms) 
len =   2048:    1184k charAt(i),  1043k cbuff[i],  732k new[i],  2007k field access.   (chars/ms) 
len =   4096:    1188k charAt(i),  1049k cbuff[i],  742k new[i],  2031k field access.   (chars/ms) 
len =   8192:    1157k charAt(i),  1032k cbuff[i],  723k new[i],  2048k field access.   (chars/ms) 

結論:

ご覧のとおり、サーバーモードの方がはるかに高速です。


2
投稿いただきありがとうございます。したがって、大きな文字列の場合でも、フィールドアクセスはcharAt()よりも2倍高速です。実際、フィールドへのアクセスは全体としてさらに高速になり、28文字の文字列の後に続きます(クレイジー!!)ですから...サーバーモードではすべてが高速になります。とても興味深い!
コーディネーター

1
ええ、反射法は本当に速いです。面白い。
ceklock 2013年

2
ところで:新しいJVMが自動的に割り出し最高の(通常は)働く-serverまたは-clientのどの:docs.oracle.com/javase/7/docs/technotes/guides/vm/...
jontejj

2
@jontejjは実際にはそれほど単純ではありません。Windowsで32ビットJVMを実行している場合、JVMは常にデフォルトでクライアントになります。
ceklock

7

最初に使用str.charAtする方が速いはずです。

Stringクラスのソースコードを詳しく調べればcharAt、次のように実装されていることがわかります。

public char charAt(int index) {
    if ((index < 0) || (index >= count)) {
        throw new StringIndexOutOfBoundsException(index);
    }
    return value[index + offset];
}

ここでは、配列にインデックスを付けて値を返すだけです。

ここで、の実装を確認するtoCharArrayと、以下が見つかります。

public char[] toCharArray() {
    char result[] = new char[count];
    getChars(0, count, result, 0);
    return result;
}

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
    if (srcBegin < 0) {
        throw new StringIndexOutOfBoundsException(srcBegin);
    }
    if (srcEnd > count) {
        throw new StringIndexOutOfBoundsException(srcEnd);
    }
    if (srcBegin > srcEnd) {
        throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
    }
    System.arraycopy(value, offset + srcBegin, dst, dstBegin,
         srcEnd - srcBegin);
}

あなたが見るように、それはSystem.arraycopy確かにそれをやらないより少し遅くなるだろうことをやっています。


2
配列へのアクセス時にインデックスがチェックされるときに、String#charAtが追加のインデックスチェックを実行するのはばかげています。
インゴ

1
8年前のスレッドを復活させるリスクがある...文字列の後ろのchar配列は、文字列自体よりも大きい場合があります。つまり、文字列「abcde」があり、部分文字列を使用して「bcd」を新しい文字列に抽出した場合、新しい文字列は最初の文字列とまったく同じchar配列によってバッキングされます。これが、文字列クラスがオフセットとカウントを維持する理由です。そのため、配列内のどの文字がこの文字列を表すかがわかります。したがって、範囲チェックは重要です。それ以外の場合は、この文字列の末尾を超えて文字にアクセスすることが可能です。
DTY

3

@Saint Hillの答えにもかかわらず、str.toCharArray()の時間の複雑さを考慮すると、

最初のものは、非常に大きな文字列でも高速です。以下のコードを実行して、自分で確認できます。

        char [] ch = new char[1_000_000_00];
    String str = new String(ch); // to create a large string

    // ---> from here
    long currentTime = System.nanoTime();
    for (int i = 0, n = str.length(); i < n; i++) {
        char c = str.charAt(i);
    }
    // ---> to here
    System.out.println("str.charAt(i):"+(System.nanoTime()-currentTime)/1000000.0 +" (ms)");

    /**
     *   ch = str.toCharArray() itself takes lots of time   
     */
    // ---> from here
    currentTime = System.nanoTime();
    ch = str.toCharArray();
    for (int i = 0, n = str.length(); i < n; i++) {
        char c = ch[i];
    }
    // ---> to  here
    System.out.println("ch = str.toCharArray() + c = ch[i] :"+(System.nanoTime()-currentTime)/1000000.0 +" (ms)");

出力:

str.charAt(i):5.492102 (ms)
ch = str.toCharArray() + c = ch[i] :79.400064 (ms)

2

nietherの方が速いまたは遅いようです

    public static void main(String arguments[]) {


        //Build a long string
        StringBuilder sb = new StringBuilder();
        for(int j = 0; j < 10000; j++) {
            sb.append("a really, really long string");
        }
        String str = sb.toString();
        for (int testscount = 0; testscount < 10; testscount ++) {


            //Test 1
            long start = System.currentTimeMillis();
            for(int c = 0; c < 10000000; c++) {
                for (int i = 0, n = str.length(); i < n; i++) {
                    char chr = str.charAt(i);
                    doSomethingWithChar(chr);//To trick JIT optimistaion
                }
            }

            System.out.println("1: " + (System.currentTimeMillis() - start));

            //Test 2
            start = System.currentTimeMillis();
            char[] chars = str.toCharArray();
            for(int c = 0; c < 10000000; c++) {
                for (int i = 0, n = chars.length; i < n; i++) {
                    char chr = chars[i];
                    doSomethingWithChar(chr);//To trick JIT optimistaion
                }
            }
            System.out.println("2: " + (System.currentTimeMillis() - start));
            System.out.println();
        }


    }


    public static void doSomethingWithChar(char chr) {
        int newInt = chr << 2;
    }

長い弦については、最初の弦を選択します。なぜ長い文字列をコピーするのですか?ドキュメンテーションは言う:

public char [] toCharArray()この文字列を新しい文字配列に変換します。

戻り値:この文字列の長さであり、その内容がこの文字列で表される文字シーケンスを含むように初期化される、新しく割り当てられた文字配列。

//編集1

JIT最適化をだますためにテストを変更しました。

//編集2

テストを10回繰り返して、JVMをウォームアップします。

//編集3

結論:

最初にstr.toCharArray();、文字列全体をメモリにコピーします。長い文字列ではメモリを消費する可能性があります。メソッドString.charAt( )は、以前にインデックスをチェックしているStringクラス内のchar配列でcharを検索します。十分に短い文字列の場合、最初のメソッド(つまりchatAtメソッド)は、このインデックスチェックのために少し遅いようです。ただし、文字列が十分に長い場合、char配列全体のコピーは遅くなり、最初の方法の方が高速です。文字列が長いほど、toCharArrayパフォーマンスが低下します。for(int j = 0; j < 10000; j++)ループの制限を変更して確認してください。JVMをウォームアップすると、コードはより速く実行されますが、比率は同じです。

結局のところ、それは単なるミクロ最適化です。


for:inただの楽しみのために、オプションを試していただけませんか?
dasblinkenlight 2012年

2
ベンチマークに欠陥があります。JITに最適化を行わせません。JITは何もしないため、ループを完全に削除できます。
JBニゼット2012年

文字列はna Iterableでも配列でもありません。
Piotr Gwiazda 2012年

2
これは有効なテストではありません。テスト1でJVMを「ウォームアップ」しているため、結果がテスト2に有利になる可能性があります。とにかく、OPの質問全体は、マイクロ最適化の匂いがします。
認識

1
そうだね。ウォームアップ後(編集2を参照)、両方の時間はまだ小さいbuはまだ近くにあります。私の例では、2番目のテストは少し高速です。しかし、文字列を長くすると、最初の文字列の方が速くなります。文字列が長いほど、char配列のコピーのため、2番目のテストは遅くなります。最初の方法でやってください。
Piotr Gwiazda 2012年

2

String.toCharArray()新しい文字配列を作成し、文字列長のメモリの割り当てを意味しSystem.arraycopy()ます。次に、使用して文字列の元の文字配列をコピーし、このコピーを呼び出し元に返します。String.charAt()はi、元のコピーの位置にある文字を返します。そのため、String.charAt()よりも高速になりString.toCharArray()ます。ただし、String.toCharArray()元の文字配列から文字ではなくコピーをString.charAt()返し、元の文字配列から文字を返します。以下のコードは、この文字列の指定されたインデックスの値を返します。

public char charAt(int index) {
    if ((index < 0) || (index >= value.length)) {
        throw new StringIndexOutOfBoundsException(index);
    }
    return value[index];
}

以下のコードは、長さがこの文字列の長さである新しく割り当てられた文字配列を返します

public char[] toCharArray() {
    // Cannot use Arrays.copyOf because of class initialization order issues
    char result[] = new char[value.length];
    System.arraycopy(value, 0, result, 0, value.length);
    return result;
}

1

2つ目は新しいchar配列を作成し、Stringからのすべての文字をこの新しいchar配列にコピーするため、最初の方が高速です(メモリの消費量が少ない)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.