ハクソク

世界を動かす技術を、日本語で。

Javaは速いが、コードはそうとは限らない

概要

  • Javaパフォーマンス最適化シリーズ第1弾の要点解説
  • DevNexusで使用した注文処理アプリの改善事例
  • 主要なアンチパターン8つを実例で紹介
  • 最適化前後で5倍のスループット、87%のヒープ削減、GC停止79%減
  • コード変更はアーキテクチャ非依存、同一JDK・同一テストで検証

Javaパフォーマンス最適化事例:アンチパターン8選

  • DevNexus講演用に開発した注文処理Javaアプリでのパフォーマンス改善事例

    • 初期状態:1,198ms(経過時間)、85,000注文/秒、ヒープ1GB超、GC停止19回
    • 最適化後:239ms419,000注文/秒、ヒープ139MB、GC停止4回
    • 同一アプリ・同一テスト・同一JDK・アーキテクチャ変更なし
  • 本記事ではパフォーマンス低下を招く8つのアンチパターンを紹介

    • これらは実際のコードベースで頻出し、コードレビューやコンパイルでは見逃されやすい
    • プロファイリングデータがなければ気づきにくい
    • 次回記事でプロファイリング結果具体的な修正内容を解説予定

1. ループ内のString連結

  • String report = "";
    for (String line : logLines) { report = report + line + "\n"; }

    • Stringはイミュータブルなため、連結ごとに新規オブジェクト生成
    • ループごとにO(n²)の文字列コピーが発生し、大規模データで深刻な遅延
    • JMHベンチマークでnが4倍になると処理時間は7倍超に悪化
  • 修正例:
    StringBuilder sb = new StringBuilder();
    for (String line : logLines) { sb.append(line).append("\n"); }
    String report = sb.toString();

    • StringBuilderは1つの可変バッファでappendごとに追記、最後にtoString()
    • JDK9以降でもループ内連結は自動最適化されないため明示的にStringBuilderを用意

2. ループ内ストリームのO(n²)反復

  • for (Order order : orders) {
    int hour = ...;
    long countForHour = orders.stream().filter(...).count();
    ordersByHour.put(hour, countForHour);
    }

    • 全注文リストを各要素ごとにストリーム処理、10,000件で1億回比較
    • JFRプロファイルでCPUサンプルの71%を消費
  • 修正例:
    for (Order order : orders) {
    int hour = ...;
    ordersByHour.merge(hour, 1L, Long::sum);
    }

    • 一度のパスでカウント集計、O(n)で効率化
    • stream()のループ内使用は冗長処理のシグナル

3. ホットパスでのString.format()多用

  • return String.format("Order %s for %s: $%.2f", orderId, customer, amount);

    • String.format()は毎回フォーマット解析・正規表現処理で最も遅い
    • ベンチマークでStringBuilderが最速、format()が最遅
  • 修正例:
    return "Order " + orderId + " for " + customer + ": $" + String.format("%.2f", amount);

    • 数値整形のみformat()、他は連結でコンパイラ最適化
    • format()は設定読み込み・エラー表示等の低頻度用途に限定

4. ホットパスでのオートボクシング

  • Long sum = 0L;
    for (Long value : values) { sum += value; }

    • 毎回ボックス化/アンボックス化でLongオブジェクト大量生成
    • 1,000,000要素で16MBのヒープ消費
  • 修正例:
    long sum = 0L;
    for (long value : values) { sum += value; }

    • プリミティブ型利用でGC負荷を回避
    • List<Long>やMap<String, Integer>が頻繁に使われる場合は注意

5. 例外による制御フロー

  • public int parseOrDefault(String value, int defaultValue) {
    try { return Integer.parseInt(value); }
    catch (NumberFormatException e) { return defaultValue; }
    }

    • 頻繁な例外発生時にfillInStackTrace()でコールスタック全走査
    • 例外発生で数百倍遅くなることも
  • 修正例:
    public int parseOrDefault(String value, int defaultValue) {
    if (value == null || value.isBlank()) return defaultValue;
    for (int i = 0; i < value.length(); i++) {
    char c = value.charAt(i);
    if (i == 0 && c == '-') continue;
    if (!Character.isDigit(c)) return defaultValue;
    }
    try { return Integer.parseInt(value); }
    catch (NumberFormatException e) { return defaultValue; }
    }

    • 事前バリデーションで例外発生率を大幅低減
    • 例外は予期しない事象のみで利用、通常フローには使わない

6. 過度な同期化(synchronized)

  • public synchronized void increment(String key) { ... }

    • 全メソッドをロックし、スレッド間でボトルネック発生
  • 修正例:
    private final ConcurrentHashMap<String, LongAdder> counts = new ConcurrentHashMap<>();
    public void increment(String key) { counts.computeIfAbsent(key, k -> new LongAdder()).increment(); }

    • ConcurrentHashMapで細粒度ロック、LongAdderで高並列性
    • Collections.synchronizedMap()も全体ロック型なので非推奨

7. 再利用可能オブジェクトの都度生成

  • return new ObjectMapper().writeValueAsString(order);

    • ObjectMapperやDateTimeFormatter、Gson等の毎回生成は高コスト
    • 構築時にモジュール検出・キャッシュ初期化等の重い処理
  • 修正例:
    private static final ObjectMapper MAPPER = new ObjectMapper();
    public String serializeOrder(Order order) throws JsonProcessingException { return MAPPER.writeValueAsString(order); }

    • スレッドセーフなオブジェクトはstatic finalで共有
    • DateTimeFormatter.ISO_LOCAL_DATE等は組み込みシングルトン

8. Virtual Threadピニング(JDK 21–23)

  • synchronizedやブロッキングI/Oで仮想スレッドのキャリアスレッドがピン留め
    • 仮想スレッドの並列性低下、スケーラビリティ阻害

  • これらアンチパターンの修正で5倍のスループット、ヒープ87%削減、GC停止79%減を実現
  • 次回はプロファイリングデータ解析詳細な修正内容を紹介予定

Hackerたちの意見

最初のリクエストのレイテンシが、JavaではホットパスのコードがC2コンパイラを通るまで本当にひどいことになることがあるんだよね。起動時にそのコードを実行してホットパスを温めることはできるけど、それがめっちゃ面倒なんだよ。C++、Go、Rustを使えば、その問題を回避できるし、コードパスのウォームアップの手間も省ける。Javaにちゃんとしたコンパイラがあればいいのに。
JVMはそれをやってないの?GraalVMはどう?
C2が静的なAOTコンパイル、例えばLLVMに対してどれだけのメリットをもたらすのか、直接比較してみたいな。もし価値があるなら、状態をフリーズして再開できて、瞬時にワークロード最適化されたスタートアップができるのに驚くよ。
GraalVMを使えばネイティブ実行可能ファイルを作れるよ。もしJVMを維持したいなら、進行中のプロジェクトLeydenで、JVMのウォームアップの一部を「事前トレーニング」できるし、完全なAOTコードコンパイルは将来的に実現する予定だよ。
長時間実行するプロセスにはJavaを使う理由はこれなんだ。もし起動が早い小さなバイナリが大事なら、起動は速いけど実行時は遅いPythonみたいな別のものを使うよ。
Excelsior JETは今はなくなっちゃったけど、GraalVMとOpenJ9があるからね。組み込み系の人たちはPTCやAicasを使って遊んでるよ。AndroidはちゃんとしたJavaじゃないけど、dex2oatがあるからね。
最近のJDKがあればほとんど問題ないよ。Leydenはウォームアップをかなり短縮していて、今後もさらに改善される見込みだよ。 https://foojay.io/today/how-is-leyden-improving-java-perform... https://quarkus.io/blog/leyden-1/
最初のリクエストのレイテンシが言語の選択によってボトルネックになるって考えには異議あり。確かにそれはあり得ると思うけど、ほとんどの開発者にとってそれが問題になるのかな?
AOTは起動時間にはいいけど、プロダクションの長期的なパフォーマンス問題には逆のトレードオフがあるよね。動的プロファイルガイド最適化を使うJITもあって、実際のワークロードに合わせてランタイムでバイナリを調整できるんだ。普通のPGOみたいに事前にプロファイルを用意する必要はない。Javaはまだこれがない(私の知る限りでは)けど、.NETにはあって、大規模なウェブアプリケーションには大きなメリットだよね。
かなり前にJVMの開発に関わってた(もう20年近く前)。その頃のJavaの使い方は、長時間稼働するサーバーがほとんどだった。ランタイムチームは、できるだけ長い間AOTキャッシングの実装を拒否してたんだ。これはJavaにとって大きなチャンスを逃したことになる。クライアントの起動時間は常にひどかったからね。ここ3〜5年でようやく状況が変わり始めた気がするのは、Graalネイティブイメージの推進が一因だと思う。私はずっと前に、JVMのメンテナがクライアントやシステムプログラミング言語としてのJavaを優先していないと結論づけた。優先順位と言っていることに注意してほしい。彼らは非常に優秀なエンジニアで、異なるユースケースに焦点を当てているけど、クライアントエコシステムからはあまりお金が得られないんだ。
JITを駆使してスタートアップ時間を合わせるのは、Javaの「速さ」がプロダクショングラフに見えないアスタリスクを付けてる良いサインだよ。どこかでアプリじゃなくてランタイムを管理してるってこと。GraalVM Native ImageみたいなAOTオプションはコールドスタートにはかなり役立つけど、そうするとお気に入りのフレームワークの半分が壊れちゃうし、別の問題に悩まされることになる。どの痛みを選ぶかだね。
アルゴリズムの複雑さを理解すること(特にループ内の再作業を避けること)は、どの言語でも役立つし、賢いアドバイスだよ。ただ、実際にはほとんどのエンタープライズウェブサービスでは、外部サービス(データベースを含む)をどれだけ効率的に呼び出すかが、実際のパフォーマンスに大きく影響するんだ。クエリのループをバルク処理に変えるだけでも大きな助けになるし(それからインデックスをうまく使うようにクエリを調整したり、アップサートを行ったり、不必要なデータを削除したりすることも重要)。LLMの改善によって、ORMを使わずにSQLをうまく活用できるようになることを期待してるよ。
ここでの著者です。DBや外部サービスの呼び出しは、しばしば最大の効果をもたらしますね。指摘してくれてありがとう。私のデモアプリでは、CPUのホットスポットは完全にアプリケーションコードにあって、I/O待ちではなかったんです。フリート全体で見ても、CPUやヒープの「小さな」改善が実際のコストやスループットの違いに繋がるんです。問題は違うけど、あなたの指摘は正しいです。ここでの目標は、特にソフトウェアがスケールで動いているときに、他のパフォーマンスの側面についてもっと考えるようにみんなに促すことです。
これも間違えやすいよね。DBとのバランスが大事。1行や2行のクエリを1000回するのは明らかに非効率だけど、100万行のクエリを作るのもまた問題がある(必要な場合でも)。ハードウェアにも依存するけど、DBを使うときは、他のアプリケーションインスタンスがDBとやり取りできるようにすることが大事だよね。2行の挿入が100万行の読み込みで20秒もブロックされるなんて最悪だよ。データを結合するタイミングについても考えなきゃいけない。常に「DBに任せればいい」というわけではないからね。特に、メインテーブルが1000行引っ張ってきて、サブテーブルからは10のユニークな行しか引っ張ってこないような場合は、結合するよりも2つのクエリを作った方がいいこともある。もちろん、これらの幅にもよるけどね。でも、100%同意するよ。ORMはこれらの問題を扱う最悪の方法だと思う。ほとんどの場合、最初から正しいことをしないし、速くするためには結局は彼らが出力するSQLを理解しなきゃいけなくなるし、カスタムSQLを書く羽目になることもあるよね。
> アルゴリズムの複雑さを理解すること(特に、ループ内での再作業を避けること)は、どの言語でも役立つし、賢いアドバイスだね。最近、neovimで自分のためにtreesitterのパフォーマンス問題を修正したんだけど、ほとんどのテキストオブジェクトプラグインがやることとは違って、パースツリーをDFSで下っていく方法を使ったんだ。 > -> このメタデータに一致するすべてのサブツリーのためにツリー全体を歩く > -> これで一致するサブツリーのリストができるから、そのサブツリーのノードを反復処理して、カーソルに「近い」ものを探す。 > でも、neovimで「daf」と入力すると、普通はカーソルのすぐ下の関数を削除したいだけなんだ。だから、同じアルゴリズムを実装するのは、パースツリーをDFSで下って(ノードごとに行番号が埋め込まれている)一致を自分で検出するだけで済むよ。学校では競技プログラミングやTCSをやってた時、こういう改善はしばしば非常に賢い不変条件から生まれたもので、何時間、何日、何週間も考え込んでいたんだ。そしたら突然、もっと賢くやる方法に気づいて、問題全体が解決することがあった(そして、賢い人たちが賢いって褒めてくれる :D)。これはそういうのじゃなくて、「APIをバイパスして、速くやれ。でも、もしかしたらメンテナンス性は下がるかも」って感じだった。業界では、可読性やメンテナンス性とのトレードオフを管理することが多いよね。私は、nが大きくなっても、ダメなn^2パターンを使って、メンテナンス担当者に「テキスト範囲に対するクエリAPIを公開してくれ」って頼む方が楽しいよ(彼らはクエリだけで、ノードのテキスト範囲は別々に持ってると思う。私が調べた限りではね;最新情報は追えてないけど)。でも、今は自分だけの考慮を超えたことに結びついてるんだ。これが直感的でない形で状態を公開してるのか?コンポーザブルなプリミティブを追加してるのか、それともライブラリを速くするためにアドホックに機能を追加してるのか?などなど。以前は、こういうのを「余計なこと」だと思って、「なんで最高のアルゴリズムを書けないんだろう」と考えてたけど、今はシステムのメンテナンスをしている立場としては…いや、アーキテクチャデザインの方が時には楽しいよ!もうあまり賢いひらめきはないかもしれないけど、視野が広がった気がする(でも、その一部はGPT Proが私のお気に入りの競技プログラミングの問題を一発で解決し始めたからだと思う、2025年の終わり頃にね D:)。
> LLMの改善によってORMを捨てられることを期待してる。クエリを書くのが早くなるという名目で、間のマッピングコードも含めてね。代わりにSQLをうまく活用して、現代のデータベースが提供する力を引き出せるといいな。sqlalchemyのようなアクティブモデルは捨てられるかもしれないけど、ORMに付いてくる型付きクエリビルダーはもっと重要になると思う。コンパイラを使って悪いクエリをキャッチできるのは大きな利点だよ。
> ORMを捨てて... SQLをうまく活用するのがいいと思う。Java(または他のJVM言語)はjooqのおかげで一番適してると思う。今まで使った中で一番いいSQL生成ライブラリだし。
ORMはパフォーマンスを殺す存在だと思ってる。SQLを直接書いた方がいつも良い結果が出るよ。データオブジェクトとデータベースオブジェクトの間に一対一の対応が必要だなんて考えは、データストレージが簡単な場合を除いては悲惨だね。
> クエリのループをバルクに変換するだけで、かなり助けになるよ。これがスピードに文句を言ってる人を見たときに、最初に見るポイントなんだ。開発者は、ローカルマシンでデータベースに対して開発してるから、デプロイ環境にあるネットワーク遅延を無視しちゃうことが多いんだよね。
LLMが登場する前から、私はすでにORMを捨ててたよ。SQLを呼び出す便利な方法がないのが時々障害になるんだ。静的型付けの言語は、コンパイル時のクエリビルダーを使わない限り、結果の型を手動で設定しなきゃいけないけど、それはまた別の問題だし。あと、jsonbが存在する前は、テーブルに分けたくない大きなプロパティの塊にしばしば直面してた。今は、入れるべきでないものをjsonbに押し込まないようにするのがちょっとした規律が必要だね。
> 実際のパフォーマンスは、外部サービス(データベースを含む)をどれだけ効率的に呼び出すかにかかってるよね。それに加えて、過去20年の経験から言うと、メモリ割り当てのせいでパフォーマンスが落ちることが多い。特にGCがある言語(JavaやJavaScriptなど)ではね。ホットループでの割り当てを減らすと、10倍や100倍のランタイム改善につながるよ。
最近の数ヶ月、sqlxとPostgresを使ってClaudeとすごくうまくいってるよ。でも、仕事ではMySQLとNode.jsを生で使って1年以上経ってるし、その前はC++から生のSQLiteを使ってた(ポインタのせいでトラウマになった、もう二度とやりたくない)から…
私にとって大事なテーマなんだけど、最適化されたコードをたくさん書いてる。特にJavaでホットデータパイプラインを使ったりね。アルゴリズムの話を除けば、結局はメモリアロケーションを避けることに尽きる。私のお気に入りのゼロアロケのgRPCやParquet、JSON、時間ライブラリなんかを使ってて、すごく速くなるんだ。全体的にJavaがオブジェクトを使うことが多いから、遅くなっちゃうんだよね。でも、データフレームみたいなものでデータを保持するJVMアプリを作ると、J2EEのビーンからはかなり遠く感じるけど、最終的にはC/C++/Rustなどでしか超えられない限界にぶつかることになるよ。
> アルゴリズムの話を除けば、結局はメモリアロケーションを避けることに尽きる。HFTの人たちがマイクロ最適化が必要なワークロードにJavaを使ってるって聞いたことがあるけど、正直言って理解できなかった。見聞きした限りでは、コードを書くときに、ほとんどのサードパーティの依存関係と互換性がなくて、ぎこちなく見えるようにしなきゃいけないんだよね。それなら、なんでJavaを使ってるの?CやC++、他の人気のある言語を使った方がずっと適してると思うんだけど。Javaの最大の弱点はエコシステムで、実際にはそれを活用できないからね。
使ってるライブラリを教えてくれる?
自然に遅いコードを書くように導くプログラミング言語を使っていると、プログラマーだけを責めるわけにはいかないよね。誰かが、PoolAllocatorを使ってアロケーションを避けることでJavaで速いコードを書いていると言っているのを聞いたことがあるけど、結局は小さなオブジェクトを「キャッシュ」するための手動メモリ管理に余計なステップが加わるだけなんだよね。それなら、タスクに合ったもっと良い言語を使った方がいいんじゃない?
たいていの時は速度が重要じゃないアプリケーションもあるよね。たった一つか二つのプロセスだけがアロケーションなしのコードを必要とする場合、なんで他のコードにその複雑さを押し付ける必要があるの?別の言語を呼び出すのも、避けたい負担があるかもしれないし。プロジェクトがそういう要件に成長することもあるし、長い間問題じゃなかったことが突然問題になることも十分に想像できるよね。そうなったら、もう全コードベースを別の言語に移行したくないよね。
悪いアイデアだね。以前プールアロケーターを作ったことがあるけど、それは高価なネットワークオブジェクトやJNIを扱う高価なオブジェクトのためだった。メモリプレッシャーを避けるためにやるのは、単に調整が必要な悪いアルゴリズムがあるってことだよ。正しい解決策になることはほとんどないね。
正直、Javaという言語が誰かをショットガンみたいな使い方に導くとは思えないな。例えば、アルゴリズムの複雑さに関する知識は基礎的なもので、StringBuilderはジュニアレベルの基本知識だし。
この話題は、管理された言語と低レイテンシ開発が一緒に出てくるたびに必ず出てくるね。トレードオフは、必要ない時でも「速く」動かすことと、ほとんどの時間は遅く動かして、速くする必要がある時に調整することの間での選択だよ。僕は低めのレイテンシ(10-20usワイヤーからワイヤー)トレーディングシステムを開発してきたけど、大半のコードは速く動かす必要がないんだ。無駄な努力で、デバッグの頭痛の種、技術的負債になってるだけ。だから、自然なトレードオフは、ホットパスを速くするために少しの痛みを伴うことだね。スパンやunsafeコード、事前に割り当てたオブジェクトプールなどを使って、代わりに他の部分では安全で簡単なプログラミング言語を使えるようになるんだ。C#では低レイテンシ開発はそれほど苦痛じゃなくて、ランタイムによってこの目的のために特に用意されたツールがたくさんあるからね。
> つまり、追加のステップを踏んだ手動メモリ管理ってことか。これは実は完璧な状況だよ。ホットパスの1%のコードに対しては慎重に手動でやることが許されてるけど、99%のコードについては心配しなくていいんだから。
Javaはオブジェクトプールを使うようにはなってないんだよね。20年間Javaのコードを書いてきたけど、オブジェクトの割り当てを避けるためにキャッシュを使ったこともないし、同僚が使ってるのも見たことない。あなたが話してた人、何も分かってないね。
こういうコメントの問題は、コメント主が考えてる「より良い言語」を読者が推測することになるってことだね。これは意図的なことが多くて、具体的なコミットメントを避けられるから、批判や評論には便利なんだよね。例えば、言及されてる「より良い言語」がCだとしたら(大体そうだけど)、標準ライブラリが文字列の長さを参照するのにO(n)の二次的な文字列操作に導く言語だし。
プロジェクト・ゾンボイド(Javaで書かれてるやつ)を少し前にデコンパイルしてみたんだ。ゲームのパフォーマンス問題が気になってね。(10年前のノートPCでめっちゃラグいし、見た目はシムズ1みたい。)最良のシナリオだと、簡単なボトルネックが見つかって修正できるかなと思ったんだけど、全体的には標準的なJavaのOOPだった。さらにその上にいろんな関数型プログラミングの要素もあった。なんか共感できるなー。彼らが始めたときは大学生だったと思うし、俺もOOPとFPのフェーズがあったから。でも、10年以上経ってもそのまま残してるんだよね。だから、どんな言語でもCを書くことができるってのは本当だけど、そういう人たちはそもそもJavaを使わない傾向があるよね ;) -- (ノッチは別?彼のコードはCみたいだけど、実際に速いかはわからない!昔の彼の4キロバイトのJavaゲームはすごく楽しんだし、各ゲームのソースも公開してたと思う。) 編集:見つけた! https://web.archive.org/web/20120317121029/http://www.mojang... 編集2:これ、ダウンロードできるやつで、まだ動くよ! https://web.archive.org/web/20120301015921/http://www.mojang...
Javaで多くのポイントをクラスとして扱うGISデータを扱ってるときに、そんな提案を見たことがある。XYZのダブルを保存するためのオブジェクトオーバーヘッドはかなりクレイジーだよね。最適化の方法は、グローバルなダブル配列を作って、"ポインタ"を使って配列の中の数値を取得・設定することだった。JavaScriptの方がずっと良いよ、ほんとに。
> その時点で、なぜそのタスクにもっと良い言語を使わないの?例えば?
ちょっとした指摘だけど、時間ごとのオーダーはもっと速くできるよ。問題は、配列の方が速くて全然問題ないのにマップを使ってること。さらに、マップは「時間」をボックス化しちゃうのが良くない。こんな感じで書くかな。 `long[] ordersByHour = new long[24];` `var deafultTimezone = ZoneId.systemDefault();` `for (Order order : orders) {` `int hour = order.timestamp().atZone(deafultTimezone).getHour();` `ordersByHour[hour]++;` `} ` 配列のサイズが分かっていて、大きくなくて、直接インデックスを使うなら、パフォーマンス的にこれ以上良くするのは難しいよ。可読性も落ちないし、Javaの開発者は配列をあまり使わないから、ちょっと馴染みがないだけだね。
もしかしたら、長整数よりも整数を使った方がいいかもね。だって、Javaのリストは整数の最大値以上にはならないから。キャッシュラインを一つか二つ節約できるよ。
もし本当に速度が必要なら、タイムスタンプのインスタントオブジェクトも消しちゃって。詳細はここを見てね: https://github.com/williame/TimeMillis
Javaの文字列の落とし穴を避けるのは、プログラミング言語設計において面白い問題だね。`String.format()`の問題は、まず悪いコンパイラと実装だと思う。リテラル文字列を最初の引数として特別扱いして、コンパイル時にパースして、構造化された表現を渡すのは難しくないよ。メソッドはランタイムキャッシングもできるし。非常に小さなLRUキャッシュでも、多くの一般的なケースを解決できるはず。少なくとも、特定のフォーマット文字列からフォーマッターを作って再利用できるようにすべきだね。正規表現みたいに、パフォーマンスを向上させるために明示的に選べるように。最終的には、文字列テンプレートの提案が戻ってきて、言語レベルでこれを修正すべきだと思う。より良い構文と、テンプレートのコンパイル時構築の保証が必要だね。言語は開発者が速いことをするのを助けるべきだよ。文字列の連結はちょっと難しいけど、JIT言語では、異なる使用パターンを最適化する文字列実装の階層を作るための多くのオプションがあるし、速さも保てる。連結に必要なのは、JSのVMが持っているRopeStringみたいなもので、他の文字列を参照するだけなんだ。問題は、ホットパスの文字列メソッド呼び出しに仮想呼び出しを使いたくないこと。Javaは単一のファイナルクラスを選んだから、すべての呼び出しが直接的なんだ。でも、ほとんどのメソッドがファイナルで直接呼び出せるような非常に小さなシールドクラスの階層を作ることができたはずだよ。ストレージにアクセスするための仮想メソッドは、最適化されたメソッドでデバーチャライズされて、呼び出しサイトを通じて一つか二つのクラスしか見ないようにするべきだった。私にとっては、一般的な文字列パターンを速くするための小さな複雑さのコストだと思う。`StringBuilder`を必要とするよりもね。
JVMソフトウェアでstraceを実行すると、すごく非効率なシステムコールのパターンが見えることがあるよね。
そうだね、Javaは結構速いけど、明らかに最適じゃないことがまだあるのが面白い。ZigやD、Rustが言ってる通り、コンパイル時にフォーマット文字列を解析して、実行時に超効率的になるのが好きなんだ。解析も正規表現もなしで、必要な文字列を得るための最適なコードだけが生成されるからね。こう言ってるけど、実際にはほとんどのコードをJava/Kotlinで書いてるよ :D もっと低レベルの言語で超効率的なコードを書けたらいいんだけど、今やってることにはJavaで十分なんだよね。
> 結局、文字列テンプレートの提案は言語レベルでこれを修正するべきだよね。彼らは試みたけど、反対派が無意味なところまで薄めちゃって、今後はこの失敗した試みを楔として使うことになるだろうね。ごめん、Javaが私たちの生きてる間にまともなStringテンプレートを得るとは思えない。
たぶん、zigのcomptimeみたいなものが少しは役立つかもね。
Common Lispがフォーマットの問題をどう扱うかを見るのは面白いよね。CLには「コンパイラマクロ」と呼ばれる一般的なインフラがあって、これはコンパイラに対してマクロとして呼び出しを展開するヒントになるんだ。マクロは、展開せずにそのままの形を残すこともできるし、その場合は未展開の関数呼び出しになる。関数自体も値に変えられて、コンパイラマクロがあっても渡すことができる。CLのフォーマットに関しては、実装は通常、フォーマットが文字列定数の場合に展開を行うコンパイラマクロ(または似たようなメカニズム)を持っていることが多い。CLにはフォーマット文字列を受け取って、(lambda (&rest args) (apply #'format args) のように動作する関数を返す「formatter」という関数もある。この関数はフォーマット文字列をコードに展開して、そのコードをコンパイルするものとして実装できる。CLのメカニズムを使えば、実装がそれを提供していなくても、フォーマットコンパイラマクロ(とformatter)の同等のものをユーザーが実装できるんだ。
その例を見てちょっと驚いたよ。特に新しいことはないからね。これは典型的な初心者の落とし穴で、少なくとも10年以上前からあるものだし。もしかしたら、90年代後半にJavaを学んで、その後J2MEで使ったからかもしれないけど、StringBuilder(昔はStringBufferだったね)を使うのはほぼ必須で、無駄なオブジェクトの割り当てを避けるようにすごく気を使ってたから。
大学のプログラミング入門コースでJavaを書いてたのを思い出すなぁ。2010年頃だったかな。PHPでオブジェクト指向プログラミングにはすでに慣れてたから、JavaのコードもPHPみたいに書いたんだよね。でも、Javaアプリのパフォーマンスの悪さには本当に驚いた。チューターの一人に聞いたら、彼がコードを見て「おお、ループの中でオブジェクトをインスタンス化してるから、そりゃ遅くなるよ」って言ってたのを今でも覚えてる。え、何それ?PHPでこれが効率的にできるなら、オブジェクト指向の旗艦であるJavaがオブジェクトのインスタンス化を速くできないのはどういうこと?今でも首を振っちゃうよ。
そうだけど、今のモダンなフレームワークはそんな道を示してないよね。
これはSpring特有の不満なんだけど、このブログ記事はSpringを前提にしてないのはわかってる。でも、`new ObjectMapper()`を見るのが嫌なんだ。Spring Bootは自動でObjectMapperを設定してくれるし、そのカスタマイズ(`java.time`の扱いやクラスパスのスキャンなど)は絶対に使いたいよね。`ObjectMapper`のBeanを使わないせいで、いろんなバグに悩まされたことがある。
まあ、ジャクソンは遅いよね。
Javaはずっと好きだったけど、全体的にSpringは言語にとってひどい影響を与えてるよね。オートワイアリングは型安全なプログラミング言語の原則に反する。どのオブジェクトが参照に付けられるかを推測させないでほしい。もしそうするなら、少なくともコンパイル時にリンクされたオブジェクトを特定してほしい、ランタイムじゃなくて。SpringのオートワイアリングはJavaを不必要に複雑に見せる。言語では強く避けるべきだと思う(リニューアルされてコンパイラの一部になるまで)。…ObjectMapperにどう関係するかはわからないけど、最近Javaでプログラムしてないから。…それに、私の不満はSpringBootには当てはまらないけどね :)
本当にパフォーマンスの良いJavaコードを書きたいなら、「spring」なんて言葉は出てこないはず。Jacksonも同じで、データが数十万を超えるなら、自分で遅延読み込みのJSONライブラリを書いた方がいいよ。コードは見た目は良くないけど、すごく速くなるから。
Javaの`synchronized`キーワードは間違いだったと思う。複数のスレッドで使うことを前提にしたクラスで、実際にすべてのメソッドに`synchronized`が付いてるのを見たことがある。だって「それが動かす唯一の方法だった」から。もちろん、すべてのメソッドが`synchronized`だと、実際には複数のスレッドで使えないし、見た目だけだよね。一般的に、ロックを避けるように頑張ってる。ロックはアンチパターンだと思う。多くの問題に対して、ロックを使おうとするのは、問題を十分に考えてないからだと思う。ロックはバンデイドで、アプリに潜在的なボトルネックを作る。非同時的なパターンを同時的な環境に押し込もうとするのは、クソみたいな修正だと思う。スレッドセーフで同時的かつ保守可能で速いものを作るのは難しい問題だと思うし、同時アプリにスレッドを無理に追加しようとするのは、デバッグが不可能なひどいコードを書く方法だと思う。好みは人それぞれだけど、今プログラムを書くときは、いつも同時性を優先して作るようにしてる(だいたいアクターモデルを使ったり再発明したりしてる)。初期のアルゴリズムを作るときから、同時性が避けられないことを受け入れるようにしてる。最後に`synchronized`を使ったのはいつだったか覚えてないけど、たまにReentrantLockを使うことがある。その時はいつも気持ちが悪い。
大きな間違いだね。「synchronized」を追加すれば並行性の問題が解決すると思ってるコードをたくさん見たことがある。しかも、synchronizedがどう実装されているかについて話す人もいないし、基本的にはオブジェクトのモニターなんだ。視点も通常間違っていて、メソッド呼び出しのフローに焦点を当てるけど、共有データへのアクセスを保護することを考えるべきなんだよね。
0. アルゴリズムを修正しなよ、コードの最適化は後でいい 1. 抽象化はできるだけ避けて、複雑なフロー制御を減らして無駄なオブジェクトの生成を減らす 2. 並行性を正しく管理する方法を学ぶ、複数のスレッドがアクセスするデータに焦点を当てて、逐次アクセスに集中する 3. 膨れ上がったフレームワーク(全部)を使わない 4. 上記の原則に従って、実際に必要な機能だけを持つ共通ライブラリを再構築することを考えてみて。簡単に10倍の改善ができるから、試してみて。