Bạn có muốn chuyến tham quan nhanh nội bộ Ruby không?
Thì bạn được thưởng thức.
Bởi vì...
Chúng ta sẽ cùng khám phá cách đối tượng Ruby được bố trí trong bộ nhớ và cách bạn có thể thao tác cấu trúc dữ liệu nội bộ để làm những điều thú vị.
Thắt an toàn và chuẩn bị cho hành trình vào sâu bộ thông dịch Ruby!
Bố cục Bộ nhớ của Mảng
Khi bạn tạo mảng, Ruby phải hỗ trợ nó với một chút bộ nhớ hệ thống và một chút siêu dữ liệu.
Siêu dữ liệu bao gồm:
- Kích thước mảng (số lượng mục)
- Dung lượng mảng
- Lớp
- Trạng thái đối tượng (đóng băng hay không)
- Con trỏ đến nơi dữ liệu được lưu trong bộ nhớ
Vì bộ thông dịch Ruby chính (MRI) được viết bằng C nên không có đối tượng.
Nhưng có thứ khác: structs.
Struct trong C giúp bạn lưu trữ dữ liệu liên quan cùng nhau, và điều này được dùng rất nhiều trong mã nguồn MRI để biểu diễn thứ như Array, String và các loại đối tượng khác.
Bằng cách nhìn vào một trong những struct đó, chúng ta có thể suy ra bố cục bộ nhớ của đối tượng.
Vì vậy hãy nhìn vào struct cho Array, gọi là 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; };
Tôi biết cái này có thể nhìn hơi đáng sợ nếu bạn không quen với C, nhưng đừng lo! Tôi sẽ giúp bạn phân tích thành các morsel dễ tiêu hóa 🙂
Điều đầu tiên chúng ta có là cái RBasic này, cũng là một struct:
struct RBasic { VALUE flags; VALUE klass; }
Đây là thứ hầu hết đối tượng Ruby có và nó chứa vài thứ như lớp cho đối tượng này và một vài cờ nhị phân nói đối tượng này có bị đóng băng hay không (và thứ khác như thuộc tính 'tainted').
Nói cách khác:
RBasic chứa siêu dữ liệu chung cho đối tượng.
Sau đó chúng ta có struct khác, chứa độ dài mảng (len).
Biểu thức union nói rằng aux có thể là capa (cho dung lượng) hoặc shared. Đây chủ yếu là chuyện tối ưu, được giải thích chi tiết hơn trong bài viết tuyệt vời của Pat Shaughnessy. Về mặt cấp phát bộ nhớ, trình biên dịch sẽ dùng kiểu lớn nhất bên trong union.
Sau đó chúng ta có ptr, chứa địa chỉ bộ nhớ nơi dữ liệu Array thực tế được lưu.
Đây là hình ảnh trông nó như thế nào (mỗi hộp trắng/xám là 4 byte trong hệ thống 32-bit):

Bạn có thể xem kích thước bộ nhớ của đối tượng dùng module ObjectSpace:
require 'objspace' ObjectSpace.memsize_of([]) # 20
Bây giờ chúng ta sẵn sàng vui một chút!
Fiddle: Thử nghiệm Thú vị
RBasic chính xác 8 byte trong hệ thống 32-bit và 16 byte trong hệ thống 64-bit. Biết điều này, chúng ta có thể dùng module Fiddle để truy cập byte bộ nhớ thô cho đối tượng và thay đổi chúng cho thử nghiệm thú vị.
Ví dụ:
Chúng ta có thể thay đổi trạng thái đóng băng bằng cách bật/tắt một bit đơn.
Đây về cơ bản là những gì phương thức freeze làm, nhưng lưu ý không có phương thức unfreeze.
Hãy triển khai nó chỉ cho vui!
Trước, hãy require module Fiddle (phần của Thư viện Chuẩn Ruby) và tạo chuỗi đóng băng.
require 'fiddle' str = 'water'.freeze str.frozen? # true
Tiếp theo:
Chúng ta cần địa chỉ bộ nhớ cho chuỗi của chúng ta, có thể lấy được như thế này.
memory_address = str.object_id * 2
Cuối cùng:
Chúng ta lật bit chính xác mà Ruby kiểm tra để xem đối tượng có bị đóng băng không. Chúng ta cũng kiểm tra xem điều này có hoạt động không bằng cách gọi phương thức frozen?.
Fiddle::Pointer.new(memory_address)[1] ^= 8 str.frozen? # false
Lưu ý chỉ số [1] tham chiếu đến byte thứ 2 của giá trị flags (được cấu thành từ 4 byte tổng cộng).
Sau đó chúng ta dùng ^= là toán tử "XOR" (OR loại trừ) để lật bit đó.
Chúng ta làm điều này vì các bit khác nhau bên trong flags có ý nghĩa khác nhau và chúng ta không muốn thay đổi thứ không liên quan.
Nếu bạn đọc bài viết ruby tricks của tôi, bạn có thể đã thấy điều này trước, nhưng bây giờ bạn biết nó hoạt động như thế nào 🙂
Điều khác bạn có thể thử là thay đổi độ dài mảng và in mảng.
Bạn sẽ thấy mảng trở nên ngắn hơn!
Bạn thậm chí có thể thay đổi lớp để làm cho Array nghĩ nó là String...
Kết luận
Bạn đã học một chút về cách Ruby hoạt động dưới capa. Cách bộ nhớ cho đối tượng Ruby được bố trí và cách bạn có thể dùng module Fiddle để chơi với nó.
Bạn có lẽ không nên dùng Fiddle như vậy trong ứng dụng thực, nhưng nó vui để thử nghiệm.
Đừng quên chia sẻ bài viết này để nhiều người hơn thấy nó 🙂