Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyの内部構造に迫る:オブジェクトのメモリレイアウトを徹底解説

Rubyの内部構造をちょっと覗いてみませんか?

それなら、この記事はきっとお役に立ちます。

なぜなら

この記事では、Rubyオブジェクトがメモリ上でどのように配置されているのか、そして内部データ構造を操作してクールなことを実現する方法を、一緒に探検していきます。

シートベルトを締めて、Rubyインタプリタの深淵への旅に出かけましょう!

配列(Array)のメモリレイアウト

配列を作成すると、Rubyはそのデータを保持するためにシステムメモリと、少しのメタデータを確保します。

メタデータには以下が含まれます

  • 配列のサイズ(要素数)
  • 配列の容量(capacity)
  • クラス情報
  • オブジェクトの状態(freezeされているかどうか)
  • 実際のデータが格納されているメモリ位置へのポインタ

主要なRubyインタプリタであるMRI(Matz's Ruby Interpreter)はC言語で書かれているため、C言語には「オブジェクト」という概念が存在しません。

しかし、代わりにあるものがあります。それは構造体(struct)です。

C言語におけるstructは、関連するデータをひとまとめにして格納するためのもので、MRIのソースコードではArrayStringなど、さまざまな種類のオブジェクトを表現するために多用されています。

これらのstructを観察することで、オブジェクトのメモリレイアウトを推測することができます。

それでは、Array用のstructであるRArrayを見てみましょう:

struct RArray {
  struct RBasic basic;

  union {
    struct {
      long len;

      union {
        long capa;
        VALUE shared;
      } aux;

      const VALUE *ptr;
    } heap;

    const VALUE ary[RARRAY_EMBED_LEN_MAX];
  } as;
};

C言語に馴染みがない方には、少し難しく見えるかもしれませんが、ご安心ください!わかりやすい部分に分解して解説します 🙂

最初に登場するのはRBasicというものです。これもまたstructです:

struct RBasic {
  VALUE flags;
  VALUE klass;
}

これはほとんどすべてのRubyオブジェクトが持っているもので、このオブジェクトのクラス情報や、オブジェクトがfreezeされているかどうかを示すバイナリフラグ(その他、かつての「tainted」属性なども)が含まれています。

つまり

RBasicには、オブジェクト共通の汎用的なメタデータが格納されているのです。

その次には、別のstructがあり、ここには配列の長さ(len)が含まれています。

unionという記述は、auxcapa(容量)またはsharedのどちらか一方になりうることを意味しています。これは主に最適化のためのもので、Pat Shaughnessy氏の優れた記事でより詳しく解説されています。メモリ割り当ての観点では、コンパイラはunion内で最大の型を使用します。

さらにptrというフィールドがあり、ここには実際のArrayデータが格納されているメモリアドレスが入っています。

この様子を図にすると次のようになります(白・グレーの各ボックスは32ビットシステムでの4バイトを表しています):

Rubyの内部構造に迫る:オブジェクトのメモリレイアウトを徹底解説

ObjectSpaceモジュールを使えば、オブジェクトのメモリサイズを確認することもできます:

require 'objspace'

ObjectSpace.memsize_of([])
# 20

これで準備万端。さあ、楽しい実験を始めましょう!

Fiddleを使った楽しい実験

RBasicは32ビットシステムではちょうど8バイト、64ビットシステムでは16バイトです。このことを知っていれば、Fiddleモジュールを使ってオブジェクトの生のメモリバイトにアクセスし、それを書き換えて楽しい実験を行うことができます。

たとえば

1ビットを切り替えるだけで、freeze状態を変更することができます。

本質的には、これこそがfreezeメソッドが行っていることです。しかし、よく考えてみると「unfreeze」メソッドは存在しませんよね。

そこで、遊び半分で実装してみましょう!

まず、Ruby標準ライブラリの一部であるFiddleモジュールをrequireし、freezeされた文字列を作成します。

require 'fiddle'

str = 'water'.freeze
str.frozen?
# true

次に:

文字列のメモリアドレスが必要です。これは次のようにして取得できます。

memory_address = str.object_id * 2

最後に:

Rubyがオブジェクトがfreezeされているかどうかを判定する際にチェックする、正確なビットを反転させます。frozen?メソッドを呼び出して、うまくいったかどうかも確認しましょう。

Fiddle::Pointer.new(memory_address)[1] ^= 8

str.frozen?
# false

インデックス[1]は、flags値(合計4バイトで構成される)の2バイト目を指していることに注意してください。

そして^=は「XOR」(排他的論理和)演算子で、これにより該当ビットを反転させています。

こうする理由は、flags内の異なるビットにはそれぞれ異なる意味があるためで、無関係な部分を変更してしまうのを避けたいからです。

以前の「Rubyトリック」の記事を読んだことがある方なら、このテクニックを見たことがあるかもしれません。でも今、あなたはその仕組みを理解したのです 🙂

ほかにも、配列の長さを変更して配列を出力してみるのも面白いでしょう。

配列が短くなる様子を目撃できますよ!

さらに、クラス情報を書き換えて、Arrayに自分がStringだと信じ込ませることさえ可能です…

まとめ

今回は、Rubyが内部でどのように動作しているのか、Rubyオブジェクトのメモリがどのように配置されているのか、そしてFiddleモジュールを使ってそれをどう操作できるのかについて学びました。

実際のアプリケーションでこのような使い方でFiddleを使うべきではありませんが、実験として試すのはとても楽しいものです。

より多くの人に見てもらえるよう、ぜひこの記事をシェアしてくださいね 🙂

  1. 【Ruby】デコレータデザインパターンでクラスを変更せずにオブジェクトを拡張する方法

    デコレータ(Decorator)デザインパターンとは、どのようなものなのでしょうか? また、このパターンをRubyのプロジェクトではどのように活用すればよいのでしょうか? デコレータデザインパターンを利用すると、クラスそのものを変更することなく、オブジェクトに新しい機能を追加して拡張することができます。 それでは、具体的な例を見ていきましょう! ログ出力とパフォーマンス計測の実装例 この例では、rest-clientのようなGemを使ってHTTPリクエストを送信します。 コードは次のようになります。 require restclient data = RestClient.get(www.

  2. TCMallocを使ってRubyのメモリ割り当てをプロファイリングする方法

    Rubyでは、メモリの割り当てはどのように行われているのでしょうか? Rubyは「ページ(page)」と呼ばれる塊単位でメモリを確保し、新しく生成されたオブジェクトはこのページの中に格納されていきます。 しかし、 ページがいっぱいになると、さらなるメモリが必要になります。 そのときRubyは、malloc関数を使ってオペレーティングシステム(OS)に追加のメモリを要求します。 このmalloc関数はOS標準で提供されているものですが、代替となる実装に差し替えることも可能です。 その代表的な実装のひとつが、Google製のTCMallocです。 TCMallocは「gperftools(Goog