Techouse Developers Blog

テックハウス開発者ブログ|マルチプロダクト型スタートアップ|エンジニアによる技術情報を発信|SaaS、求人プラットフォーム、DX推進

RubyKaigi 2025 Performance Bugs and Low-level Ruby Observability APIs (Day2)

ogp

こんにちは、2024年に新卒入社し、株式会社Techouseクラウドハウス労務でバックエンドエンジニアをしているsatohです。
本記事では、Rubykaigi 2025 での Ivo Anjo(@KnuX) さんによるセッション、「Performance Bugs and Low-level Ruby Observability APIs」について紹介させていただきます。

Performance Bugs and Low-level Ruby Observability APIs

本セッションでは、Ruby アプリケーションのパフォーマンス問題を特定するための Ruby 内部の API (Low-level Ruby Observability APIs) について解説が行われました。

Low-level Ruby Observability API とは、Ruby プログラムを動かす CRuby VM のレイヤーに近い、通常は意識されない詳細に関わる計測を行う API のことです。これらの API を使うことで、アプリケーション内部の動きを可視化できます。

アプリケーションの応答結果は正しいが応答時間が長すぎる場合や、検証環境では問題ないのに本番環境でのみ問題が発生する場合の原因として、いくつかの例が挙げられています。

  • N+1 問題
  • 利用している他サービスの遅延に起因する遅延
  • バックグラウンドで動いている関係ないスレッドによる CPU などリソースの占有
  • 高い GC 処理の負荷

Ruby では、こうしたパフォーマンス問題を特定するためにプロファイラが利用できますが、それらは Observability API を組み合わせることで構築されています。
用途に応じた API の組み合わせや使い方により異なるプロファイラが実現でき、数多くのプロファイラが作られています。

Rubyが提供する Low-level API

Ruby の Low-level API は、主に以下のヘッダーファイルで宣言されています:

  • include/ruby/debug.h
  • include/ruby/thread.h

これらの API はC言語で書かれていますが、Ruby のコードからの使い方を示す例として、KnuX さんの作成した lowlevel-toolkit gem を参照できます。
セッションでは本 gem を利用しながらこれらの API について解説が行われました。

TracePoint API

Ruby 標準の TracePoint クラスでは、Ruby の実行中に特定のイベントが発生した際に、指定した処理を実行できます。

TracePoint クラスは Ruby コード上で利用できますが、一部アクセスできないイベントが存在します。以下に示すイベントは Low-level の TracePoint API からのみ追跡できます。

  • RUBY_INTERNAL_EVENT_SWITCH: スレッドの切り替え
  • RUBY_INTERNAL_EVENT_NEWOBJ: オブジェクト割り当て
  • RUBY_INTERNAL_EVENT_FREEOBJ: オブジェクト解放
  • RUBY_INTERNAL_EVENT_GC_START/END_MARK/END_SWEEP/ENTER/EXIT: GCの各フェーズ

オブジェクト割り当てイベント

TracePoint API の利用例として、lowlevel-toolkit の track_objects_created を見てみましょう。

このツールは、以下のようにブロック内で作成されたすべてのオブジェクトを追跡し返します。大量のオブジェクトが生成されることで CPU 負荷や GC 頻発の問題が生じている箇所を調査するのに役立てられます。

pp(LowlevelToolkit.track_objects_created do
  Object.new
  Time.utc(2025, 4, 17, 1, 0, 0)
  "Hello, world!"
end)

# => [#<Object:0x000078e011750cc8>, 2025-04-17 1:00:00 UTC, "Hello, world!"]

track_objects_created は、TracePoint API を用いた以下のC拡張コードで実装されています。

具体的な動作としては、NEWOBJ イベントの Tracepoint を有効化した上で、 rb_yield 関数で与えられたブロックを実行します。
そして NEWOBJ イベントを検知すると on_newobj_event コールバックで作成されたオブジェクトを配列に保存します。

ブロックの実行が完了すると、保存したオブジェクトの配列を返します。この際、Ruby 内部のオブジェクトやクラス情報を持たないオブジェクトを除外するようにしています。具体的には、キャッシュやメソッドの情報、インクルードしたモジュールの情報などを持つオブジェクトのことであり、こうしたオブジェクトは通常の Ruby API からは隠されています。

// track_objects_created.c

static VALUE track_objects_created(RB_UNUSED_VAR(VALUE _)) {
  VALUE result = rb_ary_new();
  VALUE tp = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_NEWOBJ, on_newobj_event, (void *) result);
  rb_tracepoint_enable(tp); rb_yield(Qnil); rb_tracepoint_disable(tp);
  return filter_hidden_objects(result);
}

static void on_newobj_event(VALUE tpval, void *data) {
  VALUE obj = rb_tracearg_object(rb_tracearg_from_tracepoint(tpval));
  if (!rb_objspace_internal_object_p(obj)) rb_ary_push((VALUE) data, obj);
}

VALUE filter_hidden_objects(VALUE result) {
  for (int i = 0; i < RARRAY_LEN(result); i++)
  if (!RBASIC_CLASS(rb_ary_entry(result, i))) rb_ary_store(result, i, Qnil);
  return result;
}

GCイベント

別の TracePoint API の利用例として、GC(Garbage Collection) 処理でのイベントを追跡する print_gc_timing が紹介されました。

このツールは、ブロック内で GC が作動した際にその処理にかかった時間を出力します。これにより、アプリケーションにおいて GC がどのように活動しているか、実行時間にどれほど影響を与えているのかを調査できます。

LowlevelToolkit.print_gc_timing do
  puts "Minor GC:"
  GC.start(full_mark: false)
  puts "Major GC:"
  GC.start(full_mark: true)
end

# => Minor GC:
# => GC worked for 0.78 ms
# => Major GC:
# => GC worked for 2.60 ms

このツールは、GCの開始(GC_ENTER) と GCの終了(GC_EXIT) を追跡する以下の C 拡張コードで実装されています。
GC_ENTERGC_EXIT イベントの発生した時刻を記録し、その差を計算して処理時間を出力します。

// print_gc_timing.c
VALUE print_gc_timing(VALUE _) {
  rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_GC_ENTER | RUBY_INTERNAL_EVENT_GC_EXIT, on_gc_event, NULL);
  … 
}

static void on_gc_event(VALUE tpval, RB_UNUSED_VAR(void *_)) {
  static uint64_t gc_start_time = 0;
  rb_event_flag_t event = rb_tracearg_event_flag(rb_tracearg_from_tracepoint(tpval));

  if (event == RUBY_INTERNAL_EVENT_GC_ENTER) {
    gc_start_time = get_monotonic_time_ns();
  } else if (event == RUBY_INTERNAL_EVENT_GC_EXIT) {
    fprintf(stdout, "GC worked for %.2f ms\n", ns_to_ms(get_monotonic_time_ns() - gc_start_time));
  }
}

注意点として、GC_ENTER の tracepoint のコールバック内でオブジェクトを作成すると、無限ループが発生してしまう恐れがあります。
新しいオブジェクトに割り当てる領域が無くなったために GC が作動する場合、GC_ENTER のコールバック内でさらにオブジェクトへの割り当てをしようとして、領域が足りないために再度 GC が起動されてしまうループになってしまいます。

また、NEWOBJ イベントや GC イベント の tracepoint のコールバック内では、ほとんどの Ruby API が利用できず、呼び出すとクラッシュしてしまうとのことでした。

Postponed Job API

Postponed Job APIは、Low-level API のコールバック内など、安全でないコンテキストから、後の安全なタイミングで実行する Ruby コードを登録しておくための API です。
安全なタイミングとは、GC中やオブジェクト割り当て中などではなく、GVL を保持していて Ruby コードを安全に実行できるタイミングのことです。

上で紹介された TracePoint API のコールバック内では Ruby API を呼び出すことができませんでしたが、 Postponed Job API を利用すれば Ruby の処理を実行できます。

Postponed Job API の利用例として、lowlevel-toolkit の on_gc_finish を見てみます。
このツールでは、指定した Ruby のコードブロックを GC の終了時に実行させることができます。

at_finish = -> do
  kind = GC.latest_gc_info[:major_by]
  puts "GC finished (#{kind ? "major (#{kind})" : "minor"})"
end

LowlevelToolkit.on_gc_finish(at_finish) do
  GC.start(full_mark: false)
  GC.start(full_mark: true)
end

# => GC finished (minor)
# => GC finished (major (force))

on_gc_finish の実装は以下のようになっています。
引数で渡されたジョブ関数を rb_postponed_job_preregister で登録し、GC 終了時のコールバックにて rb_postponed_job_trigger で実行を要求しています。
これにより、C の TracePoint コールバックから Ruby の処理を安全に実行できます。

// on_gc_finish.c
static VALUE callback = Qnil;
static rb_postponed_job_handle_t postponed_id = rb_postponed_job_preregister(0, postponed, NULL);

VALUE on_gc_finish(VALUE _, VALUE user_callback) {
  callback = user_callback;
  VALUE tp = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_GC_EXIT, on_gc_finish_event, NULL);
  … 
}

void on_gc_finish_event(VALUE _, void *__) {
  rb_postponed_job_trigger(postponed_id);
}

void postponed(void *_) { rb_funcall(callback, rb_intern("call"), 0); }

Postponed Job API の使用に際しても、いくつかの注意点が挙げられています。

  • ジョブ登録のフラグはスレッドローカルのため、スレッドを跨いで処理をトリガーできない
  • 登録できるジョブの数が最大32個という制限がある(うち1つはGCが利用している)
  • ジョブが実際に実行されるタイミングでは、ジョブが登録されたタイミングのメソッドとは異なるメソッドの中にいる可能性がある

Frame-profiling API

Frame-profiling API は、極めて軽量かつ安全にバックトレースフレームを取得する API です。

新しいオブジェクトの割り当てや Ruby コードの呼び出しを行わないため、オーバーヘッドが小さく、また他の Low-level API から呼び出すことができます。
そのためプロファイラでの利用に適しており、多くのプロファイラで使われています。

Frame-profiling API の利用例として、lowlevel-toolkit にてオブジェクト割り当て時のバックトレースを取得する例が紹介されました。
以下のように、last_allocation_at で最後にオブジェクトが割り当てられた場所のスタックトレースを取得できます。

def hello
  Object.new
  nil
end

LowlevelToolkit.track_last_allocation_at do
  hello
  pp LowlevelToolkit.last_allocation_at
end

# => [["examples/newobj_backtrace.rb", "hello"], ["examples/newobj_backtrace.rb", "block in <main>"], ["examples/newobj_backtrace.rb", "<main>"]]

last_allocation_at の実装は以下のようになっています。
track_last_allocation_at に渡した Ruby ブロック内で NEWOBJ イベントを検知すると、コールバック内で rb_profile_frames によりフレーム情報を取得し、stack 変数に保存します。
last_allocation_at が呼び出されると、保存していたフレーム情報を読み出し、 rb_profile_frame_path 関数と rb_profile_frame_label 関数でフレームのファイル名およびメソッド名を取得します。

// last_allocation_at.c
static VALUE stack = Qnil;

static void on_newobj_event(VALUE tpval, RB_UNUSED_VAR(void *_)) {
  if (stack == Qnil ||
    rb_objspace_internal_object_p(rb_tracearg_object(rb_tracearg_from_tracepoint(tpval)))) return;
  VALUE buffer[MAX_DEPTH]; // Temporary buffer for calling API
  int depth = rb_profile_frames(0, MAX_DEPTH, buffer, NULL);
  rb_ary_clear(stack);
  for (int i = 0; i < depth; i++) rb_ary_push(stack, buffer[i]);
}

static VALUE track_last_allocation_at(RB_UNUSED_VAR(VALUE _)) {
  stack = rb_ary_new_capa(MAX_DEPTH);
  VALUE tp = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_NEWOBJ, on_newobj_event, NULL);
  rb_tracepoint_enable(tp); rb_yield(Qnil); rb_tracepoint_disable(tp);
  return Qnil;
}

static VALUE last_allocation_at(RB_UNUSED_VAR(VALUE _)) {
  VALUE raw_stack = stack;
  stack = Qnil; // Pause recording of stacks while we're doing the copying below
  VALUE result = rb_ary_new();
  for (int i = 0; i < RARRAY_LEN(raw_stack); i++) {
    VALUE entry = rb_ary_entry(raw_stack, i);
    VALUE file = rb_profile_frame_path(entry);
    if (file != Qnil) rb_ary_push(result, rb_ary_new_from_args(2, file, rb_profile_frame_label(entry)));
  }
  stack = raw_stack; // Resume recording again
  return result;
}

Low-level API の利用を含む C 拡張コードを動かす際の注意点として、Ruby のオブジェクトを C のデータで保存する場合、参照していることを明示的に GC に伝える必要があります。
誤って GC が C のデータから参照されている Ruby オブジェクトを解放してしまうと、参照時に Segmentation Fault が発生してしまう恐れがあるためです。
last_allocation_at では、stack 変数に Ruby オブジェクトを保持していますが、以下のように stack 変数を Ruby のグローバル変数へ登録することで GC にマークされるようにしています。

// last_allocation_at.c
void init_last_allocation_at(VALUE lowlevel_toolkit_module) {
  rb_global_variable(&stack);
  rb_define_singleton_method(lowlevel_toolkit_module, "track_last_allocation_at", track_last_allocation_at, 0);
  rb_define_singleton_method(lowlevel_toolkit_module, "last_allocation_at", last_allocation_at, 0);
}

Debug inspector API

Debug inspector APIは、現在のバックトレースやメソッドを深く調査するための API です。

1つ前に紹介された Frame-profiling API と比べ、オブジェクトの生成や Ruby API の呼び出しを伴うためにパフォーマンスでは劣りますが、より詳細な情報を得ることができます。
現在のメソッドの呼び出し元となるオブジェクト、binding および命令シーケンスのオブジェクトにアクセスできるため、デバッガでの利用に適しています。

Debug inspector API の利用例として、呼び出し元のオブジェクトと binding を取得する例が紹介されました。
以下のコードと実行結果を見てみると、say_hello メソッドで呼ばれている hello! メソッドの中から、say_hello メソッドの呼び出し元オブジェクトおよび引数にアクセスできていることがわかります。

class Hello
  def say_hello(to, secret) = to.hello!
end

class Spy
  def hello!
    puts "I was called by #{LowlevelToolkit.who_called_me}"
    puts "Secret is '#{LowlevelToolkit.who_called_me_binding.local_variable_get(:secret)}'"
  end
end

Hello.new.say_hello(Spy.new, "trustno1")
# => I was called by #<Hello:0x00007ff298ae5898>
# => Secret is 'trustno1'

who_called_me の実装は以下のようになっています。
rb_debug_inspector_open 関数により、現在のスタックフレームのスナップショットが取得され callback 関数に渡されます。
callback 関数では、rb_debug_inspector_frame_self_get にて who_called_me の2つ上のフレームを指定して、呼び出し元のオブジェクトを取得しています。

// who_called_me.c
static VALUE callback(const rb_debug_inspector_t *dbg_context, void *return_self) {
  return (return_self != NULL) ?
    // Depth: 0 -> us; 1 -> caller; 2 -> caller of caller
    rb_debug_inspector_frame_self_get(dbg_context, 2) :
    rb_debug_inspector_frame_binding_get(dbg_context, 2);
}

static VALUE who_called_me(RB_UNUSED_VAR(VALUE _)) { return rb_debug_inspector_open(callback, (void *) !NULL); }
static VALUE who_called_me_binding(RB_UNUSED_VAR(VALUE _)) { return rb_debug_inspector_open(callback, NULL); }

GVL instrumentation API

Global VM Lock (GVL) とは、複数スレッドでの Ruby コードの並列実行を制限することで、CRuby VM の内部データを保護する機能です。
GVL により Ruby 内部の競合状態を防ぐことができますが、CPU 資源を活用できずアプリケーションのレイテンシに影響を及ぼす可能性があります。

GVL instrumentation API は、GVLの取得や解放などのイベントを追跡するための API です。
これを利用して、スレッドが GVL を取得して実行中なのか、 GVL の待機をしているのか、他の処理の終了を待機しているのかなどの活動状態を知ることができます。
これは、定常的な GVL の待機がアプリケーションのパフォーマンスに及ぼしている影響を調査するのに役立ちます。

GVL instrumentation API の利用例として、GVL待ち時間を計測する例を見てみましょう。
以下のコードでは、track_wants_gvl のブロック内で、単純な数え上げをループで行う2つのスレッドが実行されています。
track_wants_gvl はブロック内で実行したそれぞれのスレッドのGVL待ち時間を計算し、その合計時間を返します。

def counter_loop(counter = 0) = (counter += 1 while counter < 150_000_000)

before = Time.now

pp(LowlevelToolkit.track_wants_gvl do
  t1 = Thread.new { counter_loop }.tap { |it| it.name = "t1" }
  t2 = Thread.new { counter_loop }.tap { |it| it.name = "t2" }
  t1.join; t2.join
end.map { |thread, wants_gvl_ns| [thread, wants_gvl_ns / 1_000_000_000.0] })

puts "Total time: #{Time.now - before}"

# => [[#<Thread:0x00007520acd952c8@t1 examples/track_wants_gvl.rb:8 dead>, 1.825822156], [#<Thread:0x00007520acd94f30@t2 examples/track_wants_gvl.rb:9 dead>, 1.854341141]]
# => Total time: 3.715163175

track_wants_gvl は以下のように実装されています。

rb_internal_thread_add_event_hookでコールバックを登録し、

  • スレッドの開始(RUBY_INTERNAL_THREAD_EVENT_STARTED)
  • 実行可能状態への遷移(RUBY_INTERNAL_THREAD_EVENT_READY)
  • 実行の再開(RUBY_INTERNAL_THREAD_EVENT_RESUMED)

を監視します。
スレッドの開始イベントを検知すると、seen_threads 配列に開始したスレッドを登録します。

スレッドは実行可能状態へ遷移すると GVL が獲得できるまで待ち、GVLを取得すると実行を再開するため、実行可能状態への遷移から実行の再開までの時間が GVL の待ち時間となります。
on_thread_event コールバックでは、RUBY_INTERNAL_THREAD_EVENT_READY イベントを検知した時刻を rb_internal_thread_specific_set 関数でスレッド固有のストレージに記録しておき、RUBY_INTERNAL_THREAD_EVENT_RESUMED イベントを検知したタイミングで待ち時間を計算しています。

// track_wants_gvl.c
static void on_thread_event(rb_event_flag_t event_id, const rb_internal_thread_event_data_t *event_data, void *data) {
  VALUE thread = event_data->thread;
  if (event_id == RUBY_INTERNAL_THREAD_EVENT_STARTED) {
    // BUG: This is not thread safe... but works sometimes
    // TODO: Replace with safe data structure
    rb_ary_push((VALUE) data, thread);
  } else if (event_id == RUBY_INTERNAL_THREAD_EVENT_READY) {
    rb_internal_thread_specific_set(thread, wants_gvl_at_key, (void *) get_monotonic_time_ns());
  } else if (event_id == RUBY_INTERNAL_THREAD_EVENT_RESUMED) {
    uint64_t wants_gvl_at = (uint64_t) rb_internal_thread_specific_get(thread, wants_gvl_at_key);
    if (wants_gvl_at == 0) return;
    uint64_t total_wants = (uint64_t) rb_internal_thread_specific_get(thread, total_wants_key);
    rb_internal_thread_specific_set(
      thread, total_wants_key, (void *) (total_wants + (get_monotonic_time_ns() - wants_gvl_at)));
  }
}

VALUE track_wants_gvl(RB_UNUSED_VAR(VALUE _)) {
  VALUE seen_threads = rb_ary_new();
  rb_internal_thread_event_hook_t *hook =
    rb_internal_thread_add_event_hook(
      on_thread_event, RUBY_INTERNAL_THREAD_EVENT_STARTED | RUBY_INTERNAL_THREAD_EVENT_READY | RUBY_INTERNAL_THREAD_EVENT_RESUMED, (void *) seen_threads);
  rb_yield(Qnil);
  rb_internal_thread_remove_event_hook(hook);
  VALUE result = rb_hash_new();
  for (int i = 0; i < RARRAY_LEN(seen_threads); i++) {
    VALUE thread = rb_ary_entry(seen_threads, i);
    rb_hash_aset(result, thread, ULL2NUM((uint64_t) rb_internal_thread_specific_get(thread, total_wants_key)));
  }
  return result;
}

Release GVL profiler

以上の Low-level Observability API の解説を踏まえ、これらを組み合わせて KnuX さんの作成した Release GVL profiler が紹介されました。
このプロファイラは、たった107行の C コードで実装されており、アプリケーション内で GVL が明示的に解放される箇所を追跡します。

GVL が明示的に解放されるのは、Ruby がネットワーク呼び出しやファイル操作を行うとき、また C 拡張コードのライブラリで重い処理を行うときなどです。
これらの発生箇所を知ることで、プログラム中でスレッド切り替えが発生する可能性のある箇所を特定できます。

Release GVL profiler の実装の一部を見てみましょう。

VALUE release_gvl_profiler(int argc, VALUE *argv, RB_UNUSED_VAR(VALUE _)) {
  …

  VALUE result = rb_hash_new();
  rb_internal_thread_event_hook_t *hook = rb_internal_thread_add_event_hook(
    on_thread_event,
    RUBY_INTERNAL_THREAD_EVENT_SUSPENDED | RUBY_INTERNAL_THREAD_EVENT_RESUMED,
    (void *) result
  );
  rb_yield(Qnil);
  rb_internal_thread_remove_event_hook(hook);

  write_stacks(filename_prefix, result);
  return result;
}

void on_thread_event(event_id, const rb_internal_thread_event_data_t *event_data, void *data) {
  VALUE thread = event_data->thread;

  if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && !ruby_thread_has_gvl_p()) {
    rb_internal_thread_specific_set(thread, release_gvl_at_key, get_monotonic_time_ns());
  } else if (event_id == RUBY_INTERNAL_THREAD_EVENT_RESUMED) {
    uint64_t release_gvl_at = (uint64_t) rb_internal_thread_specific_get(thread, release_gvl_at_key);
    if (release_gvl_at == 0) return;
    rb_internal_thread_specific_set(thread, release_gvl_at_key, 0);

    VALUE frames = rb_make_backtrace();

    VALUE stats = rb_hash_aref((VALUE) data, frames);
    if (stats == Qnil) {
      stats = rb_ary_new_from_args(2, INT2FIX(0), INT2FIX(0));
      rb_hash_aset((VALUE) data, frames, stats);
    }

    uint64_t time_spent = get_monotonic_time_ns() - release_gvl_at;
    rb_ary_store(stats, 0, ULL2NUM(NUM2ULL(rb_ary_entry(stats, 0)) + time_spent)); // Time
    rb_ary_store(stats, 1, ULL2NUM(NUM2ULL(rb_ary_entry(stats, 1)) + 1)); // Counts
}}

void write_stacks(VALUE filename_prefix, VALUE result) {
  …
}

release_gvl_profiler では、GVL instrumentation API の rb_internal_thread_add_event_hook を用いて、スレッドの中断イベントおよびスレッドの実行再開イベントを監視するようにして、与えられた Ruby コードブロックを実行します。
Ruby コードの実行中にスレッドの中断イベントを検知すると、on_thread_event コールバックにおいて、ruby_thread_has_gvl_p で現在のスレッドが GVL を所有しているか、すなわち GVL を解放しているかが判定されます。GVL の解放が行われている場合のみ、現在時刻をスレッド固有ストレージに保存します。
スレッドの再開イベントを検知した場合、GVL解放のタイミングが記録されていれば経過時間を計算し、現在のフレームでの累計待機時間に加算します。

これにより、Ruby コードブロック内の処理ごとに、明示的なGVL解放の回数と待機時間を記録できます。

パフォーマンス問題とバグ

セッションの最後は、パフォーマンスの問題をバグと捉えるべきかどうかの話で締めくくられました。

一般的にパフォーマンス上の問題は、CPUやメモリ、時間などのリソースを使いすぎてしまうことによるものです。
しかし、例えばアプリケーションは正しい結果を返すが応答時間が長い場合、どれほど長ければパフォーマンスバグであるのかに明確な基準はありません。
これらの問題がバグかどうかは、リソースの使用量自体よりも本番環境での影響を考慮して判断するべきです。
本番環境でのユーザー体験の悪化、運用コストの増大、アクセス集中時のサーバーダウンリスクなどにつながる場合は、修正すべきバグと考えるべきでしょう。
こうした影響の調査にも、Observability API やプロファイラが役立ちます。
実影響が小さい場合は、パフォーマンスの改善に時間をとる必要はないこともあるため、アプリケーションの最適化をそもそもするべきかの判断を適切に行うことが大事です。

感想

実際のコードを見ながらの Low-level API の解説がとても面白かったです。
Ruby における C拡張コードやその動作をより身近に感じ、理解を深めることができました。
また、Ruby のパフォーマンス問題がどのように起こりうるのかの解像度が上がり、実際のアプリケーション開発に役立つ視点を得られたため、今後の開発に活かしていきたいと思います。


Techouseでは、社会課題の解決に一緒に取り組むエンジニアを募集しております。 ご応募お待ちしております。

jp.techouse.com