シーン
パフォーマンスのテストと最適化には、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 倍近く遅くなります。