+1

Cách để giữ MEMORY USAGE LOW trong Ruby

Khi lập trình ở Ruby, nhiều người nghĩ rằng sử dụng bộ nhớ quá mức là tiêu chuẩn và không thể tránh khỏi. Tuy nhiên, có nhiều cách và chiến lược để giữ cho bộ nhớ sử dụng xuống và trong bài đăng này tôi sẽ chỉ cho bạn một số trong số họ.

GIỮ CÁC INTERNALS CỦA RUBY TRONG TÂM TRÍ

Các lớp được xây dựng chính của Ruby như TrueClass , FalseClass , NilClass , Integer , Float , Symbol , String , Array , Hash và Struct được tối ưu hóa cao về hiệu suất thực hiện và sử dụng bộ nhớ. Lưu ý rằng tôi đang nói về CRuby (MRI) ở đây và do đó hầu hết mọi thứ có thể sẽ không áp dụng cho các triển khai Ruby khác.

Bên trong, tức là trong mã C của nó, mỗi đối tượng trong Ruby được tham chiếu qua loại VALUE . Đây là một con trỏ tới cấu trúc C chứa tất cả các thông tin cần thiết.

Tất cả các số dưới đây có giá trị cho nền tảng Linux 64-bit nhưng nên áp dụng cho bất kỳ hệ thống 64-bit nào khác.

NIL , TRUE , FALSE VÀ SOME INTEGERS

Một số lớp học không cần phải cấp phát bộ nhớ cho cấu trúc C khi tạo một đối tượng vì các đối tượng có thể được trực tiếp đại diện bởi một VALUE . Đây là trường hợp đối với các đối tượng thuộc loại NilClass (tức là giá trị TrueClass ), gõ TrueClass (tức là giá trị true ) và nhập FalseClass (tức là giá trị false ).

Các số nguyên nhỏ trong khoảng -2 ^ 62 đến 2 ^ 62-1 cũng được biểu diễn trực tiếp dưới dạng VALUE .

Điều đó có nghĩa là gì? Nó có nghĩa là chỉ có bộ nhớ tối thiểu là cần thiết để đại diện cho những đối tượng này. Và bạn không cần suy nghĩ về việc sử dụng bộ nhớ khi sử dụng các giá trị như vậy.

Chúng ta có thể kiểm tra điều này bằng cách sử dụng phương thức ObjectSpace.memsize_of trả về bộ nhớ được sử dụng bởi một đối tượng:

2.4.2 > require 'objspace'
 => true
2.4.2 > ObjectSpace.memsize_of(nil)
 => 0
2.4.2 > ObjectSpace.memsize_of(true)
 => 0
2.4.2 > ObjectSpace.memsize_of(false)
 => 0
2.4.2 > ObjectSpace.memsize_of(2**62-1)
 => 0
2.4.2 > ObjectSpace.memsize_of(2**62)
 => 40

Như bạn thấy không có bộ nhớ bổ sung được sử dụng, ngoại trừ trường hợp cuối cùng vì số nguyên quá lớn. Khi một cấu trúc VALUE là cần thiết, một đối tượng sử dụng ít nhất 40 byte bộ nhớ.

ARRAYS, STRUCTS, HASHES AND STRINGS

Đối tượng của bốn lớp này sử dụng cấu trúc C đặc biệt thay vì cấu trúc chung. Các cấu trúc này cho phép lưu trữ một số giá trị trực tiếp bên trong thay vì cấp phát bộ nhớ thêm.

Mảng với tối đa ba yếu tố là bộ nhớ hiệu quả . Sau đó mỗi phần tử mới cần thêm 8 byte:

2.4.2 > ObjectSpace.memsize_of([])
 => 40
2.4.2 > ObjectSpace.memsize_of([1])
 => 40
2.4.2 > ObjectSpace.memsize_of([1, 2])
 => 40
2.4.2 > ObjectSpace.memsize_of([1, 2, 3])
 => 40
2.4.2 > ObjectSpace.memsize_of([1, 2, 3, 4])
 => 72

Điều này cũng áp dụng cho cấu trúc với tối đa ba thành viên, tức là những cấu trúc chỉ cần 40 byte bộ nhớ:

2.4.2 > X = Struct.new(:a, :b, :c)
 => X
2.4.2 > Y = Struct.new(:a, :b, :c, :d)
 => Y
2.4.2 > ObjectSpace.memsize_of(X.new)
 => 40
2.4.2 > ObjectSpace.memsize_of(Y.new)
 => 72

Nó là một chút khác nhau với băm nhưng điều quan trọng nhất là băm mà không có yếu tố chỉ cần 40 byte tối thiểu (do đó không có hình phạt lớn ở đó, ví dụ như cho các giá trị mặc định):

2.4.2 :044 > ObjectSpace.memsize_of({})
 => 40
2.4.2 :045 > ObjectSpace.memsize_of({a: 1})
 => 192
2.4.2 :046 > ObjectSpace.memsize_of({a: 1, b: 2, c: 3, d: 4})
 => 192
2.4.2 :047 > ObjectSpace.memsize_of({a: 1, b: 2, c: 3, d: 4, e: 5})
 => 288

Bạn cũng có thể thấy rằng một băm với tối đa bốn mục sử dụng 192 byte, vì vậy đây là mức tối thiểu mà bạn cần cho băm không rỗng.

Cuối cùng, các chuỗi có tới 23 byte được lưu trữ trực tiếp trong cấu trúc RString đại diện cho một đối tượng chuỗi:

2.4.2 :062 > ObjectSpace.memsize_of("")
 => 40
2.4.2 :063 > ObjectSpace.memsize_of("a"*23)
 => 40
2.4.2 :064 > ObjectSpace.memsize_of("a"*24)
 => 65

Kiến thức này giúp bạn như thế nào? Tôi không khuyên bạn nên thiết kế hoàn toàn xung quanh những khó khăn này nhưng chúng có thể ảnh hưởng đến quyết định của bạn khi bạn cần lựa chọn giữa các hiện thực thay thế.

YOUR EVERYDAY OBJECT

Tất cả các đối tượng "bình thường", tức là các đối tượng không có cấu trúc C đặc biệt, sử dụng cấu trúc RObject chung. Bạn có thể nghĩ rằng điều này sẽ không cho phép bạn có ý thức bộ nhớ nhưng bạn là sai. Ngay cả cấu trúc này có chế độ "bộ nhớ hiệu quả".

Nếu bạn có một mảng bộ nhớ được sử dụng bởi mảng này là để lưu trữ ( VALUE con trỏ tới) mục nhập của nó. Tương tự như vậy, nếu bạn có một chuỗi nó sử dụng bộ nhớ để lưu trữ các byte tạo nên chuỗi. Vì vậy, cho mục đích gì là bộ nhớ được sử dụng trong trường hợp của một đối tượng chung? Ví dụ biến!

Các giá trị cho các biến dụ được lưu trữ bởi đối tượng, tuy nhiên, tên của các biến dụ được lưu trữ bởi các đối tượng lớp liên quan (bởi vì bình thường các đối tượng của một lớp có cùng một biến dụ).

Giống như các mảng một đối tượng với tối đa ba biến dụ chỉ sử dụng 40 byte , một trong bốn hoặc năm sử dụng 80 byte:

2.4.2 > class X; def initialize(c); c.times {|i| instance_variable_set(:"@i#{i}", i)}; end; end
 => :initialize
2.4.2 :064 > ObjectSpace.memsize_of(X.new(0))
 => 40
2.4.2 :065 > ObjectSpace.memsize_of(X.new(1))
 => 40
2.4.2 :066 > ObjectSpace.memsize_of(X.new(2))
 => 40
2.4.2 :067 > ObjectSpace.memsize_of(X.new(3))
 => 40
2.4.2 :068 > ObjectSpace.memsize_of(X.new(4))
 => 80
2.4.2 :069 > ObjectSpace.memsize_of(X.new(5))
 => 80
2.4.2 :070 > ObjectSpace.memsize_of(X.new(6))
 => 96

CHIẾN LƯỢC

THIẾT KẾ LỚP VÀ HỆ THỐNG

Khi phát triển ứng dụng / thư viện không cần tạo ra nhiều đối tượng, bạn không thực sự cần phải có ý thức về bộ nhớ. Tuy nhiên, nếu họ cần tạo ra nhiều đối tượng, sẽ rất tốt nếu bạn giữ các thông tin ở phần sau của tâm trí khi thiết kế các lớp và tương tác.

Xem xét ví dụ này: Bạn cần tạo một lớp có thể đại diện cho các giá trị lề CSS. Theo đặc tả CSS , cho phép một đến bốn giá trị. Bạn sẽ làm điều này như thế nào?

  1. Một ý tưởng có thể là chỉ cần sử dụng một mảng. Đây không phải là một trừu tượng tốt nhưng trí nhớ mảng sẽ sử dụng hoặc là 40 byte hoặc, với bốn giá trị, 72 byte.
  2. Tuy nhiên, vì hầu hết các phương pháp mảng không thực sự áp dụng, mảng nên được gói bên trong một lớp. Đối tượng của lớp này sẽ sử dụng 80 hoặc 112 byte, tùy thuộc vào kích thước của mảng.
  3. Một khả năng khác là tạo ra một lớp và lưu trữ bốn giá trị trong các biến ví dụ khi khởi tạo. Các đối tượng sau đó sẽ luôn luôn sử dụng 80 byte.
  4. Cuối cùng, thay vì một lớp, một struct với bốn thành viên có thể được sử dụng. Đối tượng sẽ chỉ sử dụng 72 byte.

Ví dụ này có thể được tìm hiểu rộng rãi nhưng nó minh hoạ một cách độc đáo hai điểm: Đầu tiên sử dụng các loại được xây dựng thường là cách tốt nhất để bảo tồn bộ nhớ, với chi phí có một sự trừu tượng tốt. Và thứ hai là có sự chú ý của Ruby khi thiết kế các lớp học có thể làm giảm việc sử dụng bộ nhớ (ví dụ trong trường hợp ví dụ sử dụng một cấu trúc trên một lớp đơn giản tiết kiệm 10% cho mỗi đối tượng).

ĐỐI TƯỢNG SỬ DỤNG LẠI

Một cách khác để bảo tồn bộ nhớ là tái sử dụng các đối tượng khi có thể. Điều này được thực hiện dễ dàng trong trường hợp các vật không thay đổi, nhưng cũng có thể được áp dụng trong các trường hợp khác.

Một ví dụ điển hình cho việc tái sử dụng đối tượng sẽ là một trình soạn thảo văn bản đồ họa. Trình soạn thảo văn bản cần phải có thông tin về mỗi hình ảnh đại diện của một nhân vật (một glyph) có sẵn. Bằng cách lưu trữ và sử dụng lại thông tin glyph chỉ cần một mẫu cho mỗi glyph, ngay cả khi được tham chiếu từ nhiều vị trí.

Một ví dụ khác là đóng băng và dập tắt các dây. Điều này có thể được thực hiện theo từng trường hợp hoặc trên toàn cầu đối với tệp nguồn Ruby thông qua pragma "frozen_string_literal: true". Điều này cho phép thông dịch để dồn các chuỗi, giảm việc sử dụng bộ nhớ. Bắt đầu với Ruby 2.5 bạn cũng có thể tự giải nén bất kỳ chuỗi nào bằng cách sử dụng kết quả của phương pháp String#-@ , ví dụ -str .

SỬ DỤNG PHƯƠNG PHÁP VÀ THUẬT TOÁN PHÙ HỢP Tiết kiệm bộ nhớ tốt nhất đến từ việc không phân bổ thêm các đối tượng nào cả . Ví dụ: nếu bạn có một mảng và cần ánh xạ từng giá trị, bạn có thể sử dụng Array#map Array#map! hoặc Array#map Array#map! . Sự khác biệt là người đầu tiên tạo ra một mảng mới trong khi thứ hai sửa đổi mảng tại chỗ. Nó thường có thể sử dụng phương pháp thứ hai mà không có bất kỳ thay đổi mã khác. Vì vậy, nếu bạn có một hotspot sử dụng một phương pháp chuyển đổi như Array#map , hãy nghĩ rằng nếu bạn có thể sử dụng một phương pháp hiệu quả khác, hiệu quả hơn.

Việc chọn các thuật toán thích hợp cũng có thể làm giảm đáng kể việc sử dụng bộ nhớ. Ví dụ, khi sửa đổi một tập tin PDF được mật mã trong HexaPDF, có những tình huống mà giải mã và mã hóa lại các luồng dữ liệu là không cần thiết. Bằng cách xác định các tình huống này, có thể giảm sử dụng bộ nhớ và tăng tốc độ xử lý bằng cách chỉ sao chép luồng dữ liệu đầu vào thẳng vào tệp xuất. Điều này dẫn đến HexaPDF sử dụng bộ nhớ ít hơn một thư viện C ++ khi tối ưu hóa các tệp được mã hóa.

ĐO SỬ DỤNG BỘ NHỚ

Có một số đá quý giúp xác định nơi mà một chương trình phân bổ bộ nhớ. Hai mà tôi thường xuyên sử dụng là alloc_tracer và memory_profiler .

Cả hai công cụ đều có thể đo được toàn bộ chương trình hoặc có thể bật và tắt để chỉ đo các phần nhất định của chương trình. Hoặc một phương pháp cho phép bạn xác định các điểm nóng trong chương trình của bạn và sau đó hành động trên thông tin. Ví dụ, trong khi phát triển kramdown một vài năm trước đây tôi thấy rằng các lớp chuyển đổi HTML phân bổ một lượng lớn các chuỗi ném đi. Bằng cách thay đổi hotspot này thành kramdown thay thế tốt hơn sẽ nhanh hơn và ít sử dụng bộ nhớ hơn.

Để bắt đầu sử dụng hai loại đá quý này, dưới đây là hai tệp tin được dự định để tải trước bằng cách sử dụng chuyển đổi -r của tệp nhị phân ruby ​​(ví dụ: ruby -I. -ralloc_tracer myscript.rb ).

BEGIN {
  require 'allocation_tracer'
  ObjectSpace::AllocationTracer.setup(%i{path line type})
  ObjectSpace::AllocationTracer.trace
}

END {
  require 'pp'
  results = ObjectSpace::AllocationTracer.stop
  results.reject {|k, v| v[0] < 10}.sort_by{|k, v| [v[0], k[0]]}.each do |k, v|
    puts "#{k[0]}:#{k[1]} - #{k[2]} - #{v[0]}"
  end
  puts "Sum: " + results.inject(0) {|sum, (k,v)| sum + v[0]}.to_s
  pp ObjectSpace::AllocationTracer.allocated_count_table
  p
BEGIN {
  require 'memory_profiler'
  MemoryProfiler.start
}

END {
  report = MemoryProfiler.stop
  report.pretty_print
}

Hai tệp này cho phép bạn ghi lại mức sử dụng bộ nhớ mà không cần thay đổi chương trình.

PHẦN KẾT LUẬN

Có nhiều cách khác nhau để giảm thiểu sử dụng bộ nhớ của Ruby khi phát triển thư viện và ứng dụng. Biết được một chút thông dịch viên Ruby bên trong giúp hiểu rõ cách Ruby sử dụng trí nhớ và cách chúng ta có thể khai thác sự thật đó. Ngoài ra, biết hiệu suất và ảnh hưởng bộ nhớ của các phương pháp cốt lõi của Ruby giúp lựa chọn phương pháp phù hợp.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí