対処
io.r2dbc:r2dbc-postgresql
をorg.postgresql:r2dbc-postgresql
に直せば通ります。
背景
r2dbc-postgresql
は0.9
以降でリポジトリの移行が行われています。
io.r2dbc:r2dbc-postgresql
をorg.postgresql:r2dbc-postgresql
に直せば通ります。
r2dbc-postgresql
は0.9
以降でリポジトリの移行が行われています。
日頃からお世話になっているintellij-rainbow-brackets
のコードを読んでいた所、以下のコードを見つけました。
このコードはローカル関数(関数内に定義する関数)を多用する書き方をしていますが、これがパフォーマンスに影響を与えないのか気になったため確認を行いました。
上記のコードをJava
へデコンパイルすると、以下のような表現になっています。
public final void annotateUtil(/* */) { <undefinedtype> $fun$getBracketLevel$1 = new Function1() { public final int invoke(@NotNull LeafPsiElement element) { <undefinedtype> $fun$iterateParents$1 = new Function1() { public final void invoke(@NotNull final PsiElement currentNode) { while(true) { <undefinedtype> $fun$iterateChildren$1 = new Function1() { public final void invoke(@NotNull PsiElement currentChild) { } }; } } }; }; }; }
かなり要約していますが、ここからは以下のような問題点が見えます。
new
が行われているtailrec
)内では、繰り返しローカル関数のnew
が行われている検証のため、分割を行っていない状態と、分割後の状態とを再現し、それらに対するJMH
ベンチマークを作成しました。
コードは以下の通りです。
import org.openjdk.jmh.annotations.Benchmark import org.openjdk.jmh.annotations.Scope import org.openjdk.jmh.annotations.State // ベンチマーク対象(分割後) object Separated { private tailrec fun inner(i: Int, limit: Int): Int = if (i == limit) i else inner(i + 1, limit) private tailrec fun outer(i: Int, limit: Int): Int = if (i == limit) inner(i, limit) else outer(i + 1, limit) private fun caller(limit: Int) = outer(0, limit) // limit must be an integer greater than or equal to 1. fun target(limit: Int) = caller(limit) } // ベンチマーク対象(分割前) object Combined { // limit must be an integer greater than or equal to 1. fun target(limit: Int): Int { fun caller(): Int { tailrec fun outer(i: Int): Int { tailrec fun inner(i: Int): Int = if (i == limit) i else inner(i + 1) return if (i == limit) inner(i) else outer(i + 1) } return outer(0) } return caller() } } // ベンチマーク @State(Scope.Benchmark) open class Measurement { private val limit = 10 @Benchmark open fun combined() = Combined.target(limit) @Benchmark open fun separated() = Separated.target(limit) }
構築は下記を利用しています。
build.gradle.kts
に以下の設定を入れてベンチマークを行いました。
jmh { warmupForks = 2 warmupBatchSize = 3 warmupIterations = 3 warmup = "1s" fork = 2 batchSize = 3 iterations = 2 timeOnIteration = "1500ms" failOnError = true isIncludeTests = false resultFormat = "CSV" }
自分のPCで実行した結果は以下の通りです。
combined
が旧来のコードの再現で、separated
が分割後の想定です。
スコアは高いほど良いです。
Benchmark Mode Cnt Score Error Units Measurement.combined thrpt 4 12430932.298 ± 581801.766 ops/s Measurement.separated thrpt 4 80820942.692 ± 2977174.695 ops/s
推測通り、ローカル関数の利用によってパフォーマンスの問題が発生するようでした。
ベンチマーク内容について現実的な想定だと言い張るつもりは有りませんが、少なくともそれなりの影響は有ると言えます。
combined
のinner
関数を外に出すだけでもスコアが倍以上改善する様子も有りました。
この記事ではintellij-rainbow-brackets
のコードを例にローカル関数の利用がパフォーマンス低下につながることを確認しました。
ローカル関数は便利な機能であるものの、パフォーマンスが重要な場面で利用することは避けた方が良いと思います。
intellij-rainbow-brackets
のパフォーマンス改善も取り組もうと思っています。
最後に、intellij-rainbow-brackets
が継続的にやっていけるよう、スター・スポンサーもよろしくお願いします。
改善やりました。
ソースを読んだ限りですが、C#
とHaskell
は高速化すると思います。
記事執筆時点で、Flow
にはgroupBy
が実装されていません。
一方、Flux
にはgroupBy
が実装されています。
(kotlinx-coroutines-reactive
が入っているような環境では)Flow
とFlux
は相互変換できるため、検討の余地が有ると思います。
marp: true
を設定する<!-- class: invert -->
を設定する設定後のヘッダーは↓のようになります。
--- marp: true # header: 'header text' # footer: '' --- <!-- theme: default --> <!-- class: invert --> <!-- size: 16:9 --> <!-- page_number: true --> <!-- paginate: true -->
やり方そのものは↓にちゃんと書いてあるんですが、自分は理解するのに時間がかかったので書きました。
こちらのページからは公式テーマの一覧も確認できます。
github.com
↓の続きです。
wrongwrong163377.hatenablog.com
value class
を引数に含む関数をkotlin-reflect
を用いて呼び出す処理には、以下のような問題が存在していました。
これらの問題は、kotlin-reflect
に依存するライブラリがvalue class
をサポートする上でクリティカルな障害となっていました。
また、これらの問題を検知するためのテストパターンも不足している状況でした。
Kotlin
のリポジトリに以下のPRを発行し、マージして頂きました。
JetBrains
側でより良い解決方法が出たため、マージされなかったこれらの変更によって、ライブラリの実装を妨げるようなクリティカルな障害は解消されたと思われます。
感想などは前回の記事に書いたので省略します。
リリースは今の所Kotlin 1.7
になる予定です。
これによって様々なライブラリでvalue class
サポートが進むと考えると今から非常に楽しみです。
Kotlin 1.7
でリリースされました!
import org.jooq.Field import org.jooq.Record inline fun <reified T> Record.read(field: Field<*>): T = this[field, T::class.java]
以下のように使います。
import org.jooq.Field import org.jooq.Record // FOOテーブルに定義された文字列型のFieldを想定 val field: Field<String?> = FOO.STRING_FIELD // select結果などを保持するRecordを想定 val record: Record // 以下のように、要求された型に応じた読み出しができる val s1: String = record.read(field) val s2: String? = record.read(field) // エルビス演算子もよしなに判断してくれる val s3: String = record.read(field) ?: ""
jOOQ
がデフォルトで想定している読み出し方法の内最もシンプルなものは、Record
に定義された<T> T get(Field<T> field)
を利用することです。
一方、KotlinGenerator
で生成されるコードではField
(TableField
)の型引数が常にnullable
となるため、non-null
な値として読み出す場合、一々!!
で強制アンラップを行う必要が有ります。
// 単純にgetを用いた例 val s: String = record[field1]!!
また、Record
から何かしらの変換を伴う読み出しを行う場合は<U> U get(Field<?> field, Class<? extends U> type)
を利用することになりますが、この関数はKClass
を受け付けません。
このため、Kotlin
から利用した際の見た目がよろしくありません。
// 変換を伴うgetを用いた例 import java.time.LocalDate val d: LocalDate = record[field2, LocalDate::class.java]
今回定義した関数を使えば、両方ともシンプルかつ画一的な書き方ができます。
また、読み出し先がnullable
になるような場合でも同様に書くことができます。
// 今回定義した関数を使った例 import java.time.LocalDate val s: String = record.read(field1) val d: LocalDate = record.read(field2) // 要求される値がnullableな場合も同様に書ける val s2: String? = record.read(field1)
クエリに対して期待される読み出し結果は基本的にマップ先の型に全て書かれているため、この関数を利用することで表現の重複を減らして機械的に書きやすくなります。
関数の名前をread
にしているのは、get
にするとjOOQ
デフォルトの内容と名前が被ってしまうためです。
最終的にやったことをまとめた記事を投稿しました。
wrongwrong163377.hatenablog.com
JetBrains
/kotlin
にGitHub
でPR
を出し、マージしてもらいました。
当該PR
は以下です。
github.com
PR
の内容このPRに関しては全てテストです(変更行数は2,500行超、結構巨大です)。
このPR
は、足りていなかったテストコードの補完兼後続の修正を分かりやすくするためのfailing test
であり、本題となる修正は別途PR
を発行しています。
内容は、kotlin-reflect
でnullable
なvalue class
を引数に含む関数を呼び出した際にエラーになるバグの緩和です。
github.com
大まかな感想は以下の3つです。
Kotlin
に貢献できたことが嬉しいKotlin
のような巨大プロジェクトでテストパターンが全く足りていない部分が有るとは思わなかったkotlin-reflect
に依存するライブラリ全体のvalue class
サポートを進められそうなのでやりがいが有るKotlin
に貢献できたことが嬉しいKotlin
に触れたのは、2018年のエウレカでのインターンシップがきっかけでした。
wrongwrong163377.hatenablog.com
そこから利用を続け、バックエンドはJava
だった職場にKotlin
を持ち込んだり、Kotlin
製の自作OSS
を公開したり、moshi
やjackson-module-kotlin
のような有名リポジトリにコントリビュートしたり、そしてついにKotlin
そのものにコントリビュートすることができました。
ずっと好んで取り組んできたものにコントリビュートできたというのは非常に嬉しかったです。
(どうでもいいことですが、今思い返すと、初めて外部リポジトリにコントリビュートしたのが丁度1年位前でした。) wrongwrong163377.hatenablog.com
Kotlin
のような巨大プロジェクトでテストパターンが全く足りていない部分が有るとは思わなかった前述の通り、テストだけで2,000行超という非常に大きなPRを発行した訳ですが、その原因はそもそも当該箇所に関してテストパターンが全く足りていない状況だったことです。
value class
は既にstable
とされている機能ですが、実際の所関連するkotlin-reflect
の機能には多くの重大なバグが残されています。
今回自分はテストパターンの追加を行いましたが、テストパターンは恐らくまだまだ不足しています。
見たのがkotlin-reflect
単体とは言え、Kotlin
という大きなプロジェクトにこのような状況が有るというのは自分にとって大きな驚きでした。
kotlin-reflect
に依存するライブラリ全体のvalue class
サポートを進められそうなのでやりがいが有る自分は現在KT-31141(本題となるPR
で対応している問題)と、KT-27598の緩和に取り組んでいます。
これらの問題は長らくkotlin-reflect
に依存するライブラリ全体のvalue class
サポートを阻んできたものです。
つまり、自分の修正によって、kotlin-reflect
に依存するライブラリ全体のvalue class
サポートが進むということになります(あくまで上手く行けばですが)。
少なくともjackson-module-kotlin
については自分の方で進めようと思っています。
自分が書いたコードによってそのような大きな変化が起きるというのは、大規模OSS
に貢献する大きなやりがいですね。
これからもぼちぼちやっていこうと思います。