mcpField
P
performance-smell-detection

performance-smell-detection

github.com/decebalsテスト・品質761318 回インストール7 週間前に更新
リポジトリ

performance-smell-detection について

Performance Smell Detectionは、コードレビューや測定済みの性能問題の調査を行うJava開発者向けのスキルです。コードレベルの潜在的なボトルネックを示し、早まった最適化より測定と可読性を重視します。

主な機能

  1. 01Javaの性能パターンのレビュー
  2. 02コードレベルの潜在的なボトルネック検出
  3. 03文字列、ストリーム、コレクションの分析
  4. 04ボクシングと正規表現のコストの解説
  5. 05最適化前のプロファイリング重視

活用シーン

  1. 01性能が重要なJavaコードのレビュー
  2. 02測定済みのJava性能低下の調査
  3. 03Javaの性能上のトレードオフの学習

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/decebals/claude-code-java --skill performance-smell-detection

インストール後、Claude Code で/performance-smell-detectionを入力して読み込まれたか確認します。

同梱ファイルREADME.md

詳細

パフォーマンス上の懸念点を検出するスキル

Java コードにおける潜在的なコードレベルのパフォーマンス問題を特定します。

基本方針

「早すぎる最適化は諸悪の根源である」 - Donald Knuth

このスキルは、潜在的なパフォーマンス上の懸念点に気づくためのものであり、むやみに「修正」するためのものではありません。最新の JVM(Java 21/25)は高度に最適化されています。常に次の点を意識してください。

  1. まず計測する - JMH、プロファイラー、本番環境のメトリクスを使用します
  2. ホットパスに注目する - 処理時間の 90% はコードの 10% で費やされます
  3. 可読性を考慮する - マイクロ最適化よりも、わかりやすいコードのほうが重要な場合がよくあります

使用する場面

  • パフォーマンスが重要なコードパスをレビューするとき
  • 計測で確認されたパフォーマンス問題を調査するとき
  • Java のパフォーマンスパターンについて学ぶとき
  • パフォーマンスを意識したコードレビューを行うとき

対象範囲

このスキル: コードレベルのパフォーマンス(ストリーム、コレクション、オブジェクト) データベースについて: jpa-patterns スキルを使用します(N+1、遅延読み込み、ページネーション) アーキテクチャについて: architecture-review スキルを使用します


クイックリファレンス:潜在的な懸念点

懸念点重大度状況
ループ内での正規表現のコンパイル🔴 高常に修正する価値があります
ループ内での文字列連結🟡 中Java 21/25 でも依然として有効です
タイトなループ内でのストリーム🟡 中コレクションのサイズによります
ホットパスでのボクシング🟡 中まず計測します
上限のないコレクション🔴 高メモリのリスクがあります
コレクションの容量未指定🟢 低軽微です。重要な場合は計測します

文字列操作(Java 9+ / 21 / 25)

変更点

Java 9(JEP 280)以降、+ を使った文字列連結では StringBuilder ではなく invokedynamic が使用されます。JVM は単純な連結を適切に最適化します。

Java 25 では、String キーを使った Map の検索をさらに最適化するため、String::hashCode の定数畳み込みが追加されています。

引き続き有効:ループ内での StringBuilder
// 🔴 Still problematic - new String each iteration
String result = "";
for (String s : items) {
    result += s;  // O(n²) - creates n strings
}

// ✅ StringBuilder for loops
StringBuilder sb = new StringBuilder();
for (String s : items) {
    sb.append(s);
}
String result = sb.toString();

// ✅ Or use String.join / Collectors.joining
String result = String.join("", items);
現在は問題なし:単純な連結
// ✅ Fine in Java 9+ - JVM optimizes this
String message = "User " + name + " logged in at " + timestamp;

// ✅ Also fine
return "Error: " + code + " - " + description;
ホットパスでは避ける:String.format
// 🟡 String.format has parsing overhead
log.debug(String.format("Processing %s with id %d", name, id));

// ✅ Parameterized logging (SLF4J)
log.debug("Processing {} with id {}", name, id);

Stream API(状況に応じた見方)

実情

ストリームにはオーバーヘッドがありますが、多くの場合許容範囲です。

  • 100 件未満:ストリームは 2~5 倍遅くなることがあります(ただし、それでもマイクロ秒単位です)
  • 1K~10K 件:差は大幅に縮まります
  • 10K 件超:ループとの差が 50% 以内に収まることがよくあります
  • GraalVM:ストリームをループと同等の性能になるよう最適化できます

推奨事項:可読性を優先してストリームを使用します。プロファイリングでボトルネックが確認された場合にのみ、ループへの最適化を検討してください。

ストリームに問題がある場合
// 🔴 Stream created per iteration in hot loop
for (int i = 0; i < 1_000_000; i++) {
    boolean found = items.stream()
        .anyMatch(item -> item.getId() == i);
}

// ✅ Pre-compute lookup structure
Set<Integer> itemIds = items.stream()
    .map(Item::getId)
    .collect(Collectors.toSet());

for (int i = 0; i < 1_000_000; i++) {
    boolean found = itemIds.contains(i);
}
ストリームを問題なく使える場合
// ✅ Single pass, readable, not in tight loop
List<String> names = users.stream()
    .filter(User::isActive)
    .map(User::getName)
    .sorted()
    .collect(Collectors.toList());

// ✅ Primitive streams avoid boxing
int sum = numbers.stream()
    .mapToInt(Integer::intValue)
    .sum();
並列ストリーム:慎重に使用する
// 🔴 Parallel on small collection - overhead > benefit
smallList.parallelStream().map(...);  // < 10K items

// 🔴 Parallel with shared mutable state
List<String> results = new ArrayList<>();
items.parallelStream()
    .forEach(results::add);  // Race condition!

// ✅ Parallel for CPU-intensive + large collections
List<Result> results = largeDataset.parallelStream()  // > 10K items
    .map(this::expensiveCpuComputation)
    .collect(Collectors.toList());

ボクシング/アンボクシング

現在も実際に問題となるもの

ボクシングはヒープ上にオブジェクトを作成し、GC の負荷を高めます。JVM は小さい値(-128~127)をキャッシュしますが、それより大きな値はキャッシュしません。

今後の予定:Project Valhalla により、この問題は大幅に改善されます。

// 🔴 Boxing in tight loop - creates millions of objects
Long sum = 0L;
for (int i = 0; i < 1_000_000; i++) {
    sum += i;  // Unbox, add, box
}

// ✅ Primitive
long sum = 0L;
for (int i = 0; i < 1_000_000; i++) {
    sum += i;
}
プリミティブストリームを使用する
// 🟡 Boxing overhead
int sum = list.stream()
    .reduce(0, Integer::sum);

// ✅ Primitive stream
int sum = list.stream()
    .mapToInt(Integer::intValue)
    .sum();

正規表現

ループ内では必ず事前コンパイルする

このアドバイスは古くなっていません。Pattern.compile はコストが高い処理です。

// 🔴 Compiles pattern every iteration
for (String input : inputs) {
    if (input.matches("\\d{3}-\\d{4}")) {  // Compiles regex!
        process(input);
    }
}

// ✅ Pre-compile
private static final Pattern PHONE = Pattern.compile("\\d{3}-\\d{4}");

for (String input : inputs) {
    if (PHONE.matcher(input).matches()) {
        process(input);
    }
}

コレクション

容量の指定(軽微な最適化)
// 🟢 Low severity - but free optimization if size known
List<User> users = new ArrayList<>(expectedSize);
Map<String, User> map = new HashMap<>(expectedSize * 4 / 3 + 1);
用途に合ったコレクションを選ぶ
// 🟡 O(n) lookup in loop
List<String> allowed = getAllowed();
for (Request r : requests) {
    if (allowed.contains(r.getId())) { }  // O(n) each time
}

// ✅ O(1) lookup
Set<String> allowed = new HashSet<>(getAllowed());
for (Request r : requests) {
    if (allowed.contains(r.getId())) { }  // O(1)
}
上限のないコレクション
// 🔴 Memory risk - could grow unbounded
@GetMapping("/users")
public List<User> getAllUsers() {
    return userRepository.findAll();  // Millions of rows?
}

// ✅ Pagination
@GetMapping("/users")
public Page<User> getUsers(Pageable pageable) {
    return userRepository.findAll(pageable);
}

最新の Java(21/25)パターン

I/O には仮想スレッドを使用する(Java 21+)
// 🟡 Traditional thread pool for I/O - wastes OS threads
ExecutorService executor = Executors.newFixedThreadPool(100);
for (Request request : requests) {
    executor.submit(() -> callExternalApi(request));  // Blocks OS thread
}

// ✅ Virtual threads - millions of concurrent I/O operations
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (Request request : requests) {
        executor.submit(() -> callExternalApi(request));
    }
}
構造化並行性(Java 21+ プレビュー)
// ✅ Structured concurrency for parallel I/O
try (StructuredTaskScope.ShutdownOnFailure scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<User> user = scope.fork(() -> fetchUser(id));
    Future<Orders> orders = scope.fork(() -> fetchOrders(id));

    scope.join();
    scope.throwIfFailed();

    return new UserProfile(user.resultNow(), orders.resultNow());
}

パフォーマンスレビューのチェックリスト

🔴 重大度:高(通常は修正する価値があります)
  • ループ内での正規表現の Pattern.compile
  • ページネーションなしの上限のないクエリ
  • ループ内での文字列連結(StringBuilder は引き続き有効です)
  • 共有された可変状態を使う並列ストリーム
🟡 重大度:中(まず計測します)
  • タイトなループ内でのストリーム(反復回数が 100K 超)
  • ホットパスでのボクシング
  • ループ内での List.contains()(Set を使用します)
  • I/O に従来のスレッドを使用する(仮想スレッドを検討します)
🟢 重大度:低(できれば対応する項目)
  • コレクションの初期容量
  • 軽微なストリームの最適化
  • toArray(new T[0]) と toArray(new T[size]) の比較

最適化すべきでない場合

  • ホットパスではない - セットアップコード、設定、管理エンドポイント
  • 問題が測定されていない - 「遅そう」は測定結果ではありません
  • 可読性が損なわれる - 明確なコードはマイクロ最適化よりも重要です
  • コレクションが小さい - 100 項目なら、いずれにせよマイクロ秒単位で処理できます

分析コマンド

# Find regex in loops (potential compile overhead)
grep -rn "\.matches(\|\.split(" --include="*.java"

# Find potential boxing (Long/Integer as variables)
grep -rn "Long\s\|Integer\s\|Double\s" --include="*.java" | grep "= 0\|+="

# Find ArrayList without capacity
grep -rn "new ArrayList<>()" --include="*.java"

# Find findAll without pagination
grep -rn "findAll()" --include="*.java"

コメント