Một số trick cải thiện performance trong Ruby
Khi phát triển các ứng dụng với bất kì một ngôn ngữ nào, đặc biệt là với các ứng dụng lớn, với số lượng dữ liệu và các thao tác lớn thì vấn đề cải thiện performance cho những dòng code của bạn là việc hết sức quan trọng. Ruby cũng không phải là ngoại lê. Trong trang Viblo cũng có rất nhiều bài viết ...
Khi phát triển các ứng dụng với bất kì một ngôn ngữ nào, đặc biệt là với các ứng dụng lớn, với số lượng dữ liệu và các thao tác lớn thì vấn đề cải thiện performance cho những dòng code của bạn là việc hết sức quan trọng. Ruby cũng không phải là ngoại lê. Trong trang Viblo cũng có rất nhiều bài viết hay về chủ đề này. Bài viết này mình xin điểm qua một số tip giúp cải thiện performance trong Ruby mà có thể bạn ít để ý
Hãy cùng tìm hiểu qua ví dụ sau:
require 'benchmark' class Obj def with_condition respond_to?(:mythical_method) ? self.mythical_method : nil end def with_rescue self.mythical_method rescue NoMethodError nil end end obj = Obj.new N = 10_000_000 puts RUBY_DESCRIPTION Benchmark.bm(15, "rescue/condition") do |x| rescue_report = x.report("rescue:") { N.times { obj.with_rescue } } condition_report = x.report("condition:") { N.times { obj.with_condition } } [rescue_report / condition_report] end
Và đây là kết quả: Với Ruby 1.9.3:
ruby 1.9.3p194 (2012-04-20 revision 35410) [x86_64-linux] user system total real rescue: 111.530000 2.650000 114.180000 (115.837103) condition: 2.620000 0.010000 2.630000 ( 2.633154) rescue/condition: 42.568702 265.000000 NaN ( 43.991767)
Với Ruby 1.8.3 cũng cho kết quả tương tự
ruby 1.8.7 (2011-12-28 patchlevel 357) [x86_64-linux] user system total real rescue: 80.510000 0.940000 81.450000 ( 81.529022) if: 3.320000 0.000000 3.320000 ( 3.330166) rescue/condition: 24.250000 inf -nan ( 24.481970)
Các bạn có thể thấy một sự khác biệt lớn ở trên.
Tránh việc sử dụng method += để nối xâu mà hãy dùng method << . Kết quả nhận được chắc chắn sẽ như nhau, thêm một string vào cuối một string đang tồn tại để tạo thành 1 string mới. Nhưng sự khác biệt ở đây là gì? Hãy cùng xem ví dụ sau:
str1 = "str1" str2 = "str2" str1.object_id # => 16241320 str1 += str2 # str1 = str1 + str2 str1.object_id # => 16241240, id is changed str1 << str2 str1.object_id # => 16241240, id is the same
Khi bạn sử dụng +=, mặc định Ruby sẽ tạo ra một đối tượng tạm thời nhằm lưu kết quả của str1 + str2, sau đó nó sẽ override lại biến str1 với tham chiếu đến đối tượng tạm thời này. Trong khi đó, method << lại thay đổi giá trị trực tiếp từ object str1 đang tồn tại. Với việc sử dụng +=, sẽ có các hạn chế sau:
- Thêm tính toán để nối chuỗi
- Cần thêm memory để lưu trữ đối tượng tạm thời
+= chậm như nào, nó phụ thuộc vào độ dài string mà bạn đem nối:
require 'benchmark' N = 1000 BASIC_LENGTH = 10 5.times do |factor| length = BASIC_LENGTH * (10 ** factor) puts "_" * 60 + " LENGTH: #{length}" Benchmark.bm(10, '+= VS <<') do |x| concat_report = x.report("+=") do str1 = "" str2 = "s" * length N.times { str1 += str2 } end modify_report = x.report("<<") do str1 = "s" str2 = "s" * length N.times { str1 << str2 } end [concat_report / modify_report] end end
Và kết qủa là:
____________________________________________________________ LENGTH: 10 user system total real += 0.000000 0.000000 0.000000 ( 0.004671) << 0.000000 0.000000 0.000000 ( 0.000176) += VS << NaN NaN NaN ( 26.508796) ____________________________________________________________ LENGTH: 100 user system total real += 0.020000 0.000000 0.020000 ( 0.022995) << 0.000000 0.000000 0.000000 ( 0.000226) += VS << Inf NaN NaN (101.845829) ____________________________________________________________ LENGTH: 1000 user system total real += 0.270000 0.120000 0.390000 ( 0.390888) << 0.000000 0.000000 0.000000 ( 0.001730) += VS << Inf Inf NaN (225.920077) ____________________________________________________________ LENGTH: 10000 user system total real += 3.660000 1.570000 5.230000 ( 5.233861) << 0.000000 0.010000 0.010000 ( 0.015099) += VS << Inf 157.000000 NaN (346.629692) ____________________________________________________________ LENGTH: 100000 user system total real += 31.270000 16.990000 48.260000 ( 48.328511) << 0.050000 0.050000 0.100000 ( 0.105993) += VS << 625.400000 339.800000 NaN (455.961373)
Giả sử bạn cần viết một method để convert một mảng vào trong một hash, với giá trị key và value chính là các element trong mảng:
func([1, 2, 3]) # => {1 => 1, 2 => 2, 3 => 3}
Giải phap sau sẽ cho ta kết quả mong muốn:
def func(array) array.inject({}) { |h, e| h.merge(e => e) } end
Với giải thuật trên, chương trình sẽ cực kì chậm với ở trên một dữ liệu lớn, vì nó chứa các method inject và merge lồng nhau, nó có O(n2). Nhưng rõ ràng là nó phải là O(n). Hãy xem tiếp:
def func(array) array.inject({}) { |h, e| h[e] = e; h } end
Trong trường hợp này, chúng ta chỉ có 1 vòng lặp và không có bất kì một phép tính nào trong vòng lặp
require 'benchmark' def n_func(array) array.inject({}) { |h, e| h[e] = e; h } end def n2_func(array) array.inject({}) { |h, e| h.merge(e => e) } end BASE_SIZE = 10 4.times do |factor| size = BASE_SIZE * (10 ** factor) params = (0..size).to_a puts "_" * 60 + " SIZE: #{size}" Benchmark.bm(10) do |x| x.report("O(n)" ) { n_func(params) } x.report("O(n2)") { n2_func(params) } end end
Và kết quả là:
____________________________________________________________ SIZE: 10 user system total real O(n) 0.000000 0.000000 0.000000 ( 0.000014) O(n2) 0.000000 0.000000 0.000000 ( 0.000033) ____________________________________________________________ SIZE: 100 user system total real O(n) 0.000000 0.000000 0.000000 ( 0.000043) O(n2) 0.000000 0.000000 0.000000 ( 0.001070) ____________________________________________________________ SIZE: 1000 user system total real O(n) 0.000000 0.000000 0.000000 ( 0.000347) O(n2) 0.130000 0.000000 0.130000 ( 0.127638) ____________________________________________________________ SIZE: 10000 user system total real O(n) 0.020000 0.000000 0.020000 ( 0.019067) O(n2) 17.850000 0.080000 17.930000 ( 17.983827)
Đó chỉ là một ví dụ bình thường. Nhưng hãy cố gắng tránh các phép toán, method trong vòng lặp tối đa có thể
Method có thêm ! cũng tương tự như method không có !, chỉ khác mỗi việc chúng sẽ không duplicate một object. Hãy cùng xem lại ví dụ merge! lúc trước và thấy kết quả:
require 'benchmark' def merge!(array) array.inject({}) { |h, e| h.merge!(e => e) } end def merge(array) array.inject({}) { |h, e| h.merge(e => e) } end N = 10_000 array = (0..N).to_a Benchmark.bm(10) do |x| x.report("merge!") { merge!(array) } x.report("merge") { merge(array) } end
user system total real merge! 0.010000 0.000000 0.010000 ( 0.011370) merge 17.710000 0.000000 17.710000 ( 17.840856)
Truy cập trực tiếp một biến instance nhanh hơn khoảng 2 lần so với việc truy cập nó thông qua attr_accessor:
require 'benchmark' class Metric attr_accessor :var def initialize(n) @n = n @var = 22 end def run Benchmark.bm(10) do |x| x.report("@var") { @n.times { @var } } x.report("var" ) { @n.times { var } } x.report("@var =") { @n.times {|i| @var = i } } x.report("self.var =") { @n.times {|i| self.var = i } } end end end metric = Metric.new(100_000_000) metric.run
Kết quả:
user system total real @var 6.980000 0.010000 6.990000 ( 7.193725) var 13.040000 0.000000 13.040000 ( 13.131711) @var = 7.960000 0.000000 7.960000 ( 8.242603) self.var = 14.910000 0.010000 14.920000 ( 15.960125)
require 'benchmark' N = 10_000_000 Benchmark.bm(15) do |x| x.report('parallel') do N.times do a, b = 10, 20 end end x.report('consequentially') do |x| N.times do a = 10 b = 20 end end end
Output:
user system total real parallel 1.900000 0.000000 1.900000 ( 1.928063) consequentially 0.880000 0.000000 0.880000 ( 0.879675)
Để định nghĩa một method động thì cách nào nhanh hơn?: class_eval hay define_method?
require 'benchmark' class Metric N = 1_000_000 def self.class_eval_with_string N.times do |i| class_eval(<<-eorb, __FILE__, __LINE__ + 1) def smeth_#{i} #{i} end eorb end end def self.with_define_method N.times do |i| define_method("dmeth_#{i}") do i end end end end Benchmark.bm(22) do |x| x.report("class_eval with string") { Metric.class_eval_with_string } x.report("define_method") { Metric.with_define_method } metric = Metric.new x.report("string method") { Metric::N.times { metric.smeth_1 } } x.report("dynamic method") { Metric::N.times { metric.dmeth_1 } } end
Output:
user system total real class_eval with string 219.840000 0.720000 220.560000 (221.933074) define_method 61.280000 0.240000 61.520000 ( 62.070911) string method 0.110000 0.000000 0.110000 ( 0.111433) dynamic method 0.150000 0.000000 0.150000 ( 0.156537)
class_eval làm việc chậm hơn, nhưng nó được ưa thích và sử dụng nhiều vì các method được sinh ra từ class_eval lại cho tốc độ nhanh hơn
Thanks for read!