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
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 :
- Caching
- Tối ưu hóa các câu truy vấn database
- giảm việc sử dụng các phương thức tìm động
- 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.