Java パフォーマンスの最適化 - クエリ、追加、削除時のテスト配列とリンク リストのパフォーマンス比較

シーン

パフォーマンスのテストと最適化には、Java で JMH (Java Microbenchmark Harness マイクロベンチマーク テスト フレームワーク) を使用します。

Java で JMH (Java Microbenchmark Harness マイクロベンチマーク テスト フレームワーク) を使用してパフォーマンス テストと最適化を行う - プログラマーが求めた

上記の JMH を使用する場合、ヘッダーを挿入するときに Java で配列とリンク リストの比較結果をテストしました。

以下は、追加と削除のプロセスが似ているため、先頭、中間、末尾でそれぞれクエリと追加/削除のパフォーマンスを比較しています。

したがって、加算をテストするだけです。

Java の配列とリンク リスト

配列

配列 (Array) は、同じタイプの要素のコレクションで構成されるデータ構造であり、ストレージ用に連続したメモリを割り当てます。

要素に対応する格納アドレスは、要素のインデックスを使用して計算できます。

アレイの「連続」機能は、アレイが継続的に格納されるため、アクセス速度が非常に速いと判断し、その格納場所が固定されていると判断します。

そのため、アクセス速度が非常に速いです。

欠点は、比較的高いメモリ要件があり、連続したメモリの一部を見つける必要があることです。

配列のもう 1 つの欠点は、挿入と削除の効率が比較的遅いことです。配列の末尾以外のデータを挿入または削除すると、

後ですべてのデータを移動すると、一定のパフォーマンスのオーバーヘッドが発生し、配列のサイズが固定されており、動的に拡張できないという欠点もあります。

リンクされたリスト

リンク リスト (Linked list) は一般的な基本的なデータ構造であり、線形リストではありますが、データが線形順序で格納されるわけではありません。

代わりに、次のノードへのポインタ (Pointer) が各ノードに格納されます。順番に保存する必要がないので、リンクリストを挿入することができます

O(1) の複雑さを実現するには、別の線形リスト シーケンス テーブルよりもはるかに高速ですが、ノードの検索や特定の番号を持つノードへのアクセスははるかに高速です。

これには O(n) 時間がかかりますが、順次テーブルの対応する時間計算量はそれぞれ O(logn) と O(1) です。

リンク リストは、連続的なメモリ ストレージを必要としないデータ構造です。リンク リストの要素には、要素の値とポインタの 2 つの属性があります。

このポインタは、次の要素のアドレスをマークします。

リンク リストは主に次のカテゴリに分類されます。

単一リンクリスト

単一リンクリストには、情報フィールドとポインターフィールドの 2 つのフィールドが含まれます。このリンクはリスト内の次のノードを指し、最後のノードは null 値を指します。

二重リンクリスト

二重連結リストは二重連結リストとも呼ばれ、二重連結リストには次のノードへのポインタだけでなく、前のノードへのポインタも存在します。

任意のノードから前のノードにアクセスすることはもちろん、次のノードにアクセスしたり、リンク リスト全体にアクセスしたりすることもできます。

循環リンクリスト

循環リンク リストの最初のノードが最後のノードとなり、その逆も同様です。

単一リンクリストと二重リンクリストがあるのはなぜですか?

これは連結リストの削除から始まりますが、一方向連結リストで要素を削除する必要がある場合、削除されたノードを見つけるだけでなく、削除されたノードも見つける必要があります。

ポイントの前のノード (通常は先行ノードと呼ばれます)。前のノードの次のポインタを変更する必要があるだけでなく、一方向のリンク リストであるため、

したがって、削除されたノードには前のノードの関連情報が保存されないため、前のノードを見つけるためにリンク リストを再度クエリする必要があります。

これによりパフォーマンス上の問題が発生するため、二重にリンクされたリストが存在します。

リンクリストの利点:

1. リンクリストのメモリ使用率は比較的高く、連続したメモリ空間は必要なく、メモリの断片化があってもリンクリストの作成には影響しません。

2. リンクリストの挿入と削除の速度は非常に速く、配列のように多数の要素を移動する必要はありません。

3. リンクされたリストのサイズは固定されておらず、動的に簡単に拡張できます。

リンクリストの欠点

リンク リストの主な欠点は、ランダムに検索できないこと、最初のリストからたどる必要があること、検索効率が比較的低いこと、リンク リスト クエリの時間計算量が O(n) であることです。

Java言語では配列の代表がArrayList、リンクリストの代表がLinkedListなので、テストにはこの2つのオブジェクトを使います。

ノート:

ブログ:
横柄なローグ気質 blog_CSDN ブログ - C#、アーキテクチャ ロード、SpringBoot のブロガー

達成

1. テストヘッドを追加する

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2,time = 1,timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5,time = 5,timeUnit = TimeUnit.SECONDS)
@Fork(1)
@State(Scope.Thread)
//头部添加性能测试
public class ArrayListWithLinkedListTestHeaderAdd {

    private static final int maxSize = 10000; //测试循环次数
    private static final int operationSize = 100; //操作次数

    private static ArrayList<Integer> arrayList;
    private static LinkedList<Integer> linkedList;

    public static void main(String[] args) throws RunnerException {
        //启动基准测试
        Options opt = new OptionsBuilder()
                .include(ArrayListWithLinkedListTestHeaderAdd.class.getSimpleName())  //要导入的测试类
                .build();
        //执行测试
        new Runner(opt).run();
    }

    //@Setup作用于方法上,用于测试前的初始化工作
    //启动执行事件
    @Setup
    public void init(){
        arrayList = new ArrayList<Integer>();
        linkedList = new LinkedList<Integer>();
        for (int i = 0; i < maxSize; i++) {
            arrayList.add(i);
            linkedList.add(i);
        }
    }

    //用于回收某些资源
    @TearDown
    public void finish(){

    }

    @Benchmark
    public void addArrayByFirst(Blackhole blackhole){
        for (int i = 0; i < operationSize; i++) {
            arrayList.add(i,i);
        }
        //为了避免JIT忽略未被使用的结果计算/为了避免死码消除问题
        blackhole.consume(arrayList);
    }

    @Benchmark
    public void addLinkedByFirst(Blackhole blackhole){
        for (int i = 0; i < operationSize; i++) {
            linkedList.add(i,i);
        }
        //为了避免JIT忽略未被使用的结果计算/为了避免死码消除问题
        blackhole.consume(linkedList);
    }
}

テスト結果の出力は次のとおりです。

Benchmark                                              Mode  Cnt        Score         Error  Units
ArrayListWithLinkedListTestHeaderAdd.addArrayByFirst   avgt    5  6597432.461 ± 8384921.207  ns/op
ArrayListWithLinkedListTestHeaderAdd.addLinkedByFirst  avgt    5   111880.375 ±  258245.983  ns/op

ヘッド挿入時の LinkedList の平均完了時間は、ArrayList の平均完了時間よりも約 60 倍高速であることがわかります。

2. テストの途中で追加する

    @Benchmark
    public void addArrayByMiddle(Blackhole blackhole){
        int startIndex = maxSize /2 ;//获取中间值
        for (int i = startIndex; i < (startIndex + operationSize); i++) {
            arrayList.add(i,i);
        }
        //为了避免JIT忽略未被使用的结果计算/为了避免死码消除问题
        blackhole.consume(arrayList);
    }

    @Benchmark
    public void addLinkedByMiddle(Blackhole blackhole){
        int startIndex = maxSize /2 ;//获取中间值
        for (int i = startIndex; i < (startIndex + operationSize); i++) {
            linkedList.add(i,i);
        }
        //为了避免JIT忽略未被使用的结果计算/为了避免死码消除问题
        blackhole.consume(linkedList);
    }

試験結果

Benchmark                                              Mode  Cnt        Score         Error  Units
ArrayListWithLinkedListTestMiddleAdd.addArrayByMiddle   avgt    5  6703786.087 ± 9225973.854  ns/op
ArrayListWithLinkedListTestMiddleAdd.addLinkedByMiddle  avgt    5  1064823.250 ±  697306.502  ns/op

LinkedList の平均完了時間は、中間挿入時の ArrayList の平均完了時間よりも約 7 倍速いことがわかります。

3. テストの最後に追加します

    @Benchmark
    public void addArrayByTail(Blackhole blackhole){
        int startIndex = maxSize - 1 - operationSize ;
        for (int i = startIndex; i < (maxSize - 1); i++) {
            arrayList.add(i,i);
        }
        //为了避免JIT忽略未被使用的结果计算/为了避免死码消除问题
        blackhole.consume(arrayList);
    }

    @Benchmark
    public void addLinkedByTail(Blackhole blackhole){
        int startIndex = maxSize - 1 - operationSize ;
        for (int i = startIndex; i < (maxSize - 1); i++) {
            linkedList.add(i,i);
        }
        //为了避免JIT忽略未被使用的结果计算/为了避免死码消除问题
        blackhole.consume(linkedList);
    }

試験結果

Benchmark                                            Mode  Cnt        Score          Error  Units
ArrayListWithLinkedListTestTailAdd.addArrayByTail   avgt    5  6981011.770 ± 10173306.725  ns/op
ArrayListWithLinkedListTestTailAdd.addLinkedByTail  avgt    5  2180224.087 ±   913796.230  ns/op

末尾挿入を実行した場合、LinkedList の平均完了時間は ArrayList の平均完了時間よりも約 3 倍高速であることがわかります。

4. ヘッダークエリのテスト

    @Benchmark
    public void searchArrayByFirst(){
        for (int i = 0; i < operationSize; i++) {
            arrayList.get(i);
        }
    }

    @Benchmark
    public void searchLinkedByFirst(){
        for (int i = 0; i < operationSize; i++) {
            linkedList.get(i);
        }
    }

試験結果

Benchmark                                                    Mode  Cnt     Score      Error  Units
ArrayListWithLinkedListTestHeaderSearch.searchArrayByFirst   avgt    5     3.578 ±    0.835  ns/op
ArrayListWithLinkedListTestHeaderSearch.searchLinkedByFirst  avgt    5  4232.832 ± 2019.900  ns/op

5. テストの途中でクエリを実行する

    @Benchmark
    public void searchArrayByMiddle(){
        int startIndex = maxSize / 2;
        int endIndex = startIndex + operationSize;
        for (int i = startIndex; i < endIndex; i++) {
            arrayList.get(i);
        }
    }

    @Benchmark
    public void searchLinkedByMiddle(){
        int startIndex = maxSize / 2;
        int endIndex = startIndex + operationSize;
        for (int i = startIndex; i < endIndex; i++) {
            linkedList.get(i);
        }
    }

試験結果

Benchmark                                                     Mode  Cnt        Score        Error  Units
ArrayListWithLinkedListTestMiddleSearch.searchArrayByMiddle   avgt    5        4.969 ±      6.568  ns/op
ArrayListWithLinkedListTestMiddleSearch.searchLinkedByMiddle  avgt    5  1131641.772 ± 313408.389  ns/op

中央クエリを実行する場合、ArrayList の平均完了時間は LinkedList の平均完了時間よりも約 230947 倍速いことがわかります。

6. 末尾クエリのテスト

    @Benchmark
    public void searchArrayByTail(){
        for (int i = maxSize - operationSize; i < maxSize; i++) {
            arrayList.get(i);
        }
    }

    @Benchmark
    public void searchLinkedByTail(){
        for (int i = maxSize - operationSize; i < maxSize; i++) {
            linkedList.get(i);
        }
    }

試験結果

Benchmark                                                 Mode  Cnt     Score     Error  Units
ArrayListWithLinkedListTestTailSearch.searchArrayByTail   avgt    5     5.074 ±   5.384  ns/op
ArrayListWithLinkedListTestTailSearch.searchLinkedByTail  avgt    5  5689.329 ± 563.806  ns/op

末尾クエリを実行する場合、ArrayList の平均完了時間は LinkedList の平均完了時間よりも約 1100 倍速いことがわかります。

7. 要約: Java のリンク リストは配列よりも 1000 倍以上遅いですか?

ヘッド クエリを実行する場合、ArrayList の平均完了時間は LinkedList の平均完了時間よりも約 1410 倍高速であることがわかります。

最終評価から、データの初期化が完了した後、挿入操作を実行すると、特に先頭から挿入する場合、

配列はその後のすべての要素を移動する必要があるため、パフォーマンスはリンク リストよりも大幅に低下しますが、クエリの場合は、リンク リストがクエリを走査する必要があるため、パフォーマンスはまったく逆になります。

また、LinkedList は二重リンク リストであるため、中間クエリのパフォーマンスは配列クエリ (クエリ 100 要素) のパフォーマンスよりも数万倍遅くなります。

両端 (先頭と末尾) でクエリを実行する場合、リンク リストは配列 (クエリ 100 要素) よりも 1000 倍近く遅くなります。

したがって、クエリが多数あるシナリオでは配列を使用し、追加および削除操作が多数ある場合はリンク リスト構造を使用する必要があります。

おすすめ

転載: blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/131750105
おすすめ