Microsoft OpenJDKのエスケープ解析改善を試したら割と効いてる

MicrosoftのOpenJDKにエスケープ解析の改善が入ってるってことで試してみました。
https://www.infoq.com/news/2023/02/microsoft-openjdk-feature/

Microsoftのアナウンスはこちら。
https://devblogs.microsoft.com/java/microsoft-build-of-openjdk-october-2022-psu-release/

エスケープ解析は、雑にいうと、メソッド内で生成されてそのメソッド内だけで使われるようなオブジェクトについて、ヒープに確保するのではなくてフィールドをローカル変数に分解して処理をするような最適化です。
正しくは、そいういう最適化が行えることを発見する解析のことだけど。

基本的にはメソッド呼び出しを埋め込み処理に変換するインライン化とあわせて威力を発揮します。

例えばこんな感じのクラスがあるとして。

record Point(int x, int y) {
  Point add(Point other) {
    return new Point(x + other.x, y + other.y);
  }
}

こんな感じの処理があるとする。

void some() {
  var p = new Point(3, 8);
  var next = p.add(new Point(3, 3));
  printf("%d,%d - %d,%d%n", p.x(), p.y(), next.x(), next.y());
}

ここでaddメソッドをインライン化

void some() {
  var p = new Point(3, 8);
  Point other = new Point(3, 3);
  var next = new Point(p.x + other.x, p.y + other.y);
  printf("%d,%d - %d,%d%n", p.x(), p.y(), next.x(), next.y());
}

そうすると、このpothernextsomeメソッドから出ていない、エスケープしていないので、ヒープに置いてメソッド処理の終了からしばらくしてGCするよりも、メソッド終了とともに開放したほうがいいってなります。 そうするとスタックに置こうということになるのだけど、オブジェクトをスタックに置くというのは今のところめんどい。であればローカル変数に分解しようというのがscalar replacementです。

void some() {
  int p_x = 3;
  int p_y = 8;
  int other_x = 3;
  int other_y = 3;
  int next_x = p_x + other_x
  int next_y = p_y + other_y;
  printf("%d,%d - %d,%d%n", p_x, p_y, next_x, next_y);
}

これはすでにJava 8でも存在する機能なのだけど、object allocation mergeを簡素化することでscalar replacementを増やす改善ということらしい。mergeがなにかよくわからないけど、メソッド呼び出しに関わることな雰囲気。
https://github.com/openjdk/jdk/pull/9073

で、試してみました。ベンチマークによくつかっているレイトレ。3次元座標をあらわすVecをnewしまくるので、エスケープ解析が効くと速くなるアプリケーションです。

https://github.com/kishida/smallpt4j/blob/original/src/main/java/naoki/smallpt/SmallPT.java

このSAMPLES_DEFAULTは100にしています。またfastmathは使わず、java.lang.Mathを使っています。

エスケープ解析の改善を行うには次のオプションを指定します。

-XX:+UnlockExperimentalVMOptions -XX:+ReduceAllocationMerges

実行するとこんな画像が生成されます。

結果としてはこんな感じ。単位は秒。値が小さいほど性能が高いです。
RAMがReduceAllocationMergesを指定したものです。

ということで、12.65秒から12.46秒に改善、1.5%くらい速くなっていて、Microsoftのブログにある2%のスループット改善に近い値がでました。

だいたいこういう改善は、結局あまり手元のベンチマークに現れないことが多いのだけど、今回は実行する感触として速くなってるなーというのがありました。

JobRunrを試してるんだけどクセが強い

JobRunrという、バッチ処理とかバックグラウンドタスクとかを試してみたんだけど、よくわからない・・・
https://www.jobrunr.io/en/

dependencyにはjobrunrと、あと何かJSONパーサーを入れます。ここではjacksonにしてますが、GSONかyassonでもいいぽい。

<dependency>
    <groupId>org.jobrunr</groupId>
    <artifactId>jobrunr</artifactId>
    <version>6.0.0</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.11.0</version>
</dependency>

とりあえず設定。

JobRunr.configure()
        .useStorageProvider(new InMemoryStorageProvider())
        .useBackgroundJobServer()
        .initialize();

Jobを放り込んでみる。

BackgroundJob.enqueue(() -> System.out.println("Hello"));

SLF4Jのログと共に、Helloと表示されました。動いてそう。以降、SLF4Jのログは省略。

SLF4J: No SLF4J providers were found.
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See https://www.slf4j.org/codes.html#noProviders for further details.
Hello

ビルダーがあるで、ってことで使ってみる。

BackgroundJob.create(JobBuilder.aJob()
    .withName("my job")
    .withDetails(() -> System.out.println("Yeah!!!")));

なんか出てる。よさそう。ここまでは・・・

Hello
Yeah!!!

繰り返しタスクを投入してみる。3秒を指定すると5秒以上にしろと怒られたので、5秒にする。

BackgroundJob.scheduleRecurrently(Duration.ofSeconds(5),
        () -> System.out.println("Yooo!!!"));

なんか3つずつ出てくる。

Yooo!!!
Yeah!!!
Yooo!!!
Yooo!!!
Hello
Yooo!!!
Yooo!!!
Yooo!!!

ちょっと時間を同時に表示しようとLocalTime.now()を付け加える。

BackgroundJob.scheduleRecurrently(Duration.ofSeconds(5),
        () -> System.out.println("Yooo!!! %s".formatted(LocalTime.now())));

全部同じ時刻だ・・・どういうこと?

Yooo!!! 12:15:34.239878800
Hello
Yeah!!!
Yooo!!! 12:15:34.239878800
Yooo!!! 12:15:34.239878800
Yooo!!! 12:15:34.239878800
Yooo!!! 12:15:34.239878800
Yooo!!! 12:15:34.239878800

カウンターを付け加えてみる。

int[] count = {0};
BackgroundJob.scheduleRecurrently(Duration.ofSeconds(5),
        () -> System.out.println("Yooo!!!(%d) %s"
                .formatted(count[0]++, LocalTime.now())));

死んだ・・・・

Exception in thread "main" org.jobrunr.JobRunrException: JobRunr encountered a problematic exception. Please create a bug report (if possible, provide the code to reproduce this and the stacktrace)
   at org.jobrunr.JobRunrException.shouldNotHappenException(JobRunrException.java:43)
   at org.jobrunr.jobs.details.instructions.AllJVMInstructions.get(AllJVMInstructions.java:82)
   at org.jobrunr.jobs.details.AbstractJobDetailsFinder$1.visitInsn(AbstractJobDetailsFinder.java:46)
   at org.objectweb.asm.ClassReader.readCode(ClassReader.java:2213)
   at org.objectweb.asm.ClassReader.readMethod(ClassReader.java:1514)
   at org.objectweb.asm.ClassReader.accept(ClassReader.java:744)
...

結局、scheduleRecurrentlyメソッドなどでジョブ登録するラムダ式は、直接よびだされるのではなくてシリアライズされたあとで復元されて呼び出されるぽい。ので、ラムダの中で計算をしてはダメで、メソッド呼び出してそっちでいろいろやるべきってことらしい。

ということでメソッドを定義する。このとき、このメソッドはJobRunrの内部から呼び出されるのでpublicである必要がある。

static int count = 0;
public static void some() {
    System.out.println("Yooo![%d] %s".formatted(++count, LocalTime.now()));
}

で呼び出しジョブを登録する。

BackgroundJob.scheduleRecurrently(Duration.ofSeconds(5),
        () -> some());

いい感じ

Yooo![1] 12:41:48.847377500
Yooo![2] 12:41:48.859376600
Yooo![3] 12:41:48.887877700
Yooo![4] 12:42:03.937876600
Yooo![5] 12:42:03.986377300
Yooo![6] 12:42:04.018877300

あと、15秒ごとに呼び出されて3回実行しているので、まあ5秒ごとに3回というのが間に合わなかったという感じっぽい。

最後に、useBackgroundJobServer()を設定しておくと、ダッシュボードが表示できる。

JobRunr.configure()
        .useStorageProvider(new InMemoryStorageProvider())
        .useBackgroundJobServer()
        .useDashboard(8000)
        .initialize();

現実的には、@Jobアノテーションでジョブを指定することが多そうなので問題はない気がするけど、ちょっと遊ぼうとして雑に呼び出すとハマるという話。

ChatGPTに負けないために大切なこと

ChatGPT、人気ですよね。
そして、こんなに賢いAIが現れたら人間の仕事はなくなるような気がしてしまいます。
そこで、ChatGPTに負けないためにどうしたらいいか考えてみます。
※ChatGPTに負けない必要があるのか、というのはまた別の話。

原理、原則を知る

ChatGPTは膨大な知識をもっています。これを人間が対抗するのは無理です。そこで、論理などの原理、原則を知ることで知識量を補っていきます。
たとえば「カラスは暗いところで見づらい」「タイヤは暗いところで見づらい」「黒いコートは・・・」のように暗いところで見づらいものについて多くの情報を覚えておくのではなくて、「黒いものは暗いところで見づらい」という原則をしっていれば「カラスは黒い、ということは暗いところで見づらい」「タイヤは黒い、ということは・・・」「黒いコートは・・・」のように類推することができるようになります。
さらにこれを「AはBであり、BはCならばAはCである」というように意味を抜いて考えるようにできると、さまざまなものに応用できます。

論理や原理というものは、いろいろな物事に見出せる共通原則を抜き出したものなので、知っておくと多くの知識をもっているのと同じような状況をつくれます。
たとえばいま多くのプログラミング言語がありますが、「プログラミング言語とは何か」のような共通原則を知っておくと、文法などを全部勉強しなくても「こういう方針の言語だからこういう機能があるはず」「これはこういう機能だからこうやって使うはず」のように予測しながら使うことができます。

そうやって、原理や原則をしって知識を補うことで、ChatGPTに負けないようにできるかもしれません。

最新情報を知る

ChatGPTには膨大な情報がありますが、実は最新情報に弱いです。
「たしかにChatGPTは2021年9月までの情報しかもってないかもしれないけど、今後はリアルタイムにニュースを取り込んでいくのでは?」
となるかもしれません。
けれども、そもそもChatGPTが情報のよりどころにしているインターネットに最新情報が少ないのです。
たしかに「新しく何々ができた」「こういう事件が起きた」という情報はたくさん流れてきます。けれど、ではその新しくできた何々はその後どうなったか、起きた事件は結局どうなったのかという情報がネットに流れることはどんどん減っていきます。

ChatGPTは基本的にはネットの情報の多数決のような形で文章をつくっていきます。そうすると、多数の古い情報が優先されて新しい情報が埋もれるということが起きがちです。そもそも新しいことは誰もネットに書いていなかったりもします。 たとえば、ネットを見ると「Javaをインストールするときは環境変数PATHを設定する必要がある」という情報がよくみつかりますが、2020年9月リリースのJava 15からOracle JDKインストーラもPATHを設定するようになっています。けれどもPATHの設定が不要になったという情報はなかなか流れませんね。わざわざブログなどにも書かないと思います。なんなら2022年公開のYouTube動画でもPATHの設定が必要と解説していますね。

このように、ネットでは最新情報は埋もれがちだったり、そもそもネットに文書化されてなかったりで、そこを拠り所として学習するChatGPTは最新情報に弱いということになります。
そこで、自分の強みにする分野では最新の情報をちゃんとチェックしていくことでChatGPTに負けないようにできるかもしれません。

専門情報を得ておく

ChatGPTは幅広い情報をもっていて、いろいろな専門分野にも詳しいです。けれどもよく見ると「よくある解説」の範囲を出ていなかったりします。また、局所的な情報に弱く、さしさわりのない説明しか出ないことも多いです。
たとえば鈴鹿サーキットの特徴を尋ねると次のようになります。

長いストレートとテクニカルなコーナーが特徴といいますが、富士のほうがストレートは長いし、サーキットならテクニカルなコーナーはだいたいあります。スピードとドライビングテクニックはどんなサーキットでも求められるし、グランドスタンドからの観戦が良いのも当然ですね。

そんな感じで、ちょっと詳細になると途端に苦手になるのがChatGPTです。つまり、みんながよくブログに書きそうなことはよく知っているけどあまりブログに書かれてないことは知らないという感じになっています。 ということで、自分の専門分野に関してはちゃんと知っておく、そして、なにかそういった専門分野をもっておくというのがChatGPTに負けないために大事かもしれません。

まとめ

結局のところこれは、Google検索がでてきてネットに情報がなんでも載るようになったときに言われてきたことと変わらないです。
ネットの特徴がChatGPTの特徴でもあるからです。
ということで、原理・原則をつかんでおいて最新情報を更新するという、いままでも大切だったことがこれからも大切ということになると思います。

ギターのスケールを確認できるツールを作ってみた

ギターの練習をするときに、スケールとかコードのポジションを確認するのがめんどくさく、あと指板全域にわたってポジションを知りたくてもなかなか求めるものがなかったりしたので、作ってみました。
https://kishida.github.io/guitarscale/

この怪しいギター画像もStable Diffusionが生成してます。

キーやスケールを選ぶとポジションが表示されます。

コードも表示できます。

Advancedにチェックを入れると、スケールやコードの種類が増えます。ブルーススケールではブルーノートを青く表示します。

Noteのチェックを入れると度数で表示します。

7弦や8弦、あとDrop DとDADGADに対応しています。

今回はTypeScriptで書いてますが、やっぱ型があるのは いいですね。

GPTでテキストからJavaコードを生成する

昨日のエントリでは歌詞と画像を生成したのだけど、歌詞を生成する部分のプロンプトを変えて、エンジンにCodexを使うとコードを生成できます。

GPTを呼び出す部分はこんな感じ

String prompt = "//Java\n" +
        text.getText().trim().lines()
                .map("// %s%n"::formatted)
                .collect(Collectors.joining());
CompletionRequest completionRequest = CompletionRequest.builder()
        .model("code-davinci-002") // or code-cushman-001
        .prompt(prompt)
        .echo(false)
        .temperature(0.3)
        .user("testing")
        .maxTokens(500)
        .build();

プロンプトは// Javaで始まるコメントになるようにしています。そしてモデルにcode-davinci-002を使っています。

あと、生成に時間がかかってデフォルトのままではタイムアウトするので、タイムアウト無制限にしています。

OpenAiService service = new OpenAiService(TOKEN, Duration.ZERO);

これだけでコードを生成してくれるようになりますが、コードを生成してくれるプロンプトを作るのが難しいです。英語でプロンプトを書いてますが、日本語にすると画面のつくりかたを日本語で説明する感じになったりします。

単純なアルゴリズムの指定であれば、比較的楽

生成コードはHTMLエンコードされていたりするので、対応が必要です。あと、最後に</code>がついてそれ以降に説明がついてくるので切り捨てる必要もあると思います。

コードの全体はこんな感じ

package naoki.openai;

import com.theokanning.openai.OpenAiService;
import com.theokanning.openai.completion.CompletionRequest;
import com.theokanning.openai.completion.CompletionResult;
import java.awt.BorderLayout;
import java.time.Duration;
import java.util.stream.Collectors;
import javax.swing.*;

public class GenerateCode {
    public static void main(String[] args) {

        var f = new JFrame("コード生成");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        var p = new JPanel();
        var text = new JTextArea(4, 60);
        p.add(new JScrollPane(text));
        var button = new JButton("生成");
        p.add(button);
        f.add(p, BorderLayout.NORTH);
        var output = new JTextArea();
        f.add(new JScrollPane(output));
        f.setSize(800, 600);
        f.setVisible(true);
        
        OpenAiService service = new OpenAiService("your token", Duration.ZERO);

        button.addActionListener(al -> {
            String prompt = "//Java\n" +
                    text.getText().trim().lines()
                            .map("// %s%n"::formatted)
                            .collect(Collectors.joining());
            CompletionRequest completionRequest = CompletionRequest.builder()
                    .model("code-davinci-002") // or code-cushman-001
                    .prompt(prompt)
                    .echo(false)
                    .temperature(0.3)
                    .user("testing")
                    .maxTokens(500)
                    .build();

            CompletionResult result = service.createCompletion(completionRequest);
            var code = result.getChoices().get(0).getText()
                    .replace("&gt;", ">").replace("&lt;", "<");
            output.setText(code);
        });
    }
}

OpenAIのGPTとDALL·Eでテーマから歌詞と画像を生成する

やっぱ自分でもさわってみんとあかんということで、OpenAIを使ったプログラムを書いてみました。
とりあえず、テキストと画像を生成します。

JavaAPI使ってみます。中身はRetrofit。
https://github.com/TheoKanning/openai-java

MavenやGradleでこんな感じの依存を設定します。

<dependency>
    <groupId>com.theokanning.openai-gpt3-java</groupId>
    <artifactId>client</artifactId>
    <version>0.9.0</version>
</dependency>

まずはOpenAiServiceのオブジェクトをとってきます。OpenAIのサイトでアカウントを作ってトークンつくってください。
https://openai.com/api/

OpenAiService service = new OpenAiService(TOKEN);

Free Trialで$18分つかえるようになっています。これ作るのに試すだけで$0.25=30円使っているので、本格的なアプリを作ろうと思うと結構かかりそうな気配

ChatGPTのようなことをしたければ、createCompletionを使います。

CompletionRequest completionRequest = CompletionRequest.builder()
        .model("text-babbage-001")
        .prompt("次のテーマで歌詞を作って。\n" + prompt)
        .temperature(0.9)
        .echo(false)
        .user("testing")
        .maxTokens(500)
        .build();
CompletionResult result = service.createCompletion(completionRequest);

モデルにはtext-babbage-001のほかtext-ada-001text-curie-001text-davinci-003が指定できます。abc順にどんどん賢く、高くなっていきます。adaが一番安く、davinciが一番賢くて高い。開発時にはadaを使って本番ではdavinciやcurieを使うみたいな使い分けが必要そうです。
プロンプトには、生成する事柄の例を指定します。ここでは「次のテーマで歌詞を作って。」としてますが、もっと丁寧に設定すれば生成結果もまともになる気がします。

画像生成はcreateImageで。

CreateImageRequest req = CreateImageRequest.builder()
        .prompt(prompt)
        .n(1)
        .responseFormat("b64_json") // or b64_json, url
        .size("512x512")
        .build();
ImageResult imageResult = service.createImage(req);

サイズは512x512の他に256x2561024x1024が選べます。フォーマットとしてurlを選べば、画像のURLが送られてくるので、Webアプリではそっちでいいかもしれません。

動作はこんな感じ。実際はレスポンスに8秒くらい待ちます。

ソースコード全体はこんな感じ。

package naoki.openai;

import com.theokanning.openai.OpenAiService;
import com.theokanning.openai.completion.CompletionRequest;
import com.theokanning.openai.completion.CompletionResult;
import com.theokanning.openai.image.CreateImageRequest;
import com.theokanning.openai.image.ImageResult;
import java.awt.BorderLayout;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.util.Base64;
import javax.swing.*;

public class Mavenproject1openai {
    static final String TOKEN = "your openai token";
    public static void main(String[] args) {
        Image def = new BufferedImage(512, 512, BufferedImage.TYPE_INT_RGB);
        var f = new JFrame("歌詞と画像を生成");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        var label = new JLabel(new ImageIcon(def));
        f.add(BorderLayout.EAST,label);
        var resultText = new JTextArea();
        f.add(resultText);
        var top = new JPanel();
        var promptText = new JTextField(50);
        top.add(promptText);
        var button = new JButton("生成");
        top.add(button);
        f.add(BorderLayout.NORTH, top);
        f.setSize(1000, 600);
        f.setVisible(true);
        
        OpenAiService service = new OpenAiService(TOKEN);

        button.addActionListener(al -> {
            System.out.println("\nCreating completion...");
            String prompt = promptText.getText();
            
            CompletionRequest completionRequest = CompletionRequest.builder()
                    .model("text-babbage-001")
                    .prompt("次のテーマで歌詞を作って。\n" + prompt)
                    .temperature(0.9)
                    .echo(false)
                    .user("testing")
                    .maxTokens(500)
                    .build();
            CompletionResult result = service.createCompletion(completionRequest);
            resultText.setText(result.getChoices().get(0).getText());

            CreateImageRequest req = CreateImageRequest.builder()
                    .prompt(prompt)
                    .n(1)
                    .responseFormat("b64_json") // or b64_json, url
                    .size("512x512")
                    .build();
            ImageResult imageResult = service.createImage(req);
            byte[] data = Base64.getDecoder().decode(imageResult.getData().get(0).getB64Json());     
            label.setIcon(new ImageIcon(data));
        });
        
    }
}

SourceBuddyでJavaクラスを動的生成する

Javaバイトコードを操作するツールとしてはASMやJavassistがありますが、SourceBuddyはコンパイルバイトコード生成を行うツールです。
バイトコード操作を行いたい場合、既存バイトコードの変更を行う必要はなくて、実行時に新規クラスを生成できれば十分ということが多いように思います。SourceBuddyはそういう場合に使いやすいツールです。
ということで、Hello worldレベルで少し試してみました。
https://github.com/sourcebuddy/sourcebuddy

といっても、ソースコードを用意してコンパイルするだけなので、使い方はそんなに難しくありません。

まずはMavenなりGradleなりで依存を追加します。READMEには<version>2.2.0-SNAPSHOTを指定していますが、みつからなかったので2.1.0にしています。

<dependency>
    <groupId>com.javax0.sourcebuddy</groupId>
    <artifactId>SourceBuddy</artifactId>
    <version>2.1.0</version>
</dependency>

まず、これは普通のコードとして基になるインタフェースを用意します。

public interface Printer {
    void print();
}

そしてそのインタフェースをimplementsするクラスのソースコードを文字列で用意します。今回はこのコードをSourceBuddyでコンパイル、利用します。

var source = """
             package example;
             public class MyPrinter implements Main.Printer {
                @Override
                public void print() {
                    System.out.println("Hello!");
                }
             }
             """;

そしてコンパイルします。といっても、com.javax0.sourcebuddy.Compilerのstaticメソッドcompileソースコード文字列を与えるだけですね。

Class<?> clazz = Compiler.compile(source);

あとは通常のリフレクションのコードとして、コンストラクタを取得、インスタンス生成を行います。

Printer pt = (Printer) clazz.getConstructor().newInstance();

そうしてメソッドを呼び出すと、コンパイルしたコードが実行されることがわかります。

 pt.print();

ソースコード全体はこんな感じです。

package example;

import com.javax0.sourcebuddy.Compiler;
import java.lang.reflect.InvocationTargetException;

public class Main {

    public interface Printer {
        void print();
    }
    
    public static void main(String[] args) throws 
            ClassNotFoundException, // compileに必要
            Compiler.CompileException,
            NoSuchMethodException,  // getConstructorに必要
            InstantiationException, // newInstanceに必要
            IllegalAccessException, 
            IllegalArgumentException, 
            InvocationTargetException { 
        var start = System.currentTimeMillis();
        var source = """
                     package example;
                     public class MyPrinter implements Main.Printer {
                        @Override
                        public void print() {
                            System.out.println("Hello!");
                        }
                     }
                     """;
        Class<?> clazz = Compiler.compile(source);
        System.out.println(System.currentTimeMillis() - start);
        Printer pt = (Printer) clazz.getConstructor().newInstance();
        pt.print();
    }
}

SourceBuddy自体は検査例外は多く定義されてませんが、リフレクションで多くの検査例外の扱いが必要になります。 あと、compile()で結構時間がかかるので処理時間をとっています。手元の環境で450msかかっています。単発処理なのでなんどか実行すると初期化やJITの分で速くなる可能性はありますが、それでも450msというのは結構な時間がかかっています。

まあ、ソースコード生成はAPIの呼び出し方よりも生成するソースコードとか実行タイミングとかが難しいので、いろいろと試すのがいいんだと思います。