4

Ruby 中使用 freeze 優化效能的時機

 3 years ago
source link: https://blog.niclin.tw/2020/02/24/when-to-use-freeze-in-ruby/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Nic Lin's Blog

喜歡在地上滾的工程師

在 Ruby 中常量(constant)其實是可以變更的

DEFAULT_MESSAGE = "Hello"
DEFAULT_MESSAGE << "123"

puts DEFAULT_MESSAGE.inspect # => "Hello123"

透過使用 freeze 我們可以創建出一開始我們所預期不能更改的常數

DEFAULT_MESSAGE = "Hello".freeze
DEFAULT_MESSAGE << "123" # => FrozenError (can't modify frozen String)

不管在那個程式語言上,有很多時後會造成記憶體洩漏(Memory leak)的部分,通常都是在分配(allocate)後,就算 GC 收走後還是發生記憶體碎片化所造成的效能問題。

因為你創建的 object 雖然會被 GC 收走,但真正提生效能的關鍵其實就是直接減少製造垃圾,把不必要重複 allocate 的 object 放在 memory 裡面。

以 ruby 的世界為萬物皆為 object 來說,假設執行

log("warning")

其實每一次生成字串 "warning" 都是一次 object 的 allocate

所以如果你的程式裡面到處都需要呼叫這個方法,也就表示到處在塞記憶體,在效能上會有更多的開銷。

在 Ruby 上面的解法是,使用 freeze 來把該 object 緩存起來作為備用

Benchmark 效能差異

我們可以跑一下 benchmark 來做實際的測試

這裡使用 benchmark/ips 來做效能測試,這個玩意比原本的 benchmark 還要準確的原因是,他不再只是執行多少次,而是在一定的時間內,嘗試逼出執行的最大次數,對於不同設備來說,會更具參考性。

gem install benchmark-ips

def call(text)
end

Benchmark.ips do |x|
  x.report("normal") { call("hello") }
  x.report("frozen") { call("hello".freeze)  }
end

# Calculating -------------------------------------
#             normal    8.489M (± 2.9%) i/s - 42.495M in   5.010428s
#             frozen   11.014M (± 3.9%) i/s - 55.256M in   5.025667s

可以看到在 5 秒內,frozen 的對象執行次數比 normal 還來的多,這也表示被 frozen 的對象,在取用上速度比每次重新 allocate 一個還要快

用 GC 試試看到底差多少

可以打開 ruby console (irb) 來試試看,透過 GC 回收機制,到底 allocate 多少 object?

GC.start
before = GC.stat(:total_freed_objects)

RETAINED = []
100_000.times do
  RETAINED << "a string".freeze
end

GC.start
after = GC.stat(:total_freed_objects)
puts "Objects Freed: #{after - before}"

# => "Objects Freed: 102014"  (每台電腦跑出來的數字不一定一樣,但結果應該差不多)

可以從這段程式碼執行後看到 Objects Freed 大約有 10 萬筆,原因就是我們在 100_000.times 創建了一大堆的 "a string"

接著我們試試看將 "a string" 改成 "a string".freeze 有什麼差別呢?

GC.start
before = GC.stat(:total_freed_objects)

RETAINED = []
100_000.times do
+  RETAINED << "a string".freeze
end

GC.start
after = GC.stat(:total_freed_objects)
puts "Objects Freed: #{after - before}"

# => "Objects Freed: 2014"

可以看到 object freed 的部分大幅下降了,因為 freeze 後就不需要重複 allocate 一樣的 string 到 memory 內了

看看其他人是怎麼做的

如果你查看 Rails routes 的作法,因為路由用於網頁請求,需要更快速的回應,在這部分可以看到很多的 freeze

# excerpted from https://github.com/rails/rails/blob/f91439d848b305a9d8f83c10905e5012180ffa28/actionpack/lib/action_dispatch/journey/router/utils.rb#L15
def self.normalize_path(path)
  path = "/#{path}"
  path.squeeze!('/'.freeze)
  path.sub!(%r{/+\Z}, ''.freeze)
  path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase }
  path = '/' if path == ''.freeze
  path
end

再來看一下知名的非同步處理套件 sidekiq 也是在 routes 部分 freeze 很多字串

# excerpted from https://github.com/mperham/sidekiq/blob/44614b5da2ee0d89fc63a8ca3e4636f83cd424c6/lib/sidekiq/web/router.rb#L5-L14

module Sidekiq
  module WebRouter
    GET = 'GET'.freeze
    DELETE = 'DELETE'.freeze
    POST = 'POST'.freeze
    PUT = 'PUT'.freeze
    PATCH = 'PATCH'.freeze
    HEAD = 'HEAD'.freeze

    ROUTE_PARAMS = 'rack.route_params'.freeze
    REQUEST_METHOD = 'REQUEST_METHOD'.freeze
    PATH_INFO = 'PATH_INFO'.freeze
    
    ...
  end
end

Ruby 的更新解決了這個問題嗎?

在 Ruby 2.2 後

針對 hash 使用的字串已經有做自動 freeze 的部分了

# 在 Ruby 版本 2.2 之後
user["name"]

# 等同於在 ruby 2.1 版本之前這樣寫(很醜吧)
user["name".freeze]
在 Ruby 2.3 後

可以使用一行程式碼來做到自動 String 的 freeze

# frozen_string_literal: true

這個 Pull Request 可以看到 sidekiq 的改動,將到處的 freeze 拔掉,改用註釋的方式

未來的 Ruby 3.0

目前就 Ruby 之父 Matz 的說法,會希望在 Ruby 3.0 中自動 freeze 所有字串,所以在這個過渡期下,就先用上述兩個解法吧

主要兩招做優化

  1. freeze
  2. one line comment magic => # frozen_string_literal: true

要用單行註釋搞定 freeze 的需要注意,該檔案的所有 string 部分都會被 freeze 住,如果更改的部分就會跳 Error, 所以也不見得所有情況適用,可以斟酌使用

建議可以先以 freeze 來做優化會比較有彈性,如果整個檔案全部都是 freeze 在嘗試改用單行註釋比較保險。


Recommend

  • 6

    如果你有長期看我的部落格文章,應該會知道我強調很多次,Google 非常的重視用戶體驗,只以用戶為最優先考量、重視用戶體驗的網...

  • 8
    • blog.wu-boy.com 3 years ago
    • Cache

    Go 語言內 new 跟 make 使用時機

    大家接觸 Go 語言肯定對

  • 7

    產品優化專案,常見的三個錯誤 在 2021 舉辦了兩次六週 線上產品經理學習營,其中一個訓練題,就是幫服飾品牌的品牌電商,做一個 網站/APP 優化的專案。 因為我並...

  • 3

    Nic Lin's Blog喜歡在地上滾的工程師golang 在用 slice 時要注意,如果先給大小的話可以避免動態的多次 allocate 多的 memory,然後讓底層產生多個 slice假設從 SQL 拿出...

  • 8

    Helper:使用 Helper 的情境多半是:產生的 HTML code 需要與原始程式碼進行一些邏輯混合,但不希望 View 裡面搞得太髒。需要與預設的 Rails 內建的一些方便 Helper 交叉使用。使用 Helper 封裝程式碼可以帶給專案以...

  • 2

    Nic Lin's Blog喜歡在地上滾的工程師常常使用資料庫,但很多環節不清楚效能為什麼不好,也不知道怎麼做預防勝於治療,在老闆的大力推薦下就來上這個課程了,聽說還是老闆的前輩呢...

  • 6
    • blog.niclin.tw 3 years ago
    • Cache

    Service Object 使用時機

    為何要使用Service Object:顧名思義,Service Object是因為有某些類似的特定功能,像是一個『service』,跟資料庫中的model並無直接關係,因此拉出來獨立成為一個class,在邏輯上會更容易管理。不過在文章中有定義了幾個需要使用service object的情況...

  • 8
    • creating-cashflow.blogspot.com 2 years ago
    • Cache

    恐慌再現 CPI 跌眼鏡 是買入時機?

    2022年9月14日星期三 恐慌再現 CPI 跌眼鏡 是買入時機? 昨日美股因為 CPI 數據強差人意而大跌, 納指跌 5.16% ,標普跌 4.32% ,雙雙成為有史以來最大跌...

  • 9

    如果你有長期看我的部落格文章,應該會知道我強調很多次,Google 非常重視用戶體驗,只以用戶為最優先考量、重視用戶體驗的網站主,才能夠真的理解 SEO 該怎麼做,而「網站速度」自然在網站優化上是一個很重要的項目,對於 SEO 以及網站的體驗都有很大的影響。尤其自...

  • 3
    • andyyou.github.io 1 year ago
    • Cache

    Next.js 如何優化匯入函式庫

    Next.js 如何優化匯入函式庫 加速 40% 冷啟動和 28% 建置速度 在 Next.js 最新版本,官方優化了套件匯入,改善了本地開發的效能和正式環境冷啟動的速度。特別適用於使用大型的 Icon ,元件庫,或其他重複 Export 大量模組...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK