Một số trick cải thiện performance trong Ruby
Bài đăng này đã không được cập nhật trong 7 năm
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 để ý
Đừng dùng exception cho câu điều kiện
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.
Phép nối xâu(String)
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 + "\nLENGTH: #{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)
Cẩn thận vơí các phép tính trong vòng lặp
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 + "\nSIZE: #{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ể
Sử dụng !
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)
Sử dụng biến instance
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)
Assign các biến song song chậm hơn so với tuần tự
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 method động
Để đị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!
All rights reserved