12/08/2018, 13:05

TỐI ƯU HÓA CODE RUBY ON RAILS

TỐI ƯU HÓA CODE RUBY ON RAILS Có rất nhiều người cho rằng ruby rất chậm, và mình công nhận là nó chậm thật :v Tuy nhiên chậm ở đây là do rất nhiều nguyên nhân, và hầu hết tất cả nguyên nhân đó đều có cách khắc phục. Các mức độ tối ưu hóa một ứng dụng ruby on rails Design : bạn có thể tối ưu ...

TỐI ƯU HÓA CODE RUBY ON RAILS

Có rất nhiều người cho rằng ruby rất chậm, và mình công nhận là nó chậm thật :v Tuy nhiên chậm ở đây là do rất nhiều nguyên nhân, và hầu hết tất cả nguyên nhân đó đều có cách khắc phục.

Các mức độ tối ưu hóa một ứng dụng ruby on rails

  • Design : bạn có thể tối ưu hóa cấu trúc của hệ thống để nó có thể chạy thông minh hơn
  • Source : Viết code sáng sủa hơn, nhanh hơn
  • Build : tối ưu, cài đặt build, config …
  • Compile : mrbc, jrubyc, rbx complie

Ở phần này mình sẽ chủ yếu đề cập đến cách tối ưu hóa source code, việc này sẽ giúp cho bạn rất nhiều khi viết một ứng dụng ruby on rails tối ưu và tăng hiệu suất của nó lên. Mình sẽ chỉ cho các bạn một số mẹo khi sử dụng các phương thức, đây là những kiến thức mà mình rút ra trong quá trình làm việc.

Benchmark

Để bắt đầu phần này bạn cần sử dụng một công cụ để do lường, tính toán và so sánh . Bạn có thể sử dụng Benchmark, đây là một module trong thư viện chuẩn, vì vậy bạn không cần phải cài đặt bất cứ gems nào để sử dụng nó. Module Benchmark cung cấp các phương thức để đo lường và báo cáo thời gian sử dụng để chạy code.

**Ví dụ 1 ** : đo thời gian chạy biểu thức “a”*1_000_000 với 100 lần chạy

require 'benchmark'

puts Benchmark.measure {
  100.times do
    "a"*1_000_000
  end
}

kết quả là

0.020000   0.010000   0.030000 (  0.029763)

Trong đó

  • user CPU time : 0.020000
  • system CPU time : 0.010000
  • the sum of the user and system CPU times : 0.030000
  • the elapsed real time : 0.029763

Ví dụ 2 : đo thời gian để so sánh với phương thức .bm

require 'benchmark'

n = 500000
Benchmark.bm(7) do |x|
  x.report("for:")   { for i in 1..n; a = "1"; end }
  x.report("times:") { n.times do   ; a = "1"; end }
  x.report("upto:")  { 1.upto(n) do ; a = "1"; end }
end

kết quả là

              user     system      total        real
for:      0.070000   0.000000   0.070000 (  0.068266)
times:    0.050000   0.000000   0.050000 (  0.058727)
upto:     0.070000   0.000000   0.070000 (  0.060960)

Như vậy là về cơ bản bạn đã biết cách dùng benchmark, nếu bạn muốn tìm hiểu thêm thì có thể đọc tại http://ruby-doc.org/stdlib-2.0.0/libdoc/benchmark/rdoc/Benchmark.html

ruby.png

Nâng cấp source code của bạn với một số mẹo sau

  • Mẹo 1 : Proc#call với yeild

Nếu bạn đã là thông thạo Ruby on Rails thì chắc bạn không lạ gì với proc, yeild. Mình khuyên bạn nên sử dụng yeild thay cho Proc#call vì nó có thể nhanh hơn gấp 4 lần.

require 'benchmark'

def fast
	yield
end

def slow(&block)
	block.call
end

n = 500000
Benchmark.bm(7) do |bm|
	bm.report("fast:") do
		n.times do
			fast {
				a = "hello"
			}
		end
	end

	bm.report("slow:") do
		n.times do
			slow {
				a = "hello"
			}
		end
	end
end

kết quả là

              user     system      total        real
fast:     0.100000   0.000000   0.100000 (  0.103637)
slow:     0.380000   0.000000   0.380000 (  0.371526)
  • Mẹo 2 : Block với Symbol#to_proc

Việc sử dụng Symbol#proc thay thế cho Block có thể nhanh hơn từ khoảng 10% đến 15%

require 'benchmark'

n = 50000
Benchmark.bm(7) do |bm|
	bm.report("fast:") do
		n.times do
			(1..100).map(&:to_s)
		end
	end

	bm.report("slow:") do
		n.times do
			(1..100).map{ |i| i.to_s }
		end
	end
end

Kết quả là

              user     system      total        real
fast:     0.930000   0.000000   0.930000 (  0.931761)
slow:     1.030000   0.000000   1.030000 (  1.029847)
  • ** Mẹo 3 : Enumerable#map + Array#flatten so với Enumerable#flat_map**

Việc sử dụng Enumerable#flat_map sẽ nhanh hơn gần gấp đôi so với Enumerable#map + Array#flatten

require 'benchmark'

n = 50000
a = [1, [1, 2, [2, 3]], 3]

Benchmark.bm(7) do |bm|
	bm.report("fast:") do
		n.times do
			a.flat_map{ |i| i = i*2 }
		end
	end

	bm.report("slow:") do
		n.times do
			a.map{ |i| i = i*2 }.flatten(1)
		end
	end
end

Kết quả là

              user     system      total        real
fast:     0.060000   0.000000   0.060000 (  0.063241)
slow:     0.100000   0.000000   0.100000 (  0.097608)
  • Mẹo 4 : Enumerable#reverse + Enumerable#each so với Enumerable#reverse_each

Việc sử dụng reverse_each sẽ giúp bạn tiết kiệm thêm 30% thời gian xử lí.

require 'benchmark'

n = 500000
arr = [1, 2, 3, 4, 5]

Benchmark.bm(7) do |bm|
	bm.report("fast:") do
		n.times do
			arr.reverse_each { |x| x = x*2 }
		end
	end

	bm.report("slow:") do
		n.times do
			arr.reverse.each { |x| x = x*2 }
		end
	end
end
              user     system      total        real
fast:     0.230000   0.000000   0.230000 (  0.232347)
slow:     0.320000   0.000000   0.320000 (  0.319022)
  • Mẹo 5 : Hash#keys + Enumerable#each so sánh với Hash#each_key

Bạn nên sử dụng Hash#each_key thay vì sử dụng đồng thời Hash#keys.each, như vậy bạn có thể tăng tốc gấp đôi, hoặc nhỉnh hơn 1 tí.

require 'benchmark'

n = 50000
hash = {a: 1, b: 2, c: 3}

Benchmark.bm(7) do |bm|
	bm.report("fast:") do
		n.times do
			hash.each_key { |x| hash[x] = hash[x]*2 }
		end
	end

	bm.report("slow:") do
		n.times do
			hash.keys.each { |x| hash[x] = hash[x]*2 }
		end
	end
end
              user     system      total        real
fast:     0.250000   0.020000   0.270000 (  0.270158)
slow:     0.530000   0.000000   0.530000 (  0.537756)
  • Mẹo 6 : Sử dụng Array#sample thay thế cho Array#shuffle.first

Bởi vì khi bạn chạy Array#shuffle, nó sẽ đảo trật tự các phần tử trong mảng một cách ngẫu nhiên, sau đó dùng first để chọn ra một phần tử. Như vậy bạn mất 2 bước. Trong khi với Array#sample thì phương thức này sẽ chọn ngẫu nhiên một phần tử trong mảng cho bạn, nó sẽ tiết kiệm thời gian hơn.

require 'benchmark'

n = 500000
arr = [1, 2, 3, 4, 5]

Benchmark.bm(7) do |bm|
	bm.report("fast:") do
		n.times do
			arr.sample
		end
	end

	bm.report("slow:") do
		n.times do
			arr.shuffle.first
		end
	end
end
              user     system      total        real
fast:     0.050000   0.000000   0.050000 (  0.058147)
slow:     0.210000   0.000000   0.210000 (  0.211353)
  • Mẹo 7 : Sử dụng Hash#merge! thay thế cho Hash#merge nếu có thể

Note : bạn cần phải biết sự khác biệt giữa phương thức merge và merged!, nếu sử dụng merge! thì nó sẽ thay đổi hash đầu vào của bạn, còn merge thì không.

require 'benchmark'

n = 50000
h1 = { "b" => 254, "c" => 300 }

Benchmark.bm(7) do |bm|
	bm.report("fast:") do
		n.times do
			h2 = { "a" => 100, "b" => 200 }
			h2.merge!(h1)
		end
	end

	bm.report("slow:") do
		n.times do
			h2 = { "a" => 100, "b" => 200 }
			h2.merge(h1)
		end
	end
end
              user     system      total        real
fast:     0.030000   0.010000   0.040000 (  0.034328)
slow:     0.100000   0.000000   0.100000 (  0.101218)
  • Mẹo 8 : Nên dùng Hash#[] = thay thế cho Hash#merge!

Hash#merge! sử dụng để merge một hash khác vào, nên nó sẽ không nhanh bằng việc gán Hash với key và value có sẵn. Hash#[]= sẽ nhanh hơn 2.5 lần so với Hash#merge!

require 'benchmark'

n = 50000
arr = [1, 2, 3, 4, 5]

Benchmark.bm(7) do |bm|
	bm.report("fast:") do
		n.times do
			arr.each_with_object({}) do |e, h|
				h[e] = e
			end
		end
	end

	bm.report("slow:") do
		n.times do
			arr.each_with_object({}) do |e, h|
				h.merge!(e => e)
			end
		end
	end
end
              user     system      total        real
fast:     0.070000   0.010000   0.080000 (  0.072902)
slow:     0.170000   0.000000   0.170000 (  0.167375)
  • Mẹo 9 : Sử dụng String#sub thay thế cho String#gsub nếu có thể.

Sự khác biệt giữa String#sub và String#gsub đó là khi replace một string thì phương thức sub chỉ replace một lần, cho chuỗi kí tự tìm thấy đầu tiên. Còn phương thức gsub sẽ replace tất cả các chuỗi kí tự tìm được. Và chính vì thế nên phương thức sub sẽ nhanh hơn, và đặc biệt nhanh hơn với các chuỗi kí tự dài và chuỗi kí tự cần replace nằm ở phía đầu dòng.

require 'benchmark'

n = 50000
str = "http://hellorubyonrails.com"

Benchmark.bm(7) do |bm|
	bm.report("fast:") do
		n.times do
			str.sub('http://', 'https://')
		end
	end

	bm.report("slow:") do
		n.times do
			str.gsub('http://', 'https://')
		end
	end
end
              user     system      total        real
fast:     0.070000   0.000000   0.070000 (  0.068016)
slow:     0.120000   0.000000   0.120000 (  0.118961)
  • Mẹo 10 : Sử dụng String#tr thay thế cho String#gsub nếu có thể

Nếu bạn đơn giản chỉ muốn replace các kí tự bình thường, không phải là regular expression thì bạn có thể sử dụng String#tr. Nó nhanh hơn gấp 4 lần so với phương thức gsub.

require 'benchmark'

n = 50000
str = "abcdefgh"

Benchmark.bm(7) do |bm|
	bm.report("fast:") do
		n.times do
			str.tr('abc', '123')
		end
	end

	bm.report("slow:") do
		n.times do
			str.gsub('abc', '123')
		end
	end
end
              user     system      total        real
fast:     0.030000   0.000000   0.030000 (  0.035378)
slow:     0.130000   0.000000   0.130000 (  0.124897)

Tuy nhiên bạn cần chú ý String#tr sẽ replace từng kí tự đơn theo thứ tự, chứ không phải replace nhiều kí tự cùng lúc. Điếu này rất dễ gây ra lỗi, kết quả không như mong muốn Ví dụ :

2.0.0-p451 :133 > str = "just try your best"
 => "just try your best"
2.0.0-p451 :134 > str.tr('st', 'ST')
 => "juST Try your beST"
2.0.0-p451 :135 > str.tr('jtu', 'JTU')
 => "JUsT Try yoUr besT"
  • Mẹo 11 : Sử dụng gán biến tuần tự thay cho việc gán song song. Trước đây mình cũng thỉnh thoảng gán song song, việc này giúp rút gọn code, tuy nhiên nó lại làm tốc độ xử lí chậm lại 40%
require 'benchmark'

n = 500000

Benchmark.bm(7) do |bm|
	bm.report("fast:") do
		n.times do
			a = 1
			b = 2
		end
	end

	bm.report("slow:") do
		n.times do
			a, b = 1, 2
		end
	end
end
              user     system      total        real
fast:     0.040000   0.000000   0.040000 (  0.045403)
slow:     0.070000   0.000000   0.070000 (  0.069169)
  • Mẹo 12 : Hạn chế sử dụng cách bắt ngoại lệ với rescue nếu có thể Điều này sẽ giúp code của bạn chạy nhanh gấp 4 đến 5 lần. Bạn không nên lạm dụng rescue nếu bạn có thể làm một cách khác như code của mình ở bên dưới.
require 'benchmark'
n = 50000
def fast method_name
	string = "test"
	if string.respond_to?(method_name.to_sym)
		string.send(method_name)
	else
		# do something
	end
end

def slow method_name
	string = "test"
	begin
		string.send(method_name)
	rescue
		#do something
	end
end

Benchmark.bm(7) do |bm|
	bm.report("fast:") do
		n.times do
			fast("hex")
			fast("check")
		end
	end

	bm.report("slow:") do
		n.times do
			slow("hex")
			slow("check")
		end
	end
end
              user     system      total        real
fast:     0.070000   0.000000   0.070000 (  0.063933)
slow:     0.230000   0.000000   0.230000 (  0.234716)

Với các mẹo ở bên trên, bạn có thể khiến cho ứng dụng của bạn chạy nhanh hơn rất nhiều lần. Ngoài ra bạn cũng nên sử dụng đồng thời với các cách tối ưu sau đây :

  1. Caching
  2. Tối ưu hóa các câu truy vấn database
  3. giảm việc sử dụng các phương thức tìm động
  4. Tối ưu code html trên view

Mình sẽ cụ thể hơn nữa các cách nêu trên ở blog sau. Thanks for reading.

0