個人的なOSS活動として、今年はずっとOXCのことをやっておりまして。
で、このプロジェクトはとってもパフォーマンスにシビア(㍉秒とかそういう)で、PRごとにCodspeedというベンチマークCIがパフォーマンスの変化を教えてくれるようになってる。
で、今回出したとあるPRがどうにもパフォーマンスを低下させてるらしく、原因がわからず困った・・・という話。
問題のコード
これが、パフォーマンス低下に遭遇したoxlint
に対するPRの再現コードのイメージ。
fn run(node: &AstNode, ctx: &Context) {
// 前提となる材料たち
let settings = &ctx.settings();
let target_name = settings.get_name("foo");
// 対象のノード以外はEarly return
let Some(target_node) = check_target_node(node, ctx) else { return };
// 処理の本編
if target_node.name == target_name {
// ...
}
}
Linterなので、特定のASTノードに対してのみ、処理を行いたいというイメージ。
run(node, ctx)
は、ソースコード中のすべてのASTノードに対して呼び出される。
で、このコードのパフォーマンスが悪くてなんで・・・?って。
この疑似コードでいうcheck_target_node()
は、親ノードを遡ったりしてたので、最初はそのループの効率が悪いのか?とか調査してた。
問題だった部分
記事タイトルからお察しの通り、このsettings.get_name()
は、&str
ではなくString
を返す処理だった。
fn run(node: &AstNode, ctx: &Context) {
let settings = &ctx.settings();
let target_name = settings.get_name("foo"); // 👈 これ
let Some(target_node) = check_target_node(node, ctx) else { return };
if target_node.name == target_name {
// ...
}
}
なので、数多のASTノードに対して、使いもしないのにString
を生成していて、それがパフォーマンス影響になってた。
というわけで、本当に必要なときだけ生成するようにしたら、CIがパスするようになった。
fn run(node: &AstNode, ctx: &Context) {
let Some(target_node) = check_target_node(node, ctx) else { return };
// 本当に必要なケースでだけ
let settings = &ctx.settings();
let target_name = settings.get_name("foo");
if target_node.name == target_name {
// ...
}
}
String
と&str
の違いはわかってたとはいえ、ただ生成するだけでこんなに影響あるんや・・・ってのを実感した1日であった。