Chúng ta — Rubyists — rất yêu Hash. Nhưng Hash có vài nhược điểm nổi tiếng. Như Richard chỉ ra trong bài Hashie Considered Harmful, Hash đôi khi quá linh hoạt — một lỗi gõ nhẹ và bạn vô tình gán/truy cập khóa không bao giờ định dùng.
a = { type: "F150" }
a[:typo] # nil
Một số thay thế Hash phổ biến
Nếu dùng Hash lưu dữ liệu có cấu trúc, có thể quyết định không cần sự linh hoạt đó — nó chỉ dẫn đến rắc rối.
Có vài lựa chọn. Giả sử cần lưu cặp tọa độ XY. Cách một: định nghĩa class chỉ giữ cặp tọa độ.
class PointClass # không khuyên kết thúc tên class bằng "Class" :)
attr_accessor :x, :y
def initialize(args)
@x = args.fetch(:x)
@y = args.fetch(:y)
end
end
point_class = PointClass.new(x: 1, y: 2)
point_class.x # 1
Vì chỉ cần đóng gói dữ liệu, lựa chọn ngắn gọn hơn là `Struct`:
PointStruct = Struct.new(:x, :y)
point_struct = PointStruct.new(1, 2)
point_struct.x # 1
Lựa chọn thứ ba: `OpenStruct`. Trông như Struct nhưng cho phép gán giá trị tùy ý như Hash:
point_os = OpenStruct.new(x: 1, y: 2)
point_os.x # 1
Hệ quả về hiệu năng
[CẬP NHẬT 10/07/2015: Script benchmark của tôi có vẻ bất công với Hash. Như Patrick Helm chỉ ra, tôi dùng cách khởi tạo Hash kém hiệu quả. Vui lòng bỏ qua kết quả Hash. Dù điểm chính về OpenStruct siêu chậm vẫn đúng. Xem sửa đổi của ông ấy tại đây]
Nhìn 4 lựa chọn, tôi tò mò về hệ quả hiệu năng. Rõ ràng mọi lựa chọn đều đủ nhanh nếu dữ liệu ít. Nhưng với hàng nghìn hoặc triệu item, chênh lệch Hash vs OpenStruct vs Struct vs class bắt đầu quan trọng.
Tại Honeybadger, hàng nghìn exception được báo cáo mỗi giây, nên hiểu hiệu năng luôn là ưu tiên.
Tôi viết script benchmark đơn giản, dùng gem `benchmark-ips` vì tự xác định sample size tốt và báo standard deviation.
Khởi tạo (Initialization)
Benchmark thời gian khởi tạo PointClass, PointStruct, Hash, OpenStruct cho thấy PointClass và PointStruct thắng tuyệt đối. Chúng nhanh gấp ~10 lần OpenStruct, và ~2 lần Hash.
PointClass và PointStruct nhanh gần 10x OpenStruct.
Kết quả hợp lý: Struct đơn giản nhất nên nhanh nhất. OpenStruct phức tạp nhất (wrapper cho Hash) nên chậm nhất. Mức độ chênh lệch khá ngạc nhiên.
Sau thí nghiệm, tôi rất ngại dùng OpenStruct ở code quan tâm tốc độ. Sẽ coi chừng các Hash trong code performance-critical.
Đọc / Ghi (Read / Write)
Khác với khởi tạo, 4 lựa chọn gần như ngang nhau khi set/get giá trị.
Benchmark đọc/ghi cho thấy không chênh lệch lớn giữa Struct, class, hash và OpenStruct.
Script Benchmarking
Muốn chạy benchmark trên máy mình, dùng script dưới. Tôi chạy trên MRI 2.1, macOS. Tò mò về hiệu năng trên interpreter khác, Michael Cohen có gist tuyệt vời với kết quả MRI 2.2, JRuby và khác.
require 'benchmark/ips'
require 'ostruct'
data = { x: 100, y: 200 }
PointStruct = Struct.new(:x, :y)
class PointClass
attr_accessor :x, :y
def initialize(args)
@x = args.fetch(:x)
@y = args.fetch(:y)
end
end
puts "\n\nINITIALIZATION =========="
Benchmark.ips do |x|
x.report("PointStruct") { PointStruct.new(100, 200) }
x.report("PointClass") { PointClass.new(data) }
x.report("Hash") { Hash.new.merge(data) }
x.report("OpenStruct") { OpenStruct.new(data) }
end
puts "\n\nREAD =========="
point_struct = PointStruct.new(100, 200)
point_class = PointClass.new(data)
point_hash = Hash.new.merge(data)
point_open_struct = OpenStruct.new(data)
Benchmark.ips do |x|
x.report("PointStruct") { point_struct.x }
x.report("PointClass") { point_class.x }
x.report("Hash") { point_hash.fetch(:x) }
x.report("OpenStruct") { point_open_struct.x }
end
puts "\n\nWRITE =========="
Benchmark.ips do |x|
x.report("PointStruct") { point_struct.x = 1 }
x.report("PointClass") { point_class.x = 1 }
x.report("Hash") { point_hash[:x] = 1 }
x.report("OpenStruct") { point_open_struct.x = 1 }
end